> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloudhumans.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Update a category

> Change a category's name, description, URL handle or translation link.

Send only the fields you are changing; a body with none of them is a 400, and a field sent as `null` is cleared. This is how a naming mistake gets fixed after the fact — including the slug, which is exactly why the category is addressed here by numeric id and not by slug.

`locale` is **not** writable. An article inherits the language of its category, so changing it would move the language of every article inside without touching one of them; move an article to a category of the other language with [update an article](/api-reference/cloudchat/endpoints/update-an-article) instead. `position` and `parent_category_id` are not writable here either — ordering and nesting stay in the dashboard.

Requires being an administrator of the account or a member of the portal.

export const CloudChatYourValues = () => {
  const STORAGE_KEY = "cloudchat-api-selected-account";
  const CREDENTIALS_HREF = "/api-reference/cloudchat/credentials";
  const [saved, setSaved] = useState(null);
  const [ready, setReady] = useState(false);
  useEffect(() => {
    let value = null;
    try {
      const raw = window.localStorage.getItem(STORAGE_KEY);
      if (raw) {
        const parsed = JSON.parse(raw);
        if (parsed && parsed.instance && parsed.account) {
          value = {
            instance: String(parsed.instance),
            account: String(parsed.account),
            name: typeof parsed.name === "string" && parsed.name ? parsed.name : null
          };
        }
      }
    } catch (error) {
      value = null;
    }
    setSaved(value);
    setReady(true);
  }, []);
  const shell = "not-prose rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 px-4 py-3 mb-6";
  if (!ready || !saved) {
    return <div className={shell}>
        <p className="text-sm text-gray-600 dark:text-gray-400">
          Every request below needs your <code>cloudchat-instance</code> header and your account
          id.{" "}
          <a href={CREDENTIALS_HREF} className="underline underline-offset-2">
            Paste your token
          </a>{" "}
          and they will show up here, ready to copy.
        </p>
      </div>;
  }
  return <div className={shell}>
      <div className="flex flex-wrap items-baseline gap-x-6 gap-y-2">
        <div>
          <span className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
            cloudchat-instance
          </span>
          <span className="ml-2 font-mono text-sm text-gray-900 dark:text-gray-100">
            {saved.instance}
          </span>
        </div>
        <div>
          <span className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
            accountId
          </span>
          <span className="ml-2 font-mono text-sm text-gray-900 dark:text-gray-100">
            {saved.account}
          </span>
        </div>
        {saved.name && <span className="text-sm text-gray-600 dark:text-gray-400">{saved.name}</span>}
        <a href={CREDENTIALS_HREF} className="text-sm text-gray-500 dark:text-gray-400 underline underline-offset-2">
          Change
        </a>
      </div>
    </div>;
};

<CloudChatYourValues />


## OpenAPI

````yaml api-reference/specs/cloudchat/v1.json PATCH /v1/accounts/{accountId}/portals/{portalSlug}/categories/{categoryId}
openapi: 3.0.1
info:
  title: Cloud Chat API
  version: v1
  description: >-
    The curated public surface of Cloud Chat. Every call needs two things: the
    Bearer token from `POST /auth/v1/signin`, and the `cloudchat-instance`
    header that says which Cloud Chat instance your company lives on.


    If you don't know your instance value, the [Find your
    credentials](/api-reference/cloudchat/credentials) page reads it out of your
    own token in the browser.
servers:
  - url: https://api.cloudhumans.com/cloudchat
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Canned Responses
    description: >-
      Canned responses — the shortcuts agents expand while replying. Each one
      belongs to a single account and is identified by its short code.
  - name: Conversations
    description: >-
      Read-only access to the conversations of an account. What you can see
      follows the same rule as the dashboard: an administrator token sees every
      inbox, an agent token only the inboxes it is a member of.
  - name: Help Center
    description: >-
      Portals, categories and articles of the help center — the public FAQ your
      customers read.
  - name: Uploads
    description: Store the images help center articles embed.
  - name: Labels
    description: >-
      Read-only access to the labels an account has defined. A label is a tag
      conversations and contacts carry; this endpoint only lists the label
      definitions themselves.
  - name: Inboxes
    description: >-
      Read-only access to the inboxes a token can see — every inbox of the
      account for an administrator, only its memberships for an agent. Never
      includes the channel's credentials.
  - name: Agents
    description: >-
      Read-only access to the human and AI agents of an account. A hidden admin
      (the platform's own support user) never appears, on the list or by id.
  - name: Teams
    description: >-
      Teams group agents for assignment. Listing and reading are open to any
      member; creating and updating a team, and managing its membership, require
      an administrator token. A system-managed team (provisioned by the
      platform) can be read like any other but never updated.
  - name: Macros
    description: >-
      Macros bundle a sequence of actions an agent runs against a conversation
      from the dashboard. This surface lets you list, read and write macro
      definitions — running one is not part of the v1 contract. A macro is
      either `global` (visible to the whole account) or `personal` (visible only
      to its author); an agent token can only create personal macros.
  - name: Automation Rules
    description: >-
      Read-only access to the account's automation rules — administrator only.
      An agent token gets a 403 on every operation in this group.
  - name: Availability Reasons
    description: >-
      The reasons an agent can go `busy` for, configured per account. Listing is
      open to any member; creating, updating and deleting require an
      administrator token. Deleting is a soft delete — the reason disappears
      from listings but agents' past availability history keeps referencing it.
paths:
  /v1/accounts/{accountId}/portals/{portalSlug}/categories/{categoryId}:
    patch:
      tags:
        - Help Center
      summary: Update a category
      description: >-
        Change a category's name, description, URL handle or translation link.


        Send only the fields you are changing; a body with none of them is a
        400, and a field sent as `null` is cleared. This is how a naming mistake
        gets fixed after the fact — including the slug, which is exactly why the
        category is addressed here by numeric id and not by slug.


        `locale` is **not** writable. An article inherits the language of its
        category, so changing it would move the language of every article inside
        without touching one of them; move an article to a category of the other
        language with [update an
        article](/api-reference/cloudchat/endpoints/update-an-article) instead.
        `position` and `parent_category_id` are not writable here either —
        ordering and nesting stay in the dashboard.


        Requires being an administrator of the account or a member of the
        portal.
      operationId: updateHelpCenterCategory
      parameters:
        - $ref: '#/components/parameters/AccountId'
        - $ref: '#/components/parameters/CloudChatInstance'
        - $ref: '#/components/parameters/PortalSlug'
        - $ref: '#/components/parameters/CategoryId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryUpdateRequest'
            example:
              category:
                name: Getting Started
                description: First steps with the product.
      responses:
        '200':
          description: The category as stored, after the change.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Category'
              example:
                id: 12
                slug: getting-started
                name: Getting Started
                description: First steps with the product.
                locale: en
                position: 10
                parent_category_id: null
                associated_category_id: null
                created_at: '2026-05-02T11:04:17.000Z'
                updated_at: '2026-08-19T18:44:03.771Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Two situations answer with this code: the account is suspended, or
            your user is neither an administrator of the account nor a member of
            the portal. Help center content is portal-scoped — ask an
            administrator to add you to the portal members.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: forbidden
                  message: You are not allowed to perform this action.
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: >-
            A refused value, with `error.details` naming the field: a `slug`
            another category of this portal already uses **in the same locale**
            (`code: "taken"`), a `name` cleared to empty, or an
            `associated_category_id` that is not a category of this portal, is
            this category itself, or would build a chain because this category
            is already the root other categories translate — that last one says
            how many are linked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                error:
                  code: validation_failed
                  message: The request payload is invalid.
                  details:
                    - field: slug
                      code: taken
                      message: >-
                        Another category of this portal already uses this slug
                        in this locale. Omit the slug to have one derived from
                        the name.
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  parameters:
    AccountId:
      name: accountId
      in: path
      required: true
      description: >-
        Your Cloud Chat account. It has to be an account your token grants
        membership on, and it has to live on the instance in the
        `cloudchat-instance` header — the two travel together. Account numbers
        are only unique **within** an instance, so the same number is a
        different company on another instance. Usually a mismatched pair fails
        closed with a 401, because your user does not exist on the other
        instance — but if your identity happens to exist on both, the call
        succeeds against the other company's data, silently. Read it and you are
        looking at the wrong help center; write it and you have stored into the
        wrong account. Send the two values that were given to you together, and
        never try a number to see what answers.
      schema:
        type: integer
        example: 1
    CloudChatInstance:
      name: cloudchat-instance
      in: header
      required: true
      description: >-
        Your Cloud Chat instance ID — an integer, fixed for your company, told
        at onboarding. [The API
        overview](/api-reference/cloudchat/overview#two-headers-every-call)
        explains how instances work, how to find yours, and the errors a wrong
        or missing value produces.
      schema:
        type: integer
        example: 1
    PortalSlug:
      name: portalSlug
      in: path
      required: true
      description: >-
        The portal's slug — the URL-safe handle you see in its public address
        (`/hc/<slug>/...`), not a numeric id. [List
        portals](/api-reference/cloudchat/endpoints/list-portals) returns every
        slug on the account. Note the asymmetry: portals are addressed by slug,
        articles by numeric id.
      schema:
        type: string
        example: acme-help
    CategoryId:
      name: categoryId
      in: path
      required: true
      description: >-
        The `id` returned when the category was created or listed. Numeric —
        never the category slug, and that is on purpose: [updating a
        category](/api-reference/cloudchat/endpoints/update-a-category) can
        rewrite the slug, so a slug would be an address the endpoint itself can
        invalidate.
      schema:
        type: integer
        example: 12
  schemas:
    CategoryUpdateRequest:
      type: object
      description: >-
        The fields to change go under a `category` wrapper. At least one of them
        is required; `locale` is absent on purpose, since articles inherit the
        language of their category.
      required:
        - category
      properties:
        category:
          type: object
          minProperties: 1
          properties:
            name:
              type: string
              example: Getting Started
              description: >-
                New display name shown on the help center. Renaming changes no
                URL — the slug stays as it is, so only touch `slug` when the
                handle itself is wrong.
            description:
              type: string
              nullable: true
              example: First steps with the product.
              description: New short text shown under the name. Send `null` to clear it.
            slug:
              type: string
              example: getting-started
              description: >-
                New URL-safe handle. Rewrite it only to fix a wrong one, because
                it is part of a public URL: the category page moves from
                `/hc/<portal>/<locale>/categories/<old>` to the new handle and
                the old address stops resolving. **Article URLs are not
                affected** — they are built from the portal and the article
                slug, so published article links keep working. One more
                consequence: a saved article filter that names this category by
                `category_slug` stops matching and returns an empty list with no
                error, so switch such filters to `category_id`. A handle another
                category of this portal already uses **in the same locale** is
                refused with `code: "taken"`; the same handle in a different
                locale is allowed.
            associated_category_id:
              type: integer
              nullable: true
              example: 31
              description: >-
                Links this category to the same category in another locale. Pass
                the `id` of any category of the family — the server normalizes
                it to the family root. Send `null` to unlink this category from
                its family, which is how a wrong link gets undone. Must belong
                to this portal. Two refusals that exist only here: a category
                cannot be linked to itself, and a category that is already the
                root other categories translate cannot be linked to another one
                — unlink those first, and the message says how many there are.
    Category:
      type: object
      description: A category of one help center portal.
      required:
        - id
        - slug
        - name
        - locale
        - position
        - created_at
        - updated_at
      properties:
        id:
          type: integer
          example: 12
          description: What goes in `category_id` when creating or updating an article.
        slug:
          type: string
          example: getting-started
        name:
          type: string
          example: Getting Started
        description:
          type: string
          nullable: true
          example: First steps with the product.
        locale:
          type: string
          example: en
          description: >-
            An article created in this category inherits this locale, and
            [translate](/api-reference/cloudchat/endpoints/translate-an-article)
            translates into it. Older categories can carry a locale that is no
            longer among the portal's `allowed_locales`: they are still listed
            and still render, but any write to them is refused until that locale
            is allowed again through [updating the
            portal](/api-reference/cloudchat/endpoints/update-a-portal).
        position:
          type: integer
          nullable: true
          example: 10
          description: >-
            The order the help center renders, ascending. Null on a category
            that never got one — which is most of them, because nothing assigns
            a position automatically. The listing puts those last.
        parent_category_id:
          type: integer
          nullable: true
          example: null
          description: Set when the category is nested under another.
        associated_category_id:
          type: integer
          nullable: true
          example: null
          description: >-
            Root category linking the translations of the same category across
            locales.
        created_at:
          type: string
          format: date-time
          example: '2026-05-02T11:04:17.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-08-10T14:32:05.123Z'
    Error:
      type: object
      description: The error envelope every Cloud Chat API response uses.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: >-
                The stable, machine-readable reason. Branch on this, never on
                `message`.
              enum:
                - unauthorized
                - forbidden
                - not_found
                - validation_failed
                - bad_request
                - internal_error
              example: not_found
            message:
              type: string
              description: >-
                The reason in words, localized to your account's language
                (English, Spanish or Brazilian Portuguese; English when the
                account is set to anything else). Wording changes with the
                account and between releases, so never match on it.


                Two cases stay in English whatever the account is set to:
                `unauthorized`, decided before any account is known, and the
                account-level `not_found`, which must not reveal the account's
                language.
              example: Resource could not be found.
            details:
              type: array
              description: >-
                Present on some rejections — a 400 for a refused query parameter
                or upload mode, for example. One entry per offending field,
                localized to the account's language like `message` — match on
                `code` and `field`, never on the text.
              items:
                type: object
                required:
                  - field
                  - code
                  - message
                properties:
                  field:
                    type: string
                    description: Which request field the rule was about.
                  code:
                    type: string
                    description: Stable, machine-readable reason.
                  message:
                    type: string
                    description: The rule in words.
    ValidationError:
      type: object
      description: A `validation_failed` error, which carries `details`.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - details
          properties:
            code:
              type: string
              enum:
                - validation_failed
              example: validation_failed
            message:
              type: string
              description: >-
                Localized to the account's language, like every other envelope
                message.
              example: The request payload is invalid.
            details:
              type: array
              description: >-
                One entry per rejected field. Unlike the envelope message above,
                the texts come from the model layer and are in English in
                practice (the model layer ships no translations) — match on code
                and field, never on the text.
              items:
                type: object
                required:
                  - field
                  - code
                  - message
                properties:
                  field:
                    type: string
                    description: >-
                      Which request field the rule was about — `short_code` or
                      `content`. A rule that is not tied to either is reported
                      as `base`, so treat `base` as "the payload as a whole"
                      rather than a field you can highlight.
                    enum:
                      - short_code
                      - content
                      - base
                    example: short_code
                  code:
                    type: string
                    description: >-
                      Which rule it broke — `blank` for a missing or empty
                      value, `taken` for a `short_code` already used on the
                      account.
                    example: taken
                  message:
                    type: string
                    example: has already been taken
    GatewayError:
      type: object
      description: >-
        Rejected by the gateway before Cloud Chat saw it, so it does not use the
        `error` envelope.
      required:
        - message
      properties:
        message:
          type: string
          example: API rate limit exceeded
        request_id:
          type: string
          description: Gateway request id. Quote it when reporting a problem.
          example: f3f7567638d4b65a8003057d0c77d275
  responses:
    BadRequest:
      description: >-
        The request itself is malformed, before any value is validated:


        - a body that isn't valid JSON, or one with no `canned_response`
        wrapper,

        - a `canned_response` that isn't an object (`{"canned_response":
        "text"}`),

        - `page`, `per_page`, `search` or `sort` sent as an array or a nested
        object (`?page[]=1`, `?sort[x]=y`) instead of a plain value,

        - **the `cloudchat-instance` header missing or holding a value we don't
        route,**

        - an `accountId` that is not a number — including the literal
        `{accountId}` that some API clients send when the argument is left
        unset. This one answers with the normal envelope and `error.details`
        naming `accountId`, and it is deliberately **not** a 404: a 404 would
        mean the account cannot be reached, while this means the value was never
        an account id at all, so re-read the argument instead of trying other
        numbers.


        **Two different shapes answer with 400, and the difference will bite you
        on day one.** That last case is rejected before the request reaches the
        API, so `error` is a plain **string** instead of the usual object:


        ```json

        { "error": "<human-readable message>" }

        ```


        Every other 400 uses the normal envelope, where `error` is an object
        with `code` and `message`. Reading `error.code` without checking the
        type first throws on the missing-header case — which is the most likely
        400 of your first integration. Check `typeof error === "string"` first,
        or read `error?.code` defensively.


        A 400 always means fix the request shape; it never means a value was
        rejected. Rejected values are 422.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: bad_request
              message: The request is malformed.
    Unauthorized:
      description: >-
        No Bearer token, or one that is expired, malformed, or not a Cognito
        token. Sign in again for a fresh `id_token`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: >-
                Authentication is required. Send a valid Bearer token in the
                Authorization header.
    NotFound:
      description: >-
        The resource is not there for you. Three situations answer with this
        code, and the first two apply to **every** endpoint — the account is
        resolved before anything else runs, so a list or a create fails this way
        too:


        - the account does not exist on this instance,

        - the account exists but your token has no membership on it,

        - on an endpoint that names one, the resource it names (a canned
        response, a portal, an article) does not exist where the path says it
        does.


        The first two are deliberately indistinguishable — same status, same
        body, and the message stays in English in both, since translating it
        would reveal the account's configured language. So you cannot use this
        endpoint to find out which account ids exist. The third case is
        localized like every other error, which is safe because you only reach
        it once membership is established.


        Either way a 404 is not worth retrying. Check the `accountId`, the
        `cloudchat-instance` value, and that your user is a member of the
        account.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: not_found
              message: Resource could not be found.
    TooManyRequests:
      description: >-
        Rate limited per source IP. Note the envelope: this one is `{ "message":
        ... }`, because the request never reached the API.


        Don't hardcode the limit — read it from the response. `Retry-After` says
        how many seconds to wait, and every response (not just this one) carries
        `ratelimit-limit`, `ratelimit-remaining` and `ratelimit-reset`, the last
        in seconds.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/GatewayError'
          example:
            message: API rate limit exceeded
    InternalError:
      description: >-
        Something failed on our side. The response never carries the underlying
        error, but it is reported to our monitoring automatically — no need to
        file anything for a one-off.


        Retrying is reasonable, with one caveat on `POST`: a 500 does not prove
        the write did not happen, so a retry can come back `422` with
        `short_code` already taken. That 422 means the first attempt succeeded —
        list the account and confirm before treating it as a failure.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: internal_error
              message: An unexpected error occurred. Please try again later.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        The `id_token` from `POST /auth/v1/signin`, sent as `Authorization:
        Bearer <id_token>`. Not the `access_token` — that one does not carry the
        identity Cloud Chat authorizes on.

````