# Errors and rate limits
Source: https://nordvec.com/cs/docs/guides/errors-and-rate-limits

The one error envelope every failed request returns, the rate-limit headers, and how to retry a write safely.



Every endpoint fails the same way, so a client handles errors, rate limits and
retries once and reuses that code everywhere, including over
[MCP](/docs/guides/mcp).

## The error envelope [#the-error-envelope]

Every non-2xx response is one JSON object:

```json
{
  "defined": false,
  "code": "TOO_MANY_REQUESTS",
  "message": "Too many requests",
  "data": { "reason": "rate_limit.exceeded", "retryAfterMs": 12000 }
}
```

* `code` is the HTTP-level error, for example `UNAUTHORIZED`, `FORBIDDEN`,
  `NOT_FOUND`, `BAD_REQUEST` or `TOO_MANY_REQUESTS`.
* `data.reason`, when present, is a finer machine-readable reason such as
  `auth.key_not_found` or `rate_limit.exceeded`. Branch on it rather than on
  `message`, which is for people and may change.
* `defined` is `true` when the operation lists that error in the
  [API reference](/docs/api), and `false` for errors any request can meet
  (authentication, rate limits, an unknown route).
* A validation failure answers `BAD_REQUEST` with the problems in
  `data.formErrors` and `data.fieldErrors`.

Every response also carries an `X-Request-ID`. Quote it when you contact
support, and we can find that exact request.

## Common statuses [#common-statuses]

| Status | Code                    | What to do                                                            |
| ------ | ----------------------- | --------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`           | Fix the request; `data.fieldErrors` names the fields                  |
| `401`  | `UNAUTHORIZED`          | Send a valid key or session                                           |
| `403`  | `FORBIDDEN`             | The key lacks the scope or the role the operation needs               |
| `404`  | `NOT_FOUND`             | The resource does not exist, or you are not allowed to see it         |
| `409`  | `CONFLICT`              | A duplicate write is still in flight; retry shortly                   |
| `413`  | `PAYLOAD_TOO_LARGE`     | The request body is over 1 MB; split a bulk push into smaller batches |
| `422`  | `UNPROCESSABLE_CONTENT` | The request is well formed but cannot be applied                      |
| `429`  | `TOO_MANY_REQUESTS`     | Wait for `Retry-After`, then retry                                    |

## Rate limits [#rate-limits]

Every response states the limit it was counted against, in two forms:

* the `X-RateLimit-*` headers;
* the IETF structured fields `RateLimit` (live state: `r` is the requests
  remaining, `t` the seconds until the window resets) and `RateLimit-Policy`
  (the quota: `q` is the limit, `w` the window in seconds).

A `429` also carries `Retry-After` in seconds and `data.retryAfterMs`. Wait at
least that long before the next request; retrying sooner is counted and
refused again.

## Retrying writes safely [#retrying-writes-safely]

A write operation that lists an `Idempotency-Key` header in the
[API reference](/docs/api) can be retried without doing the work twice. Send
one key per logical write and repeat the same key on every retry:

* the same key with the same body within 24 hours replays the stored response;
* the same key with a different body is refused with `422`;
* a duplicate that arrives while the first is still running gets `409`.

An operation without the header is not idempotent, so retry it only when you
know the first attempt did not land.
