# Errors

> Every error the Pidgeon API returns: one envelope, one code, what each one means, and which of them are worth retrying.

A failure is always the same shape, with an HTTP status that means the same
thing as the code inside it.

```json
{
"error": {
  "code": "validation",
  "message": "Enter only the part before the @.",
  "field": "local_part"
}
}
```

`message` is written for a person and is safe to show one. It never carries a
stack, a DNS response code or a provider's wording — if you are surfacing API
errors in your own interface, this is the string to use.

## The codes

| Code              | Status | What it means                                       |
| ----------------- | ------ | --------------------------------------------------- |
| `validation`      | 400    | The request is malformed. `field` says where.       |
| `unauthenticated` | 401    | No key, or a key that has been deleted.             |
| `forbidden`       | 403    | This key may not do that: its access or its reach.  |
| `blocked`         | 403    | A rule on the address refused it. Remove the rule.  |
| `not_permitted`   | 403    | The address's `send_policy` refused it.             |
| `not_found`       | 404    | No such resource — or one that is not yours.        |
| `conflict`        | 409    | It already exists, or something else changed first. |
| `gone`            | 410    | It was here, and has been deleted.                  |
| `resync`          | 410    | Too far behind to catch up: page again from the top |
| `entitlement`     | 402    | Your plan does not include this.                    |
| `abuse_blocked`   | 403    | The account is paused pending review.               |
| `rate_limited`    | 429    | Too many requests. See the headers below.           |
| `unavailable`     | 503    | Ours, and temporary. Retry.                         |
| `internal`        | 500    | Ours, and unexpected.                               |

`forbidden` and `not_found` deliberately return the same sentence. Confirming
that a resource exists but belongs to somebody else is itself a leak, so the
API does not.

`gone` is the one place we do distinguish *existed* from *never existed*, and
it is scoped so that the distinction tells you nothing you did not already
have: you get `410` only for something deleted from a resource **you can
already read**. Asked about somebody else's, it is a `404` like anything
else. It is answered for as long as the event log remembers the deletion;
after that the honest answer is `404` again, because we no longer know.

## Rate limit headers

Every successful response carries them, so a client can pace itself rather
than discover the limit by hitting it.

```http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1757760060
```

`Reset` is unix seconds. A `429` also carries `Retry-After`.
