Errors and limits
A failure is a non-2xx status and an error string you can switch on. The strings are stable; the prose beside them is not.
The shape
{ "error": "invalid_caller" }Some carry context beside the code, which is always additional and never replaces it:
{ "error": "number_limit", "limit": 3 }{ "error": "slow_down", "retry_s": 47 }{ "error": "invalid_direction", "accepted": ["inbound", "outbound"] }Status codes
| Code | Means | Notes |
|---|---|---|
| 200 | Fine | Including some outcomes you might expect to be errors; see below. |
| 201 | Made something | A block, a key, a number. |
| 400 | We can't read it | A malformed number, a missing required field, a value outside the accepted set. The body names which. |
| 401 | Who are you | Missing, unknown, or revoked key. Also session_required on endpoints that need a signed-in person rather than a key. |
| 402 | Your plan won't | A cap you have reached: number_limit, or plan_required for something Guard-only. |
| 403 | Not with that | read_only_key on a write, role_required when your team role is too low, phone_unverified for a text to a phone you have not proved. |
| 404 | No such thing | Also unknown_call when you send turns for a call you never started. |
| 409 | Conflicts with what's there | number_taken when another account protects the number, or not_protected for a number you have not added. |
| 429 | Too fast | With a retry-after header and retry_s in the body. Wait that long. |
| 500 | Ours | Body is { "error": "internal" }. Retry; if it persists, tell us. |
The 200s that aren't successes
Some things that went wrong still answer 200, on purpose. Anything a platform delivers to us as a webhook gets one, because the alternative is a platform retrying, and a retry storm during an attack is the last thing anybody needs. These carry a flag instead:
{ "call_id": "CA7f3e…", "ignored": true, "why": "live_monitor_is_guard" }
{ "call_id": "CA7f3e…", "ignored": true, "why": "call_turn_cap" }{ "call_id": "CA7f3e…", "duplicate": true, "score": 0.1, "action": "allow", "reasons": [] }{ "call_id": "CA7f3e…", "score": 0, "action": "allow", "reasons": ["daily_cap"], "capped": true, "cap": 100 }duplicate means we had already scored that call_id and are handing back the first answer. capped means your plan's daily scoring allowance is spent: the call is recorded and allowed, not scored.
Error strings you'll actually hit
| Code | Means |
|---|---|
| invalid_caller | The calling number could not be parsed as E.164. |
| invalid_number | Your own number could not be parsed as E.164. |
| invalid_to / invalid_from | On an event: one of the two numbers could not be parsed. |
| invalid_direction | Something other than inbound or outbound. |
| invalid_pattern | A block pattern that isn't a number. |
| missing_call_id | An event with no call_id. |
| unknown_call | Turns or an end for a call we never saw start. |
| unknown_type | An event type we don't have. The body lists the ones we do. |
| not_protected | A call to a number that isn't on your account, with register: false. |
| number_limit | Your plan's protected-number cap, with the limit in the body. |
| number_taken | Another account already protects that number. |
| plan_required | A Guard feature on Watch. The body names the feature. |
| read_only_key | A write attempted with a cw_read_ key. |
| webhook_must_be_https | An alert webhook URL that isn't https. |
| slow_down | Rate limited. retry_s says how long. |
| unauthorized | Missing or unknown key. |
Sign-up, sign-in and team endpoints have their own set: weak_password, invite_invalid, token_invalid and the like. They are only reachable from the dashboard and are not part of this reference.
Rate limits
Event ingestion is limited per account at 1,200 events a minute, twenty a second, set well above any real traffic, because our busiest hour is one of your attacks. Past it we accept the request and drop it rather than erroring, so nothing upstream starts retrying.
One call may write 3,000 turns before we stop recording its turns.
Reads are limited per key at 600 a minute, with a 429 and a retry-after past it. That is ten a second on one key, far above what a dashboard or an integration does; it exists for a loop that got away, or a key that got out. Each key has its own allowance, so a busy integration and a wallboard on separate keys never slow each other.
daily_cap above. That is a plan allowance, it resets at midnight UTC, and it is listed with the rest under plans and limits.Retrying
Everything on events is keyed on your call_id, so retrying a delivery you are unsure about is always safe. On 429, wait retry_s. On 500, back off and retry. On a 400, do not retry; the request will fail the same way forever.