Docs

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:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "limit: must be at most 100",
    "param": "limit",
    "request_id": "0b2c8f6e-41f9-4c0a-8d55-6f6a3c1d9e21"
  }
}
FieldMeaning
typeThe family of the error (see below).
codeA stable machine-readable code in snake_case. Branch your code on it.
messageA human-readable explanation in English. It can change: never parse it.
paramThe offending parameter. Present only for validation errors.
request_idIdentical to the X-Request-Id response header. Give it to support: it finds your request instantly.

Error families#

typeStatusWhat to do
authentication_error401Fix the key. Do not retry.
permission_error403The key is valid but not allowed (suspended organization, live access not enabled, sandbox-only route). Do not retry.
invalid_request_error400, 404, 409, 422The request is wrong or conflicts with the current state. Fix it. Do not retry unchanged.
idempotency_error409, 422See Idempotency. A 409 idempotency_in_progress can be retried after a short wait.
rate_limit_error429Wait for the delay in Retry-After, then retry. See Rate limits.
api_error5xxOur 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.

Error codes
StatusCodeTypeMeaning
400idempotency_key_invalididempotency_errorThe Idempotency-Key header is empty, too long or contains invalid characters.
400invalid_cursorinvalid_request_errorUnknown starting_after cursor.
400invalid_jsoninvalid_request_errorThe request body is not valid JSON.
400unsupported_versioninvalid_request_errorThe Tilt-Pay-Version header names a version that does not exist.
401api_key_revokedauthentication_errorThe API key has been revoked in the Hub.
401invalid_api_keyauthentication_errorThe API key is unknown or malformed.
401missing_api_keyauthentication_errorThe Authorization: Bearer <key> header is missing.
403live_feature_unavailablepermission_errorPhone verification is not enabled for live keys.
403live_not_enabledpermission_errorLive access is not enabled for your organization yet.
403organization_suspendedpermission_errorYour organization is suspended. Contact Tilt Pay support.
403sandbox_onlypermission_errorThis route only accepts sandbox keys.
404event_not_foundinvalid_request_errorNo event matches this identifier.
404route_not_foundinvalid_request_errorNo route matches this method and path.
404user_not_foundinvalid_request_errorUnknown user (or belonging to another project or environment).
404webhook_delivery_not_foundinvalid_request_errorNo webhook delivery matches this identifier.
404webhook_endpoint_not_foundinvalid_request_errorUnknown endpoint (or belonging to another project or environment).
409already_verifiedinvalid_request_errorThe phone number is already verified (live).
409delivery_in_progressinvalid_request_errorThe original delivery is still pending or being retried. Replay it once it has settled.
409endpoint_disabledinvalid_request_errorThe webhook endpoint is disabled. Re-enable it before replaying a delivery.
409feature_unavailableinvalid_request_errorLive identity verification sessions are not enabled yet.
409idempotency_in_progressidempotency_errorThe original request with this Idempotency-Key is still running. Retry shortly.
409insufficient_fundsinvalid_request_errorInsufficient sandbox balance (payment, withdrawal).
409invalid_stateinvalid_request_errorKYC is already approved for this user.
409kyc_requiredinvalid_request_errorKYC must be approved (deposit, withdrawal).
409no_pending_verificationinvalid_request_errorNo code is pending: call POST /v1/users/{id}/verification first.
409tagname_already_takeninvalid_request_errorThis tagname is already used by another user of the project.
409user_already_existsinvalid_request_errorThis phone number or external_id already exists in the project.
409user_suspendedinvalid_request_errorThe user is suspended.
409webhook_endpoint_existsinvalid_request_errorAn endpoint with this URL already exists in this environment.
409webhook_endpoint_limitinvalid_request_errorThe project already has 20 webhook endpoints.
413payload_too_largeinvalid_request_errorThe request body exceeds the maximum size accepted by the API.
422idempotency_key_reuseidempotency_errorThe Idempotency-Key was already used with a different method, route or body.
422invalid_verification_codeinvalid_request_errorIncorrect code (the message tells how many attempts remain).
422invalid_webhook_urlinvalid_request_errorThe 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.
422validation_errorinvalid_request_errorA field is invalid, not enabled in your published signup flow, or a required field is missing. param names the field.
422verification_attempts_exceededinvalid_request_errorToo many incorrect attempts: request a new code.
422verification_expiredinvalid_request_errorThe code has expired: request a new one.
429rate_limit_exceededrate_limit_errorQuota, resend delay or hourly limit reached. Wait for Retry-After seconds.
500internal_errorapi_errorInternal error. Retry; give request_id to support if it persists.
503kyc_session_unavailableapi_errorThe identity verification session could not be started. Retry shortly.
503verification_unavailableapi_errorThe phone verification service is temporarily unavailable. Retry shortly: a failed check does not use up an attempt.