Guides
Errors
The error format, how to react to each family of error, and the full list of error codes.
Every error response has the same JSON shape, whatever the route:
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "limit: must be at most 100",
"param": "limit",
"request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
}
}| Field | Meaning |
|---|---|
type | The family of the error (see below). |
code | A stable machine-readable code in snake_case. Branch your code on it. |
message | A human-readable explanation in English. It can change: never parse it. |
param | The offending parameter. Present only for validation errors. |
request_id | Identical to the X-Request-Id response header. Give it to support: it finds your request instantly. |
Error families#
type | Status | What to do |
|---|---|---|
authentication_error | 401 | Fix the key. Do not retry. |
permission_error | 403 | The key is valid but not allowed (suspended organization, live access not enabled, sandbox-only route). Do not retry. |
invalid_request_error | 400, 404, 409, 422 | The request is wrong or conflicts with the current state. Fix it. Do not retry unchanged. |
idempotency_error | 409, 422 | See Idempotency. A 409 idempotency_in_progress can be retried after a short wait. |
rate_limit_error | 429 | Wait for the delay in Retry-After, then retry. See Rate limits. |
api_error | 5xx | Our side. Retry with exponential backoff, using an Idempotency-Key on POST. If it persists, contact support with the request_id. |
const res = await fetch(url, init)
if (!res.ok) {
const { error } = await res.json()
if (error.code === 'user_already_exists') return findExistingUser()
if (res.status === 429) await sleep(Number(res.headers.get('Retry-After')) * 1000)
throw new Error(`${error.code} (request ${error.request_id})`)
}Error codes#
The table below is generated from the API specification, so it always matches the API you are calling. A route's own codes are also listed on its reference page.
| Status | Code | Type | Meaning |
|---|---|---|---|
| 400 | idempotency_key_invalid | idempotency_error | The Idempotency-Key header is empty, too long or contains invalid characters. |
| 400 | invalid_cursor | invalid_request_error | Unknown starting_after cursor. |
| 400 | invalid_json | invalid_request_error | The request body is not valid JSON. |
| 400 | unsupported_version | invalid_request_error | The Tilt-Pay-Version header names a version that does not exist. |
| 401 | api_key_revoked | authentication_error | The API key has been revoked in the Hub. |
| 401 | invalid_api_key | authentication_error | The API key is unknown or malformed. |
| 401 | missing_api_key | authentication_error | The Authorization: Bearer <key> header is missing. |
| 403 | live_feature_unavailable | permission_error | Phone verification is not enabled for live keys. |
| 403 | live_not_enabled | permission_error | Live access is not enabled for your organization yet. |
| 403 | organization_suspended | permission_error | Your organization is suspended. Contact Tilt Pay support. |
| 403 | sandbox_only | permission_error | This route only accepts sandbox keys. |
| 404 | event_not_found | invalid_request_error | No event matches this identifier. |
| 404 | route_not_found | invalid_request_error | No route matches this method and path. |
| 404 | user_not_found | invalid_request_error | Unknown user (or belonging to another project or environment). |
| 404 | webhook_delivery_not_found | invalid_request_error | No webhook delivery matches this identifier. |
| 404 | webhook_endpoint_not_found | invalid_request_error | Unknown endpoint (or belonging to another project or environment). |
| 409 | already_verified | invalid_request_error | The phone number is already verified (live). |
| 409 | delivery_in_progress | invalid_request_error | The original delivery is still pending or being retried. Replay it once it has settled. |
| 409 | endpoint_disabled | invalid_request_error | The webhook endpoint is disabled. Re-enable it before replaying a delivery. |
| 409 | feature_unavailable | invalid_request_error | Live identity verification sessions are not enabled yet. |
| 409 | idempotency_in_progress | idempotency_error | The original request with this Idempotency-Key is still running. Retry shortly. |
| 409 | insufficient_funds | invalid_request_error | Insufficient sandbox balance (payment, withdrawal). |
| 409 | invalid_state | invalid_request_error | KYC is already approved for this user. |
| 409 | kyc_required | invalid_request_error | KYC must be approved (deposit, withdrawal). |
| 409 | no_pending_verification | invalid_request_error | No code is pending: call POST /v1/users/{id}/verification first. |
| 409 | tagname_already_taken | invalid_request_error | This tagname is already used by another user of the project. |
| 409 | user_already_exists | invalid_request_error | This phone number or external_id already exists in the project. |
| 409 | user_suspended | invalid_request_error | The user is suspended. |
| 409 | webhook_endpoint_exists | invalid_request_error | An endpoint with this URL already exists in this environment. |
| 409 | webhook_endpoint_limit | invalid_request_error | The project already has 20 webhook endpoints. |
| 413 | payload_too_large | invalid_request_error | The request body exceeds the maximum size accepted by the API. |
| 422 | idempotency_key_reuse | idempotency_error | The Idempotency-Key was already used with a different method, route or body. |
| 422 | invalid_verification_code | invalid_request_error | Incorrect code (the message tells how many attempts remain). |
| 422 | invalid_webhook_url | invalid_request_error | The URL is not acceptable: https is required (http://localhost is accepted in sandbox, outside production), private or internal addresses, credentials and sensitive ports are refused. |
| 422 | validation_error | invalid_request_error | A field is invalid, not enabled in your published signup flow, or a required field is missing. param names the field. |
| 422 | verification_attempts_exceeded | invalid_request_error | Too many incorrect attempts: request a new code. |
| 422 | verification_expired | invalid_request_error | The code has expired: request a new one. |
| 429 | rate_limit_exceeded | rate_limit_error | Quota, resend delay or hourly limit reached. Wait for Retry-After seconds. |
| 500 | internal_error | api_error | Internal error. Retry; give request_id to support if it persists. |
| 503 | kyc_session_unavailable | api_error | The identity verification session could not be started. Retry shortly. |
| 503 | verification_unavailable | api_error | The phone verification service is temporarily unavailable. Retry shortly: a failed check does not use up an attempt. |