# Errors

Every failed Bird API request returns the same JSON envelope, nested under a top-level `error` key:

```json
{
  "error": {
    "type": "validation_error",
    "code": "E01001",
    "name": "ValidationError",
    "message": "Request validation failed.",
    "doc_url": "https://bird.com/docs/api/errors/E01001",
    "request_id": "req_01krdgeqcxet5s7t44vh8rt9mg",
    "details": [
      { "param": "to", "message": "must contain at least one recipient" },
      { "param": "subject", "message": "exceeds maximum length of 998 characters" }
    ]
  }
}
```

The HTTP status code is set correctly at the transport layer — `400` for bad input, `401`/`403` for auth, `404` for missing resources, `409` for conflicts, `429` for rate limits, `5xx` for Bird-side failures — and the body adds specificity. Status alone tells you the category; the envelope tells you exactly what happened.

## The envelope fields

| Field        | Role                                                                                                                                                                                                                                              |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | Broad category for coarse branching — `validation_error`, `auth_error`, `permission_error`, `not_found_error`, `conflict_error`, `rate_limit_error`, `billing_error`, `internal_error`, and a handful of others. A closed enum that grows rarely. |
| `code`       | Opaque, stable identifier (`E01001`). **The canonical reference** — unique, never renamed, never reused. When an error is retired, its code is permanently reserved.                                                                              |
| `name`       | Human-readable slug (`ValidationError`) for log readability. Always paired with `code`, never a replacement for it.                                                                                                                               |
| `message`    | Human-readable description. **Not stable** — wording changes without notice. Display it, log it, never parse it.                                                                                                                                  |
| `param`      | For input-related errors, the offending field. Omitted when not applicable.                                                                                                                                                                       |
| `doc_url`    | Permanent link to the docs page for this code. Valid forever, even for retired codes.                                                                                                                                                             |
| `request_id` | Always present, and also returned as the `X-Request-Id` response header. Quote it in support requests — it lets Bird trace the exact request.                                                                                                     |

**Branch on `type` for coarse handling and `code` for specific handling — never on `message`.** A typical client switches on `type` (retry on `rate_limit_error`, surface `validation_error` to the user, page someone on `internal_error`) and matches individual `code` values only for the few errors it handles specially.

```bash
curl -s https://us1.platform.bird.com/v1/email/messages \
  -X POST \
  -H "Authorization: Bearer bk_us1_invalid" \
  -H "Content-Type: application/json" \
  -d '{}' | jq '.error | {type, code, request_id}'
```

The opacity of codes is deliberate: a code like `E04012` doesn't pretend to be self-documenting, so every code gets a real docs page (its `doc_url`) describing the exact cause and what to do about it. The full catalog lives in the [error reference](/docs/api/errors).

## Two structured exceptions

### Validation failures: one code, many details

Field-level validation does not get a separate code per field-and-failure combination. Every validation failure is `E01001 ValidationError` with a `details` array listing each field-level problem as `{param, message}` — see the envelope example above. The `details` array is present only on `validation_error` responses, and the `message` strings inside it are human-readable, not machine-stable: use `param` to map problems to form fields, use the message for display.

### External-system failures: `vendor_code`

When a failure originates in a system outside Bird — an upstream email provider, an SMS carrier, a card network, a recipient's mail server — the envelope carries one stable Bird `code` plus a `vendor_code` field with the external system's own code, verbatim:

```json
{
  "error": {
    "type": "internal_error",
    "code": "E05003",
    "name": "SparkPostError",
    "message": "An error occurred communicating with the mail infrastructure provider.",
    "doc_url": "https://bird.com/docs/api/errors/E05003",
    "request_id": "req_01krdgeqcxet5s7t44vh8rt9mg",
    "vendor_code": "1902"
  }
}
```

This keeps Bird's catalog small and stable while still giving you the full upstream taxonomy when you need it — the docs page for each such code explains what its `vendor_code` values mean in context.

## Handling errors well

- **Retry `429` and `5xx`, nothing else by default.** Honor `Retry-After` on rate limits (see [Rate limits](/docs/guides/rate-limits)), use exponential backoff on `5xx`, and send an [`Idempotency-Key`](/docs/guides/idempotency) so retries of mutating requests are safe.
- **Log `code`, `name`, and `request_id` together.** The code is what you'll search the docs and your own logs for; the request ID is what support needs.
- **Tolerate new codes and types.** New error codes ship regularly as products grow, and the `type` enum occasionally gains a value — write your handler with a sane default branch rather than an exhaustive match.

## Next steps

- [Error reference](/docs/api/errors) — the full error catalog, one page per code
- [Idempotency](/docs/guides/idempotency) — safe retries for mutating requests
- [Rate limits](/docs/guides/rate-limits) — limit headers and backoff guidance