# Errors

Every failed request returns the same JSON envelope under a top-level `error` key, with an HTTP status that tells you the coarse category. This page is the wire contract; for guidance on branching, retries, and the code catalog philosophy, see [Errors](/docs/guides/errors).

```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": "contact_id", "message": "this field is reserved and not yet supported" },
      { "param": "topic_id", "message": "this field is reserved and not yet supported" }
    ]
  }
}
```

## Envelope fields

| Field         | Always present | Description                                                                                                                                                   |
| ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`        | Yes            | Broad category for coarse branching — a closed enum (`auth_error`, `validation_error`, `rate_limit_error`, ...).                                              |
| `code`        | Yes            | Opaque, stable identifier matching `E\d{5}`. Unique, never renamed, never reused — the canonical thing to match on.                                           |
| `name`        | Yes            | Human-readable slug (`ValidationError`) for log readability. Always paired with `code`, never a replacement for it.                                           |
| `message`     | Yes            | Human-readable description. Not stable — display or log it, never parse it.                                                                                   |
| `doc_url`     | Yes            | Stable link to the documentation page for this `code`.                                                                                                        |
| `request_id`  | Yes            | Correlation ID, also returned as the `X-Request-Id` response header. Quote it in support requests.                                                            |
| `param`       | No             | The offending field, when a single field is at fault.                                                                                                         |
| `details`     | No             | Per-field validation failures as `{param, message}` objects, where `param` is a dotted path like `to[0].email`. Present only on `validation_error` responses. |
| `vendor_code` | No             | Verbatim code from a downstream system (an SMTP reply code, a payment decline code) when one is worth acting on.                                              |

## HTTP status mapping

Each `type` maps to exactly one HTTP status, so the status and the envelope never disagree.

| Status | `type`                      | Meaning                                                                                                                                                                                     |
| ------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `bad_request_error`         | The request was malformed — unparseable body, invalid header (for example, a bad `Idempotency-Key`).                                                                                        |
| `401`  | `auth_error`                | Missing, invalid, or revoked credentials. See [Authentication](/docs/api/authentication).                                                                                                   |
| `402`  | `billing_error`             | The request requires a payment method, balance, or plan the organization does not have.                                                                                                     |
| `403`  | `permission_error`          | Authenticated, but not allowed — insufficient API key scope or workspace role.                                                                                                              |
| `404`  | `not_found_error`           | No such route, or no such resource in this workspace.                                                                                                                                       |
| `409`  | `conflict_error`            | The request conflicts with current state — including idempotency conflicts `E01004` and `E01005` ([Idempotency](/docs/api/idempotency)).                                                    |
| `412`  | `precondition_error`        | A precondition for the request was not met.                                                                                                                                                 |
| `413`  | `payload_too_large_error`   | The request body exceeds the maximum allowed size.                                                                                                                                          |
| `421`  | `misdirected_error`         | The request was routed to the wrong region — use the regional host that matches your key's `bk_{region}_` prefix.                                                                           |
| `422`  | `validation_error`          | The body parsed but the values are invalid. `details` lists each failing field; reserved-but-unshipped fields such as `contact_id` and `topic_id` on email sends also land here (`E04007`). |
| `429`  | `rate_limit_error`          | A [rate-limit group](/docs/guides/rate-limits) is exhausted. `Retry-After` gives the wait in seconds, and the `RateLimit`/`RateLimit-Policy` headers name the group and quota.              |
| `500`  | `internal_error`            | Bird-side failure. Safe to retry with backoff — and with the same idempotency key.                                                                                                          |
| `501`  | `not_implemented_error`     | The endpoint is declared but not yet implemented.                                                                                                                                           |
| `503`  | `service_unavailable_error` | A dependency is temporarily unavailable. Retry with backoff.                                                                                                                                |

## Handling errors in the SDKs

Each [SDK](/docs/sdks/concepts) maps the envelope onto its language's native error model and carries every envelope field (`type`, `code`, `message`, `doc_url`, `request_id`, ...) on the error value.

<!-- bird:snippet email.errors -->

```typescript
import { BirdRateLimitError, BirdValidationError, BirdAPIError } from "@messagebird/sdk";

try {
  await bird.email.send({
    from: { email: "onboarding@messagebird.dev", name: "Bird" },
    to: ["delivered@messagebird.dev"],
    subject: "Hello from Bird",
    html: "<p>My first Bird email.</p>",
  });
} catch (err) {
  if (err instanceof BirdRateLimitError) console.log(`rate limited — retry in ${err.retryAfter}s`);
  else if (err instanceof BirdValidationError) console.error(err.details);
  else if (err instanceof BirdAPIError) console.error(err.code, err.requestId);
  else throw err;
}
```

<!-- bird:snippet email.errors -->

```go
if err != nil {
	var rle *bird.RateLimitError
	var ve *bird.ValidationError
	var ae *bird.APIError
	switch {
	case errors.As(err, &rle):
		fmt.Println("rate limited; retry after", rle.RetryAfter)
	case errors.As(err, &ve):
		for _, d := range ve.Details {
			fmt.Printf("%s: %s\n", d.Param, d.Message)
		}
	case errors.As(err, &ae):
		fmt.Printf("API error %s (status %d, request %s)\n", ae.Code, ae.StatusCode, ae.RequestID)
	default:
		log.Print(err) // transport: *bird.ConnectionError or *bird.TimeoutError
	}
}
```

<!-- bird:snippet email.errors -->

```python
from bird import APIStatusError, RateLimitError, ValidationError

try:
    client.email.send(
        from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
        to=["delivered@messagebird.dev"],
        subject="Hello from Bird",
        text="My first Bird email.",
    )
except RateLimitError as err:
    print("rate limited; retry after", err.retry_after)
except ValidationError as err:
    print(err.status_code, err.details)
except APIStatusError as err:
    print(err.status_code, err.code, err.request_id)
```

## Related

- [Errors concepts](/docs/guides/errors) — branching strategy, validation details, and `vendor_code` semantics
- [Authentication](/docs/api/authentication) — credentials behind `401` and `403`
- [Idempotency](/docs/api/idempotency) — the `409` conflict errors and safe retries
- [Rate limits](/docs/guides/rate-limits) — groups, headers, and handling `429`