# Error Codes

> Whisper control plane errors: the error object in the envelope, every operation code with its status, the transport codes, and what to retry.

*Source: https://www.whisper.security/docs/control-plane/errors*

---
A failed operation keeps the envelope. `ok` is `false`, `status` carries the status, and `error` carries the reason.

## The error object

```text
{
  "op": "policy",
  "ok": false,
  "status": 403,
  "error": {"code": "FORBIDDEN_SCOPE", "message": "<what was missing>", "retryable": false},
  "retry_after": null
}
```

`error` is either a string or an object with `code`, `message` and `retryable`. **Branch on `code`. Never branch on `message`,** which is written for a person and changes. On the statuses that carry it, `retry_after` gives the number of seconds to wait.

## Bad request

| Code | Status | Condition | What to do |
|---|---|---|---|
| `UNKNOWN_OP` | 400 | `op` is not an operation the plane accepts. | Check the spelling against [Operations](/docs/control-plane/operations). |
| `BAD_ARGS` | 400 | An argument is missing, or a value is outside what the argument accepts. | Read the operation's page for the argument shape. |
| `DOMAIN_NOT_REGISTRABLE` | 400 | The name in a `domain` call cannot be attached as given. | See [Manage a domain](/docs/control-plane/operations/domain). |

## Refused

| Code | Status | Condition | What to do |
|---|---|---|---|
| `FORBIDDEN_SCOPE` | 403 | The key does not carry the scope the operation needs. | Check the table in [Keys and scopes](/docs/control-plane/auth). |
| `ANONYMOUS_WRITE` | 403 | A write arrived with no key. | Send `X-API-Key`. |
| `DEVICE_RESOLVE_ONLY` | 403 | The credential belongs to a resolve-only device, which cannot change anything. | Use an account key. |
| `AGENT_REVOKED` | 403 | The identity named has been cut. | Mint a fresh one with `register`. |
| `AGENT_EXPIRED` | 403 | The identity is no longer in service. | Mint a fresh one. |
| `RESERVED_NAME`, `OFFENSIVE_NAME`, `PREMIUM_NAME` | 403 | The label is not available under the naming rules. | Pick another label. |

## Not found

| Code | Status | Condition | What to do |
|---|---|---|---|
| `NOT_FOUND` | 404 | No such identity, address or name under this account. | Read the roster with `list`. |

## Conflict

| Code | Status | Condition | What to do |
|---|---|---|---|
| `REGISTER_IN_PROGRESS` | 409 | A register for this account is already running. | Wait for it, then read `list`. |
| `WG_KEY_IN_USE` | 409 | The WireGuard public key is bound to another identity. | Generate a new pair. |
| `DOMAIN_TAKEN`, `DOMAIN_IN_BAILIWICK`, `DOMAIN_OVERLAPS_ANOTHER_ACCOUNT`, `DOMAIN_UNDER_HELD_APEX` | 409 | The name, or a name above it, is held elsewhere or on hold. | Pick a name you hold outright. |

## Too much for one answer

| Code | Status | Condition | What to do |
|---|---|---|---|
| `RESULT_TOO_LARGE` | 413 | The answer does not fit in one response. | Narrow the window, lower `limit`, or read by `kind`. |

## Slow down

| Code | Status | Condition | What to do |
|---|---|---|---|
| `AGENT_LIMIT`, `IDENTITY_LIMIT`, `TENANT_LIMIT` | `429` | The account's allowance is used up. | [Sign in](https://console.whisper.security) to see the account, or ask Whisper to widen it. |
| `DOMAIN_LIMIT`, `BYOD_CAPACITY` | `429` | The account cannot take another name right now. | Ask Whisper. |
| `ACTION_LIMIT` | `429` | Too many actions in flight. | Wait for `retry_after` seconds, then send it again. |

## Try again

| Code | Status | Condition |
|---|---|---|
| `STORE_UNAVAILABLE` | 503 | The store behind the plane did not answer. |
| `PUBLISH_FAILED` | 503 | The change did not reach DNS. |
| `BACKEND_BUSY`, `READ_BUSY` | 503 | The plane could not take the call right now. |
| `PRIMARY_UNREACHABLE` | 503 | The authoritative side did not answer. |
| `GRAPH_BUSY` | 503 | The graph could not answer in time. |

Treat any `503` as worth sending again, and honour `retry_after`. On a write, read the state back with `list` or `agent` before retrying, so you do not make a second allocation.

## Errors from the endpoint itself

These come from the request rather than the operation, so there is no `op` row to read.

| Code | Status | Condition | What to do |
|---|---|---|---|
| `no_query` | 400 | The body carried no statement. | Send a `query` field, or a `q` form field. |
| `bad_request` | 400 | The body could not be read. | Check the JSON. |
| `unknown_op` | 400 | The statement named an operation the endpoint does not serve. | See [Operations](/docs/control-plane/operations). |
| `read_only` | 400 | A raw Cypher statement carried a write clause. | Send reads only. Changes go through `whisper.agents`. |
| `rate_limited` | `429` | Requests arrived faster than the endpoint took them. | Wait for the interval the response gives, then send it again. |
| `graph_unavailable` | 502 | A forwarded graph read could not be served. | Retry. |
| *(no code)* | 504 | A forwarded graph read did not come back. | Narrow the statement, then retry. |

## Malformed Cypher

A statement the parser cannot read never reaches an operation, so it does not come back in this envelope. It comes back as a problem document instead, described in [Errors](/docs/cypher-api/errors).
