Two headers, every call
Authorization carries the id_token from POST /auth/v1/signin — 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 page reads it out for you.
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
until a token appears.
To just get a token now — challenge and all — the
Find your credentials page runs the whole flow for you and
asks only for the code from the text message.
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.
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.
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.
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, 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:
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 returns404, 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
A400 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:
Over the limit you get
429 with a bare message, since the request never reached the API:
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.