# Errors and limits

Every Tenant API endpoint uses a single error format and standard HTTP status codes. The full list of endpoints and response examples is in the [API reference (OpenAPI)](/en/api/api-reference).

## Error format [#error-format]

Error responses are JSON with a single shape:

```json
{
  "success": false,
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "Request validation failed",
    "param": "flow_id",
    "details": [
      {
        "field": "flow_id",
        "code": "required",
        "message": "flow_id is required"
      }
    ],
    "request_id": "req_abc123",
    "retryable": false
  }
}
```

**Field reference:**

| Field | Description |
|------|----------|
| `success` | Always `false` on error |
| `error.type` | Error category, derived from the HTTP status: `authentication_error` (401), `authorization_error` (403), `not_found_error` (404), `rate_limit_error` (429), `service_unavailable` (503), `api_error` (5xx), `invalid_request_error` (other 4xx) |
| `error.code` | Error code (for example, `validation_error`, `not_found`, `unauthorized`, `conflict`) |
| `error.message` | Short error description (for developers and logging) |
| `error.param` | Parameter that caused the error (for validation errors) |
| `error.details` | Per-field error array (for validation errors) |
| `error.request_id` | Request identifier (for support enquiries) |
| `error.doc_url` | Link to the relevant documentation section (when available) |
| `error.retryable` | Whether the request is safe to retry (`true` for some 5xx errors and timeouts) |

## HTTP status codes [#http-status-codes]

| Code | Description |
|-----|----------|
| **200** | Request succeeded |
| **201** | Resource created |
| **400** | Bad request (error in body or parameters) |
| **401** | Authentication failed (HMAC signature / JWT token missing or invalid) |
| **403** | Forbidden (insufficient scope or attempt to access another tenant's resource) |
| **404** | Resource not found (session, applicant, link, etc.) |
| **409** | Conflict (for example, the review task is already assigned or the decision is already made) |
| **410** | Resource no longer available (for example, session expired) |
| **422** | Field validation failed |
| **429** | Rate limit exceeded; the **Retry-After** header indicates how long to wait |
| **500** | Internal server error; if `error.retryable = true`, the request can be retried |

## Rate limiting [#rate-limiting]

Limits apply per IP address and per API key. Default values:

| Scope | Requests/sec | Requests/min |
|---------|-------------|-------------|
| Per IP address | 20 | 200 |
| Per API key | 40 | 400 |

When the limit is exceeded, the server returns HTTP 429 Too Many Requests. Response headers:

| Header | Description |
|-----------|----------|
| `X-RateLimit-Limit` | Maximum requests in the current window |
| `X-RateLimit-Remaining` | Remaining requests |
| `X-RateLimit-Reset` | Unix time at which the limit resets |
| `Retry-After` | Seconds until the next allowed attempt |

**Recommendation:** apply exponential backoff on retries and honour the `Retry-After` header.

## Common errors [#common-errors]

### `invalid_signature` (401) [#invalid_signature-401]
Invalid HMAC signature. Recheck the signature construction (see [Authentication](/en/api/authentication)).

### `invalid_signature` (401) — clock drift [#invalid_signature-401--clock-drift]

If `X-Timestamp` deviates from the server time by more than 5 minutes, the server also returns `invalid_signature` (there is no separate `timestamp_expired` code). Confirm that the client clock is synchronised (NTP).

### `unauthorized` (403) [#unauthorized-403]
The API key does not have sufficient scopes. Review the key scopes in the admin panel.

### `not_found` (404) [#not_found-404]
The requested resource (applicant, session, link, etc.) does not exist or belongs to another tenant.

### `link_already_consumed` (409) [#link_already_consumed-409]
The verification link has already been consumed. Create a new link.

### `link_expired` (410) [#link_expired-410]
The verification link expired. Create a new link with a longer `ttl_seconds`.

### `validation_error` (422) [#validation_error-422]
Request fields failed validation. Per-field details are in the `error.details` array.

### `rate_limit_exceeded` (429) [#rate_limit_exceeded-429]
Rate limit exceeded. Honour the `Retry-After` header and apply exponential backoff.

### `internal_server_error` (500) [#internal_server_error-500]
Internal server error. Check `error.retryable` — when `true`, the request can be retried.

## OTP verification: limits and errors [#otp-verification-limits-and-errors]

OTP confirms a phone number or email address during a KYC session.

### OTP code parameters [#otp-code-parameters]

| Parameter | Value |
|----------|----------|
| Code length | 6 digits |
| Code lifetime | Phone: 10 minutes by default. Email: 15 minutes by default. Flow step configuration can override the lifetime within the supported range. |
| Minimum resend interval | 15 seconds by default; flow step configuration can override it |
| Maximum entry attempts | 10 |

### OTP errors [#otp-errors]

On failed verification, OTP endpoints return the standard error envelope with an OTP error code and a text message explaining the reason. When available, the remaining attempt count is included in `error.details` as `attempts_remaining`.

#### `rate_limit_exceeded` (429) [#rate_limit_exceeded-429-1]
OTP send rate limit exceeded. Wait until the limit resets (see the `Retry-After` header).

#### `otp_invalid` / `otp_expired` (400/422) [#otp_invalid--otp_expired-400422]
The code expired (older than 10 minutes). Request a new code through `POST .../otp/send`.

#### `otp_invalid` (400/422) [#otp_invalid-400422]
The entered code is wrong. The remaining attempt count is in `error.details` when the endpoint returns it.

#### `otp_invalid` (400/422), max attempts exceeded [#otp_invalid-400422-max-attempts-exceeded]
All 10 attempts were used. The code is invalidated — request a new one through `POST .../otp/send`.

---

**See also:** [Authentication](/en/api/authentication) (codes 401, 403), [API reference (OpenAPI)](/en/api/api-reference) (full endpoint and response code specification).
