Documentation
Errors
One envelope, one stable code to branch on — and a rule for the codes this table doesn't list yet.
Every non-2xx response has the same body:
{
"error": {
"code": "resend_too_soon",
"message": "A code was just sent to this number. Try again in 18s."
}
}
Branch on code. It is stable. message is written for a human reading your logs and may be reworded without notice.
| Code | Status | What happened |
|---|---|---|
unauthorized | 401 | Missing, malformed, unknown, revoked, or expired key |
forbidden | 403 | The key lacks the required scope |
invalid_request | 400 | A field failed validation; message names it |
invalid_phone | 400 | phone could not be read, or country is unsupported |
invalid_channel | 400 | channel is not voice or sms |
missing_target | 400 | check was sent without a verification_id or phone |
not_found | 404 | No such verification in your workspace |
insufficient_balance | 402 | Out of Callable tokens — top up to keep sending |
no_phone_number | 409 | Your workspace has no number to call from |
no_sms_number | 409 | Your workspace has no SMS-capable (mobile) number |
sms_sender_unavailable | 409 | Your SMS number could not be used — contact support |
voice_unconfigured / sms_unconfigured | 409 | That channel is not available — contact support |
verification_expired | 409 | The code's time ran out |
verification_failed | 409 | The attempt budget was spent |
verification_canceled | 409 | Superseded by a newer code, or cancelled |
verification_undelivered | 409 | The code was never delivered — request a new one |
resend_too_soon | 429 | Inside the 30-second resend cooldown |
phone_rate_limited | 429 | 5 codes to this number in the last hour |
workspace_rate_limited | 429 | Your workspace's hourly send cap |
too_many_attempts | 429 | This verification's attempts are spent |
check_conflict | 409 | Two checks for one verification arrived at once — retry |
carrier_rejected | 502 | We tried; the carrier did not accept it |
provider_unreachable | 502 | We could not reach the delivery network |
delivery_failed, provider_<number> | 502 | The delivery network refused it. The number is its HTTP status, for support to read |
storage_failed, internal_error | 500 | Our fault. Safe to retry |
method_not_allowed | 405 | Wrong HTTP verb for that path |
Treat an unlisted code as its status class. This table covers what you will realistically see, but it is not a closed set — delivery networks surface new failure modes and we pass them through rather than flattening them into something vaguer. Write your switch over the codes you handle specially and let everything else fall through to a status-based default: 4xx is your request, 5xx is ours, 429 means wait. A client that throws on an unrecognised code is a client that breaks on a Tuesday.
A wrong code is not an error
POST /v1/otp/check returns 200 with "verified": false when the code does not match. Mistyping is the expected outcome of a verification flow, not a failed request — branch on the field, not on the status code. Only genuine failures use the error envelope.
The rate limits behind resend_too_soon, phone_rate_limited and workspace_rate_limited are worth reading before you design your retry behaviour.