> ## 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.

# Cloud Chat API

> Base URL, authentication, the instance header, and the conventions every endpoint follows.

The Cloud Chat API lets you read and manage your Cloud Chat data from your own systems. It is a curated surface: a set of endpoints we commit to and version, not a window onto everything the Cloud Chat app does internally. The endpoints available today are listed in the sidebar, and the set grows over time.

Everything on this page applies to all of them. Each endpoint's own page covers what is specific to it.

```text theme={null}
https://api.cloudhumans.com/cloudchat/v1
```

## Two headers, every call

```bash theme={null}
curl https://api.cloudhumans.com/cloudchat/v1/accounts/1/canned-responses \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "cloudchat-instance: 1"
```

**`Authorization`** carries the `id_token` from [`POST /auth/v1/signin`](/api-reference/cloudchat/v1/auth/authentication/sign-in-and-get-a-token) — not the `access_token`, which does not carry the identity we authorize on. One token works across every Cloud Humans API, and expires after the `expires_in` seconds the sign-in response reports.

**`cloudchat-instance`** says which Cloud Chat instance holds your data. Cloud Chat runs on several instances with independent databases, and your company lives on exactly one of them, so the value is fixed for you: set it once in your client's configuration and forget it. You are told it at onboarding, and it is also written into your own token — the [Find your credentials](/api-reference/cloudchat/credentials) page reads it out for you.

<Warning>
  Omit `cloudchat-instance` and the call is rejected before authentication, with a `400` that does not use the normal error envelope:

  ```json theme={null}
  { "error": "<human-readable message>" }
  ```

  Send the *wrong* instance and you get a `401` or a `404` instead: either that instance does not accept your token, or the account you asked for simply isn't in its database. You never reach someone else's data by guessing.
</Warning>

## Signing in is a loop, not a call

`POST /auth/v1/signin` does not always answer with a token. Some accounts have a temporary
password, and some have two-step verification turned on — for those, a `200` carries a
**challenge** instead: no token, and a `session` to answer it with.

So the shape to write is a loop, not a single request. Post credentials, then keep answering
whatever comes back at
[`POST /auth/v1/signin/challenge`](/api-reference/cloudchat/v1/auth/authentication/answer-a-sign-in-challenge)
until a token appears.

To just *get* a token now — challenge and all — the
[Find your credentials](/api-reference/cloudchat/credentials) page runs the whole flow for you and
asks only for the code from the text message.

| The response contains                | What it is                                                 | What you do                                              |
| ------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------- |
| `id_token`                           | Done.                                                      | Use it as `Authorization: Bearer`. Stop.                 |
| `challenge: "SMS_MFA"`               | A code has just been texted to the phone on the account.   | Ask for the code, post it back with the `session`.       |
| `challenge: "NEW_PASSWORD_REQUIRED"` | The password is temporary — first access, or it was reset. | Ask for a new password, post it back with the `session`. |
| `error`                              | Nothing to answer.                                         | Show the failure and start over.                         |

**Branch on the body, not on the status.** Both outcomes are `200`, and both endpoints return
the same two shapes — which is also why answering a challenge can hand you *another* one.
A first-access user who also has two-step verification genuinely goes
`NEW_PASSWORD_REQUIRED` → `SMS_MFA` → token, and each step returns a **new** `session` that
replaces the one you just used.

```bash theme={null}
# 1. credentials in
curl -sX POST https://api.cloudhumans.com/auth/v1/signin \
  -H "Content-Type: application/json" \
  -d '{"email":"you@company.com","password":"..."}'
# {"challenge":"SMS_MFA","session":"AYABeF7x..."}

# 2. the code the user just received, answered against that session
curl -sX POST https://api.cloudhumans.com/auth/v1/signin/challenge \
  -H "Content-Type: application/json" \
  -d '{"challenge":"SMS_MFA","session":"AYABeF7x...","email":"you@company.com","code":"123456"}'
# {"id_token":"eyJ...","access_token":"eyJ...","expires_in":3600,"token_type":"Bearer"}
```

### If you are putting this on a screen

Three things are worth knowing before the first user hits it.

**Send the code as a string.** Codes can start with a zero, and `"012345"` parsed into a JSON
number is a different code. A number is rejected with a `400` rather than coerced, precisely so
this shows up on your first test instead of as one user in ten who "can't log in".

**A wrong code and a dead session look identical.** Both come back
`400 CodeMismatchException`, so the screen cannot honestly say which happened. Offer a retry,
and next to it a way back to the beginning — retrying against an expired session never
recovers, and a user with no way out is stuck on a screen that only ever says the code is wrong.

**The session is not yours to keep.** It belongs to one sign-in attempt, so there is nothing to
persist and nothing to resume — and it stops being accepted well before the token it leads to
would have expired. When that happens the only move is `/auth/v1/signin` again, which sends a
fresh code.

<Note>
  Accounts configured for a challenge type other than these two — an authenticator app, for
  instance — cannot finish signing in through this API, and have to use the Cloud Chat interface
  to get a token.
</Note>

## Accounts are explicit

The account id travels in the path: `/v1/accounts/{accountId}/...`. It has to be an account your token grants membership on, and it has to live on the instance you named — the two are checked together.

That is deliberately unlike the [Claudia API](/api-reference/claudia), where the account is derived from the token alone. Cloud Chat users routinely hold several accounts, so making the caller name one is clearer than picking for them.

## Errors

Every response the API itself produces uses one envelope:

```json theme={null}
{
  "error": {
    "code": "validation_failed",
    "message": "The request payload is invalid.",
    "details": [
      { "field": "short_code", "code": "taken", "message": "has already been taken" }
    ]
  }
}
```

| `code`              | HTTP | When                                                             |
| ------------------- | ---- | ---------------------------------------------------------------- |
| `bad_request`       | 400  | The request shape is wrong                                       |
| `unauthorized`      | 401  | Missing, expired, or malformed Bearer token                      |
| `forbidden`         | 403  | The account is suspended                                         |
| `not_found`         | 404  | The account or the resource is not there for you                 |
| `validation_failed` | 422  | Values rejected by a validation rule; `details` names the fields |
| `internal_error`    | 500  | A failure on our side, already reported to our monitoring        |

Branch on `code`. It is stable. `message` is not: it is localized to your account's language — English, Spanish or Brazilian Portuguese, falling back to English — and the wording changes between releases. `details`, when present, stays in English regardless, because it describes fields rather than speaking to an end user.

### 404 covers "not yours", not just "not there"

One distinction is worth internalizing before you write error handling, because it is the opposite of what most APIs do. **Asking for an account you are not a member of returns `404`, not `403`** — identical to asking for an account that does not exist, same body, and the message stays in English in both so it cannot even leak the account's configured language.

That is on purpose: nobody can walk the `accountId` space to learn which accounts exist. The cost is that a `404` doesn't tell you which of the two happened, so there is nothing to retry — check the `accountId`, the `cloudchat-instance` value, and that your user is actually a member.

`403` is left with exactly one meaning: you *are* a member, and the account is **suspended**. Nothing on it can be read or written until that is resolved.

### 400 is about shape, 422 is about values

A `400` always means the request itself was unreadable — invalid JSON, a missing or non-object payload wrapper, or a query parameter sent as an array or nested object (`?page[]=1`). A `422` means the request was understood and a value was refused. Retrying a `400` unchanged never helps; the fix is always in how the request is built.

## Rate limits

Requests are rate limited per source IP. The limit is not something to hardcode — read it from the response, because every response carries the current state:

| Header                | Meaning                                          |
| --------------------- | ------------------------------------------------ |
| `ratelimit-limit`     | Requests allowed in the current window           |
| `ratelimit-remaining` | How many you have left                           |
| `ratelimit-reset`     | Seconds until the window resets                  |
| `Retry-After`         | On a `429` only: seconds to wait before retrying |

Over the limit you get `429` with a bare `message`, since the request never reached the API:

```json theme={null}
{ "message": "API rate limit exceeded" }
```

Handle it by waiting, not by retrying immediately: sleep for `Retry-After` seconds and try again. If you are running a batch, watch `ratelimit-remaining` as you go and pause when it approaches zero — that keeps you from ever seeing a `429`, and it adapts on its own if the limit changes.
