Errors
Every failed Bird API request returns the same JSON envelope, nested under a top-level error key:
Code example
{
"error": {
"type": "validation_error",
"code": "E01001",
"name": "ValidationError",
"message": "Request validation failed.",
"doc_url": "https://docs.bird.com/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.
Code example
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.
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:
Code example
{
"error": {
"type": "internal_error",
"code": "E05003",
"name": "SparkPostError",
"message": "An error occurred communicating with the mail infrastructure provider.",
"doc_url": "https://docs.bird.com/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), use exponential backoff on 5xx, and send an Idempotency-Key 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.
Related
- Error reference — the full error catalog, one page per code
- Idempotency — safe retries for mutating requests
- Rate limits — limit headers and backoff guidance