Errors and limits
The error shape every surface returns, what each status class means, the codes worth branching on, and the plan and rate limits a call can meet.
The error shape
Every error from the REST API has the same body:
{
"error": {
"code": "unknown_approver",
"message": "A sentence for a person to read.",
"details": { "…": "whatever the refusal carries, when it carries something" }
}
}The `code` is the contract. Branch on it, never on the message, which is written for people and will be reworded. The SDKs raise it as DeliverdError.code, the CLI prints Error (code): message, and the MCP server returns most refusals as an error result carrying the same code.
Status classes
| Status | Means | What to do |
|---|---|---|
| 400 | The request itself is wrong. | Change it. Retrying the same request fails the same way. |
| 401 | The credential was not accepted. | Check the key or token. |
| 402 | Your plan does not include this, or a plan limit is reached. | Upgrade, or free something up. Not retryable as it is. |
| 403 | A refusal about you, or a rule about you. | Not retryable. Read the code: a missing scope, a role, or an organisation rule. |
| 404 | No such thing, or nothing this key may see. | Check the id. The two are deliberately indistinguishable. |
| 409 | The request was fine and the world moved. | Re-read and retry: someone published first, a question was answered, a version is awaiting approval. |
| 413, 415, 422 | The body is too large, the wrong type, or a scan blocked what was in it. | Change the content. |
| 429 | Too many requests. | Slow down and retry; the SDKs do this for you. |
| 500 | Ours. | Retry later. A named 500 code means a write failed for a reason you could not have avoided. |
Codes worth branching on
| Code | Status | When |
|---|---|---|
auth_error | 401 or 403 | The key or token is missing, malformed, revoked or expired (401), or its organisation is suspended or its agent disabled (403). |
insufficient_scope | 403 | The credential is fine and lacks the scope this call needs; the message names it. Reissue the key — scopes are fixed when a key is made. |
validation_error | 400 | The body does not match the schema; details lists the problems. |
unknown_approver | 400 | An approver, reviewer or respondent is not someone the organisation knows. |
ambiguous_approver | 400 | A name matches more than one person or team. Name it by id. |
self_approval | 403 | The requester named only themselves. |
refused_by_rule | 403 | An organisation rule or ethics rule refused the request. The message carries its explanation. |
action_not_permitted | 403 | This agent is not permitted to make this kind of request. |
agent_not_permitted | 403 | Agent keys may not do this: registering actions, writing policy. |
not_permitted | 403 | Your role does not allow this, whatever the key's scopes. |
agent_not_found | 404 | The agent this key acts as has been deleted. |
unknown_action | 404 | The gate was asked about an action nobody registered. |
invalid_input | 400 | The gate's input does not match the action's schema; problems names each fault. |
question_not_found | 404 | That question is not on this approval. |
already_answered | 409 | Somebody has already answered that question. |
version_conflict | 409 | expectedVersion is stale: somebody published first. |
approval_pending | 409 | A version of this report is awaiting approval. |
upload_not_found | 404 | The upload ticket is gone — usually because the first publish used it. |
scan_blocked | 422 | The publish scan found something it will not serve. |
public_sharing_denied | 403 | Agent keys cannot create public links. |
invalid_cursor | 400 | A list cursor this API did not issue. |
idempotency_key_reused | 422 | The same Idempotency-Key with a different body. |
idempotency_in_progress | 409 | The first request with this key has not finished. Retry with the same key. |
rate_limited | 429 | Too many requests from this key. |
The gate's refusal is not on this list, because it is not an error: a denied action answers 200 with allowed: false. See The gate.
Plan limits
A call that needs something your plan does not include, or that would take you past a limit, answers 402:
| Code | Status | When |
|---|---|---|
feature_unavailable | 402 | The plan does not include this capability — agent identities, for example. |
plan_required | 402 | Organisation-wide analytics, below the plan that includes it. |
plan_limit | 402 | The plan's number of live reports is reached. Archive one or upgrade. |
storage_limit | 402 | Storing this version would pass the plan's storage. |
seat_limit | 402 | The plan's number of seats is reached. |
limit_reached | 402 | The plan's number of scheduled agents is reached. |
| Free | Team | Business | Enterprise | |
|---|---|---|---|---|
| Live reports | 10 | 200 | 1,000 | Unlimited |
| Seats | 2 | 10 | 50 | 200 |
| Storage | 1 GB | 25 GB | 100 GB | 500 GB |
| Scheduled agents | — | 2 | 10 | 100 |
| Model screens a month | — | — | 1,000 | 10,000 |
Your organisation's actual limits are on Admin → Billing; an Enterprise agreement can change them.
Rate limits
Each API key or token may make up to 120 requests a minute. Past that, calls answer 429 rate_limited until the minute is up. The SDKs retry a 429 with backoff, honouring Retry-After, and their polling starts quick and slows down, so an agent waiting on a person does not spend its allowance.
Retrying safely
- Retry
409,429and5xx. Do not retry other4xxwithout changing the request. - Send an
Idempotency-Keyon everyPOSTand keep it across your retries, so a request that arrives twice is done once. The SDKs do this for you. A5xxreleases the key, so your retry is a real attempt. - Give requests an
externalIdso a restarted agent can find what it already asked rather than asking twice. The gate requires one.