Error reference
Every problem+json type we emit — and the four failures that are not problem+json.
How to read an error
Every failure the API decides on comes back as application/problem+json (RFC 9457) with a type URI that links to the section below, a code, a title and a detail. Branch on code, never on the wording.
We never put an identifier in the body. A 404 is deliberately indistinguishable from “not yours” — telling you which would let a key enumerate other customers' decks.
Forbidden
403 · forbidden · do not retry
The credential is valid but this account is not on the access list for the public API, or the key belongs to an organisation that has switched org-key spend off.
What to do: Ask us to add your account. Access is an explicit allow-list while the surface is in preview — nothing about your key is wrong.
Not found
404 · not_found · do not retry
There is no such resource, or it is not visible to this credential. The two are deliberately indistinguishable: a 403 would confirm that an id you guessed belongs to somebody else.
What to do: Check the taskId. If you are using an organisation key, confirm the deck was created by that organisation.
Insufficient credits
402 · insufficient_credits · do not retry
The wallet funding this call does not hold the credits the request reserves.
What to do: The body carries `options` — an ordered list of what you can do next (top up, subscribe, unlock this deck). Present them; do not guess a price.
Price changed since you quoted
409 · price_drift · retryable
You sent `expected_credits` and the live price no longer matches — typically an admin retune between your quote and your submit.
What to do: Re-quote and resubmit with the new number. Never strip `expected_credits` to make the 409 go away; it is the only thing standing between a retune and an unexpected charge.
Conflict
409 · conflict · do not retry
The request collides with the current state — most often an idempotency key you already used with a different body.
What to do: Use a fresh idempotency key for a genuinely new request, and the same one only for a true retry of the identical call.
Validation failed
400 · validation_failed · do not retry
A field is missing or outside its allowed set.
What to do: Read `detail`. Note that FastAPI's own 422 body is NOT problem+json — see “Four failures that are not problem+json” below.
Payload too large
413 · payload_too_large · do not retry
The request body exceeded the cap for that path.
What to do: Upload materials through the materials endpoint, which carries the large-body cap; other paths do not.
Too many requests
429 · rate_limited · retryable
You crossed either the per-IP edge bucket or your per-credential bucket.
What to do: Honour `Retry-After` and the `X-RateLimit-*` headers. They describe the per-credential bucket, which is the tighter of the two.
Public API disabled
503 · surface_disabled · retryable
The public surface kill switch is off. This affects reads too.
What to do: Retry with backoff. This is an operator action, not a problem with your request; it is announced on the changelog feed when planned.
Internal error
500 · internal_error · retryable
We broke. It is on our side.
What to do: Retry once, then tell us. Include the time and the endpoint — never your key.
| Failure | What arrives instead |
|---|---|
| 422 from request validation | `{"detail": [{"loc": …, "msg": …}]}` — FastAPI's own shape. |
| 503 when the database is unreachable | A plain JSON body from the shared handler, not problem+json. |
| 429 raised before authentication | The edge bucket answers before any `/v1` code runs; treat any 429 as rate limiting regardless of body. |
| 413 from the body-size middleware | Pure-ASGI and outside the sub-app, so it cannot be reshaped by a handler. |