Errors
One envelope, a stable code to branch on, and a message you can show a human.
The shape
{
"error": {
"type": "permission_error",
"code": "scope_missing",
"message": "This key is missing the `customers:write` permission.",
"request_id": "req_1f4e2b0b9f5a2c1d0f7a41c8"
}
}Branch on code. It is stable and will not change wording underneath you. type is the coarse family, useful for generic handling. message is written to be readable by whoever ends up staring at it.
Always keep the request id
Every response — success or failure — carries x-kleos-request-id, and every failure repeats it in the body. Log it. It is the single fastest way for Kleos support to find the exact request you are asking about, and the same id appears in your console's activity log.
Codes
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_api_key | The key is malformed, unknown, or not a Kleos key. |
| 401 | key_revoked | The key was revoked. Create a new one in the console. |
| 401 | key_rotated | The key was rotated and its grace window has closed. Deploy the replacement. |
| 401 | key_expired | The key passed its expiry date. |
| 403 | scope_missing | The key is valid but lacks the permission this endpoint needs. |
| 403 | ip_not_allowed | The request came from an address outside the key's allowlist. |
| 403 | emporos_suspended | The account is suspended. Your Kleos contact can say why. |
| 403 | live_not_enabled | A live key was used before the account was approved for live traffic. |
| 404 | customer_not_found | No such customer in this mode. |
| 404 | call_not_found | No call or queued call of yours matches that id. |
| 404 | event_not_found | No event of yours matches that id in this mode. |
| 400 | country_not_supported | The customer's country is not a market Kleos opens. `GET /v1/coverage` lists the ones it does. |
| 400 | customer_required | A call was queued without saying which of your customers it is for. |
| 400 | phone_required | A call was queued without a number to call. |
| 400 | invalid_phone | The number could not be read as a real number for that customer's country. |
| 400 | notes_too_long | `notes` exceeded 2,000 characters. |
| 400 | external_ref_too_long | `external_ref` exceeded 128 characters. |
| 409 | call_not_cancellable | The call has already been claimed, dialled or cancelled. |
| 500 | provisioning_not_recorded | The provisioning request was not recorded, so nothing was started. Retry safely. |
| 500 | onboarding_link_not_created | The onboarding link was not created and nothing was sent. Retry safely. |
| 401 | missing_session_token | An assistant request arrived with no session token. |
| 401 | invalid_session_token | The session token is malformed, unknown, or not a session token. |
| 401 | session_expired | The session passed its expiry. Mint another from your backend. |
| 401 | session_revoked | The session was revoked. Mint another from your backend. |
| 429 | session_exhausted | The session reached its message ceiling. Mint another. |
| 400 | end_user_ref_required | A session was requested without saying who it is for. |
| 400 | ttl_out_of_range | `ttl_seconds` was outside 60–3600. It is refused, never quietly clamped. |
| 400 | session_selector_required | A revocation named neither `session_id` nor `end_user_ref`. |
| 400 | message_required | An assistant message was empty. |
| 400 | message_too_long | An assistant message exceeded 4,000 characters. |
| 402 | insufficient_balance | The balance cannot cover this. Top up in the console. |
| 503 | chat_unavailable | The assistant produced no answer. Nothing was charged. Retry. |
| 409 | idempotency_key_reused | The same idempotency key arrived with a different body. |
| 429 | rate_limit_exceeded | Too many requests. `Retry-After` says how many seconds to wait; the response also carries the window's limit and reset. |
| 400 | invalid_limit | A spending or concurrency limit was negative, fractional, or not a number. |
| 400 | invalid_json | The request body was not valid JSON. |
| 400 | subject_required | A data-subject request named no person, or named one in a form we could not read. |
| 400 | confirmation_required | An erasure arrived without `confirm: true`. Nothing was changed. |
| 404 | subject_not_found | No records for that person under any of your customers. Deliberately the same answer whether we hold nothing about them or hold it for a different controller. |
| 503 | dsar_unavailable | A data-subject request could not be run. Nothing was read and nothing was changed — retry. |
| 503 | tax_profile_unavailable | Your tax details could not be read. Nothing changed — retry. |
| 400 | invalid_number_kind | A number order asked for something other than "number" or "caller_id". |
| 400 | caller_id_number_required | A caller ID was ordered without `display_number` — your customer's own number, the one their prospects recognise. |
| 400 | invalid_display_number | `display_number` could not be read as a real number. Send it in E.164. |
| 422 | number_unavailable_in_country | A number cannot be obtained in that country today. The message names what is missing and who owes it. |
| 409 | number_already_rented | That customer already rents one of these. Release it first, or you would be billed for both. |
| 422 | number_not_obtained | No number was obtained, and nothing was charged. |
| 500 | number_not_recorded | A number was obtained but could not be recorded, so nothing was charged. Retry safely. |
| 402 | charge_failed | The number was obtained but the first month could not be charged. Support has the details. |
| 500 | rental_not_recorded | The number was obtained and charged but the rental was not recorded. Contact support — do NOT retry, a retry would charge again. |
| 404 | number_not_found | No such number for that customer in this mode. |
| 400 | release_not_confirmed | A release arrived without `confirm_phone_number`, or it did not match. Nothing was released — releasing is permanent, so the URL alone is not enough. |
| 409 | release_needs_support | This number cannot be released automatically. It has NOT been released. |
| 502 | release_failed | The number could not be handed back, so it has NOT been released and your rental is unchanged. |
| 500 | release_not_recorded | The number was handed back but the rental was not updated. Contact support — do not retry. |
| 500 | endpoint_not_described | Kleos could not establish which permission this endpoint requires, so it refused rather than guess. Nothing was read and nothing was done. You will never see this from a documented endpoint. |
| 401 | session_endpoint | An API key was sent to an endpoint that is opened by an end-user session token instead. Mint a session from your backend and send that. |
| 500 | api_error | Something failed on our side. Nothing was charged. |
Refusals are logged, not hidden
Every refusal is written to your account's activity log with its reason code, visible at console.kleos.click. “Kleos said no at 14:02 because the key was missing calls:write” should be a two-second answer, not a support thread.
Retrying
5xx and network timeouts are worth retrying with backoff — send an idempotency key and a retry cannot duplicate anything. 4xx is not worth retrying: the same request will be refused the same way until you change something.
When a 500 comes back, nothing was charged. Kleos does not bill for a request it could not complete.