# Errors and rate limits

## Response envelope

Resource endpoints wrap every response in the same envelope:

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Human-readable explanation",
    "field": "X-Tenant-ID",
    "retryable": false
  },
  "meta": {
    "requestId": "...",
    "correlationId": "...",
    "timestamp": "2026-11-02T10:00:00Z",
    "version": "1.0"
  }
}
```

On success `success` is `true` and the result is in `data`. Fields without a value are left out. Include
`meta.requestId` when you contact support.

The token endpoints (`/v1/auth/...`) return plain OAuth-style JSON instead of the envelope.

Always branch on the **HTTP status** first and use `error.code` for detail. Some statuses currently carry a
generic code — for example a missing or invalid bearer token returns `401` with `error.code`
`VALIDATION_ERROR`. Treat any `401` as "get a new token".

## Error codes

| Code | Typical status | Meaning |
| --- | --- | --- |
| `VALIDATION_ERROR` | 400, 422 | A header or field is missing or invalid, or the body doesn't match the schema. Check `field`. Also returned with `401` for a missing or invalid token. |
| `UNAUTHORIZED` | 401 | Token rejected. Some `401`s carry `VALIDATION_ERROR` instead — branch on the status. |
| `FORBIDDEN` | 403 | Missing scope, or a tenant that isn't yours. |
| `NOT_FOUND` | 404 | The resource or tenant doesn't exist. |
| `CONFLICT` | 409 | The request conflicts with the resource's current state. |
| `IDEMPOTENCY_CONFLICT` | 409 | Idempotency key reused with a different body, or still in flight. |
| `RATE_LIMITED` | 429 | Too many requests — see below. |
| `REPLAY_EXPIRED` | 403 | A webhook delivery is too old to replay. |
| `SERVICE_UNAVAILABLE` | 503 | Temporary problem on our side. Retry with backoff. |
| `INTERNAL_ERROR` | 500 | Unexpected error. Retry with backoff; contact us if it persists. |

Some endpoints return more specific codes; the [API reference](/developers/api-reference) lists them.

## Retrying

Retry when `retryable` is `true`, on `429`, and on `5xx`. Use exponential backoff with jitter and keep the
same `X-Idempotency-Key` so a retry never creates a duplicate.

## Rate limits

Limits apply per tenant, separately for reads and writes per hour, plus a per-second burst limit. Your limits
depend on your agreement; ask us if you need them raised.

Rate-limited responses tell you where you stand (token and health endpoints don't carry these headers):

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests left in the window |
| `X-RateLimit-Reset` | Seconds until the window resets |

The headers can occasionally be missing; treat them as advisory and rely on `429` + `Retry-After`.

When you exceed a limit you get `429 RATE_LIMITED` with a `Retry-After` header (seconds). Wait that long
before retrying.
