Start Here
Handling Errors
The error shape
Every failure has the same body: a machine-readable code and a human-readable message meant for your logs.
{
"error": {
"message": "otp_id: Invalid OTP ID",
"code": "VALIDATION_ERROR"
}
}Error codes
VALIDATION_ERRORThe body failed validation. The message names each failing field, e.g. "phone: phone is required when channel is \"sms\"".
UNAUTHORIZEDMissing, malformed, unknown or revoked API key.
NOT_FOUNDNo such record, a record belonging to another workspace, or an unknown route. The first two are indistinguishable on purpose.
CONFLICTAn Idempotency-Key was reused with a different body or on a different route.
IDEMPOTENCY_CONFLICTA request with the same Idempotency-Key is still being processed. Comes with Retry-After: 2.
UNSUPPORTED_DESTINATIONSMS is not available for the number’s country yet — send on the email channel. A broadcast gets this only when no number in the list is SMS-routable; otherwise those numbers are left out of totalCount.
QUOTA_EXCEEDEDA broadcast would exceed your workspace's daily or in-flight recipient quota. The message says how much room is left. No Retry-After.
RATE_LIMIT_ERRORA rate limit was hit, or an OTP ran out of verify attempts. Comes with Retry-After when a budget was exceeded.
INTERNAL_ERRORSomething failed on our side. In production the message is generic; the detail stays in our logs.
SERVICE_UNAVAILABLEWe could not check your Idempotency-Key, so nothing was done.
What to retry
Retry 429 RATE_LIMIT_ERROR, 409 IDEMPOTENCY_CONFLICT, 500 and 503 — after the Retry-After header when one is present, with backoff otherwise. Retry 429 QUOTA_EXCEEDED only once earlier broadcasts have drained or the 24-hour window has moved on. Do not retry other 4xx responses unchanged; they will fail the same way.
Idempotency-Key you used the first time. That is what makes a retry after a timeout safe — see Idempotency.
