# Errors

The shape of an API error and the closed set of codes it can carry.

## Error shape

The body is flat — nothing nests under an `error` key:

```json
{
  "code": "validation_failed",
  "message": "The request does not match the contract",
  "details": {
    "issues": [
      {
        "path": ["content", "subject"],
        "code": "too_small"
      }
    ]
  },
  "requestId": "01a0ba31-3fe4-7cc4-a1c6-64bdaaa5d99c"
}
```

### Response attributes

- `code` `string` — Required. From the closed set in the table below.
- `message` `string` — Required. Readable, but not meant for an end user — we never translate it.
- `details` `free-form map | null` — Required. null unless the code has something structured to add, like the list of fields that failed validation.
- `requestId` `string` — Required. The same id that arrives in the x-request-id header of every response — error or not — so you can capture it even when your client never reads the body.

## Codes this surface can answer with

Of the codes the system carries, these are the ones a `/v1` caller can actually receive:

| Code | HTTP status | When it shows up |
| --- | --- | --- |
| `bad_request` | 400 | A rule the schema alone cannot express — `POST /v1/email` with no address across `to`, `cc` and `bcc`. |
| `unauthorized` | 401 | The bearer token is missing, or does not match any active key. |
| `api_key_expired` | 401 | The bearer token names a key that is past its own expiration. The key stays revocable and visible in the panel; it just no longer authenticates. |
| `payment_required` | 402 | The organization is over its plan's sending limit. |
| `forbidden` | 403 | The key is valid, but does not carry the permission this action needs. |
| `not_found` | 404 | The resource does not exist, or does not exist for this organization — the response never reveals which. |
| `conflict` | 409 | The `idempotencyKey` was already used with a different body, or the referenced template is not active. |
| `validation_failed` | 422 | The body does not match the schema — an unrecognized field, a wrong type, more than 50 addresses in one field. |
| `rate_limited` | 429 | This key exceeded its per-second request limit. |
| `internal` | 500 | Something failed on our side. |

The full set is longer — the rest are codes for flows `/v1` does not expose, like an expired
invitation — but any client can safely treat a code it does not recognize as a generic error.
