> ## Documentation Index
> Fetch the complete documentation index at: https://docs.drin.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The Drin error envelope and the type-to-status table: validation_error, authentication_error, rate_limited, suppressed, and more.

Drin uses conventional HTTP status codes and returns a consistent JSON envelope on any non-2xx response. Switch on error.type — it's stable and machine-readable; never parse the message.

## The envelope

Every non-2xx response carries the same shape:

```json Error response theme={null}
{
  "error": {
    "type": "validation_error",
    "message": "`to` must contain at least one recipient.",
    "param": "to"
  }
}
```

| Field     | Description                                                                       |
| --------- | --------------------------------------------------------------------------------- |
| `type`    | Machine-readable category — switch on this. One of the values in the table below. |
| `message` | Human-readable explanation. Safe to log; don't parse it — wording may change.     |
| `code`    | Optional finer-grained code, when present.                                        |
| `param`   | Optional offending request field, when the error is about input.                  |

## Error types

Each `type` maps to a status range. The status tells you *how* to react; the type tells you *why*.

| `type`                 | Status        | Meaning                                                                                                                |
| ---------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `validation_error`     | `400` / `422` | The request was malformed or failed validation — `param` points at the field.                                          |
| `authentication_error` | `401`         | Missing, malformed, or revoked API key.                                                                                |
| `permission_error`     | `403`         | The key is valid but not allowed to do this — wrong project, sandbox restriction, or plan limit.                       |
| `not_found`            | `404`         | No such resource for this account.                                                                                     |
| `conflict`             | `409`         | Conflicts with current state — an idempotency replay with a different body, or deleting a domain with sending history. |
| `suppressed`           | `409`         | Every recipient is on this project's suppression list, so nothing was sent.                                            |
| `rate_limited`         | `429`         | Too many requests — back off and retry. See [Rate limits](/api-reference/rate-limits).                                 |
| `internal_error`       | `5xx`         | Something went wrong on our side. Safe to retry idempotently.                                                          |

## Request IDs

Every response echoes a request id in the `X-Request-Id` header. Include it when you contact support — it lets us find the exact request in our logs.

<Warning>
  **Retrying safely.** The SDK auto-retries `429` and `5xx` with exponential backoff and full jitter. A `POST` is only retried when you pass an [`Idempotency-Key`](/idempotency), so a send is never duplicated. If you build your own client, do the same — never blindly retry a bare `POST`.
</Warning>

Working in TypeScript? Each `type` is also a typed error subclass (`DrinValidationError`, `DrinRateLimitError`, `DrinSuppressedError`, …), so you can branch with `instanceof`. See the [Errors guide](/errors) and the [TypeScript SDK](/sdk/typescript).
