> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nextmoca.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Every status this API returns, what causes it, whether to retry — and the two cases that are not errors at all.

## Two error shapes, and how to tell them apart

Some failures are produced by the service; some are produced by the gateway in
front of it, before the service is reached. They do not share a body shape.

<CodeGroup>
  ```json Service error theme={null}
  {
    "error": "request_too_large",
    "detail": "input_tokens 143820 exceeds the limit of 100000",
    "limit_name": "input_tokens",
    "limit": 100000,
    "observed": 143820
  }
  ```

  ```json Gateway error theme={null}
  { "message": "Too Many Requests" }
  ```
</CodeGroup>

**Match on the HTTP status code, and — for service errors only — on `error`.**

* `error` is a stable machine-readable code: `bad_request`, `request_too_large`,
  `engine_error`, `not_found`.
* `detail` is for a human reading a log. It is **not stable**; do not parse it.
* The gateway's key is capitalised inconsistently between cases (`message` in
  some, `Message` in others) because those are the platform's own bodies passed
  through unchanged. Do not build on that key at all.

## The table

| Status | `error`                 | Cause                                              | Retry                                          |
| ------ | ----------------------- | -------------------------------------------------- | ---------------------------------------------- |
| `400`  | `bad_request`           | Malformed JSON, missing required field, wrong type | **No** — fix the payload                       |
| `401`  | *(gateway)*             | **No `Authorization` header at all**               | **No** — fix the client                        |
| `402`  | *(gateway)*             | Credit exhausted — **not serving yet**, see below  | **No** — add credit                            |
| `403`  | *(gateway)*             | Credential presented and refused                   | **No** — no cause is transient                 |
| `404`  | `not_found`             | Authenticated, but no such route                   | **No**                                         |
| `413`  | `request_too_large`     | Over the per-request token or record ceiling       | **No** — shrink or split                       |
| `429`  | *(gateway)*             | Rate/burst, or daily quota                         | **Depends** — see below                        |
| `500`  | `engine_error`, or none | The service could not produce a response at all    | **Only if the cause is transient** — see below |

## 401 vs 403

These mean genuinely different things and are worth wiring to different
alerts.

<CardGroup cols={2}>
  <Card title="401 — the header was missing entirely" icon="circle-question">
    No `Authorization` header at all. Rejected at the gateway **before anything of
    ours runs**. This is almost always a deployment mistake: an unset environment
    variable, a proxy stripping the header, a misspelled header name.
  </Card>

  <Card title="403 — a header arrived and was refused" icon="ban">
    Anything else. **Including an `Authorization` header that is present but not
    `Bearer`-shaped** — that reaches authorization and is denied, it does not
    become a 401. Other causes: an unknown, revoked or malformed key; a suspended
    organisation; a control plane too stale to vouch for the key.
  </Card>
</CardGroup>

The `403` body deliberately does not say which cause applied. An error that
distinguishes "no such key" from "suspended organisation" is an oracle for
anyone probing keys — so the message is uniform on purpose, and the way to find
out which one you hit is to ask us.

<Note>
  **A `403` right after minting a key is usually a provisioning race, not a bad
  key.** Associating a new key with its plan is not instantaneous — it can take a
  minute or two. Retry once after a short delay before concluding anything.
</Note>

## Retrying a 429

Both a rate limit and a daily quota surface as `429`, and they want opposite
behaviour:

| Body                               | Meaning       | Retry                                                                      |
| ---------------------------------- | ------------- | -------------------------------------------------------------------------- |
| `{"message": "Too Many Requests"}` | Rate or burst | **Yes** — exponential backoff with jitter. Usually clears in milliseconds. |
| `{"message": "Limit Exceeded"}`    | Daily quota   | **No** — will not clear until the quota period rolls.                      |

Branch on the body here if you can — it is the one place a gateway message is
worth reading, because the status code alone cannot distinguish "retry shortly"
from "retry tomorrow". Treat it as a hint rather than a contract: it is the
platform's string, not ours, so **default to capped exponential backoff when the
body is missing or unrecognised**, and let a circuit breaker stop the retries
rather than relying on the message to tell you to.

Your plan limits are approximate in both directions
([why](/limits#what-each-limit-actually-does)), so **a `429` is a normal
operating condition, not an incident**. Build backoff in from the first day.

## 402: not serving yet

`402` is reserved for "your organisation is out of credit", and is documented
here ahead of time so client libraries reserve the case rather than folding it
into `403`. Until it ships, a credit problem does not produce a distinct status.

<Note>
  **Reserve the case; do not test against it.** Nothing returns `402` today.
</Note>

## The two cases that are not errors

### Reading a stand-down

Needlepath can decide that trimming would not pay and hand your context back
essentially intact. This is a **`200`** with:

```json theme={null}
{
  "fallback_used": true,
  "records_selected": 3,
  "tokens_saved": 0,
  "gate": { "engaged": false, "reason": "standdown:high_drift", "signals": {} },
  "format_metrics": { "engine_fallback_reason": "nothing_cleared_the_selection_floor" }
}
```

Send what you were going to send. A stand-down is the engine declining to make
things worse, and on some workload shapes it is the *common* outcome rather than
a rare one — do not alarm on it and do not treat a `tokens_saved` of zero as a
failure.

<Warning>
  **`fallback_used: true` does not tell you why.** Three different things produce
  it:

  1. a deliberate gate stand-down — `gate.engaged` is `false` with a `reason`;
  2. an empty or unusable record set;
  3. an engine-internal exception — `selection_error` is populated.

  If you care about the difference, read `gate`, `selection_error` and
  `format_metrics.engine_fallback_reason`. `fallback_used` alone conflates a
  healthy decision with an outage.
</Warning>

Two more traps in the same area:

* **`safety` is `null` on the plain fixed-budget path**, because no coverage
  verdict is computed there. A null `safety` means "no verdict ran", never "the
  verdict was fine".
* **An empty `selected[]` is not proof that nothing was selected.** It is also
  empty when you sent `return_per_record: false`. Read `records_selected`.

### An engine failure returns 200, not 5xx

If the *selection pass itself* raises, you get a **`200`** with
`selection_error` populated and a usable full-context body — because the correct
client behaviour in that case is to send your original context, and the body
already contains it.

<Warning>
  **This decides how you monitor us.** An availability metric defined on "non-5xx"
  will report perfect health straight through an engine outage. Alarm on
  `selection_error` being non-null, not on status code.
</Warning>

A `500` is reserved for the cases where no response could be produced at all.

Two shapes, and they want different handling:

* **`{"error": "engine_error", …}`** — ours. If the cause is one of the two
  rough edges below (an unresolvable operating point, an unknown `kind`) it is
  **deterministic and retrying it unchanged will fail identically**. Fix the
  request instead.
* **No `error` key** — the platform's, produced when something in front of the
  service fails. Retry that one with capped exponential backoff.

Since you cannot always tell in advance which you have: retry a `500` at most a
couple of times with backoff, and stop. A `500` that survives two attempts on an
unchanged request is not going to clear on the third.

## Two rough edges worth knowing

Both currently surface as a `500` where a `400` would be more informative. They
are behaviour, not intent — documented so you can recognise them in a log rather
than debug them twice.

| You sent                                               | You get                                         |
| ------------------------------------------------------ | ----------------------------------------------- |
| A `budget.operating_point` that is not in the registry | `500` `engine_error`, `detail` naming the label |
| A `records[].kind` outside the published enum          | `500` `engine_error`                            |

Neither is retryable: the same request fails identically. Read
`/v1/operating-points` for valid labels, and the
[record kinds](/concepts/records-and-tasks#record-kinds) for valid kinds.

## Timeouts and fail-open

Set a client timeout you are happy to add to your critical path. Every response
reports the server-measured selection time as `engine_latency_ms`; your round
trip adds network transit on top of it, and the two are different measurements
that should not be added together casually. Measure both from your own client
before you choose a timeout — a 10-second default is generous for either.

Whatever the failure — timeout, non-2xx, unparseable body, empty selection —
**send your original context unmodified**. Selection is an optimisation, and a
failed optimisation degrades to "no optimisation", never to an empty prompt.
There is a worked implementation in the
[quickstart](/quickstart#5-fail-open).
