# Errors and status codes > Every status this API returns, the stable code that comes with it, and what to do about each — including the 504 that does not mean your request failed. Source: https://docs.stg.perdurance.dev/errors/ --- Every error is one JSON object with the same shape, whatever produced it: ```json { "error": { "code": "held_too_long", "message": "the request is still running as 01J8F2ZK9QX3M4NBVWT7; re-send the identical body to attach to it, or read it back by id" } } ``` `code` is stable and meant to be branched on. `message` is meant to be read by a person and may change. ## The table | Status | `code` | What happened | What to do | |---|---|---|---| | `400` | `bad_request` | The request was malformed | Read `message`; it names the problem | | `401` | `unauthorised` | No key, or one that does not match | Check the key; both "missing" and "wrong" answer the same way on purpose | | `403` | `forbidden` | The key is valid but holds no grant for that scope, or a rule refused the call | Widen the grant, or read `message` for which rule refused | | `403` | `password_change_required` | The account still has a password it did not choose | Set a password before anything else | | `403` | `password_rejected` | The current password offered did not match | — | | `404` | `not_found` | No such resource, or not one this key can see | — | | `409` | `conflict` | It already exists, or something changed underneath | Re-read and retry | | `410` | `record_expired` | The request happened; its record has been swept | Don't retry the read. See below | | `415` | `unsupported_media_type` | The body was not sent as `application/json` | Set the header | | `422` | `unprocessable_entity` | Well-formed, but not something this service can act on | Most often a dialect mismatch — see below | | `502` | `upstream_failed` | The provider call itself failed | `message` carries what the provider said | | `504` | `held_too_long` | A synchronous answer gave up waiting | **The request is still running.** See below | | `500` | `internal` | A fault on our side | `message` says nothing about the cause, deliberately | ## Three that do not mean what they usually mean ### `410 record_expired` is not `404` A `404` means the id was never yours. A `410` means it was, the request ran, and its **record** — the body you sent, the answer, and the chunks — has been swept past the retention horizon its namespace was given. Everything you bill and report from is still there: the status, the model, the token counts, the cost and the timings are on [`GET /requests`](/usage), and the fee ledger outlives them both. What is gone is the content, and retrying the read will not bring it back. If you are seeing this on requests you still need, the horizon is a namespace setting — raise it in the console under Namespaces. ### `504 held_too_long` is not a failure A synchronous call holds the connection for at most the deployment's **synchronous hold** — five minutes by default. When a generation outlasts that, the connection is let go with a `504`. **The execution is not cancelled.** It is still running, and its answer will be stored. The message carries the request id, and you have two ways back: ```bash # attach to the same execution — no second provider call curl "$PERDURANCE_URL/chat/completions" -H "Authorization: Bearer $PERDURANCE_KEY" \ -H 'Content-Type: application/json' -d "$IDENTICAL_BODY" # or read it back by id curl "$PERDURANCE_URL/requests/$ID" -H "Authorization: Bearer $PERDURANCE_KEY" ``` Treating this as a failure and re-submitting a *different* body is the one mistake that costs real money: it starts a second provider call while the first is still running. ### `403` is two different things Plain `forbidden` with the standard message means the key holds no grant for that scope — an authorisation problem you fix by widening the grant. `forbidden` with a message naming a rule means the opposite: the credential was sufficient, and a rule declined what was asked. The message names which one, because a caller acting well inside their own authority needs to know what they met. ## `superseded`, mid-stream A replayed stream can carry an `event: failed` with: ```json { "status": "superseded", "detail": "another worker took this request up and the chunks streamed so far were discarded with the attempt that wrote them; the request is still running and can be retrieved by its id" } ``` This is a recovery in progress: the worker that was producing your chunks died, another took the request up, and the partial output from the dead attempt was discarded rather than spliced together with the new one. The request is still running. Re-open the stream by id and you will get the new attempt's output from its beginning. ## What is never in an error A `500` says nothing about its cause. Provider credentials, prompt bodies and password hashes do not appear in any error body, and a sign-in that failed cannot be used to learn which email addresses exist — every rejected credential answers identically.