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

# Errors

> Dairo's error model: one error shape, a stable code to branch on, the full code list with HTTP statuses, and how the SDKs surface it.

When a request fails, Dairo returns one predictable shape and a stable `code` that says
exactly what went wrong, so your app or your agent can react instead of parsing English.

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_domain",
    "message": "domain is not verified",
    "param": null
  }
}
```

| Field     | Type             | Purpose                                                                                      |
| --------- | ---------------- | -------------------------------------------------------------------------------------------- |
| `type`    | `string`         | Coarse error class to switch on at a high level.                                             |
| `code`    | `string`         | Stable machine code for the specific condition. Branch on this.                              |
| `message` | `string`         | Human-readable explanation, for display and logs, not for branching.                         |
| `param`   | `string \| null` | The request field an error refers to. Dairo currently returns `null` here; branch on `code`. |

A few structured errors add a `details` object. Deleting a domain that still owns inboxes
returns `409` with code `cascade_confirmation_required` and
`details: { requiresCascadeConfirmation, inboxes, messages }`, so a client can prompt
before deleting.

<Warning>
  Branch on `code`, never on `message` (the prose can change) and never on HTTP status alone
  (one status maps to several codes: a `429` can be `rate_limited`, `plan_limit_reached`,
  `budget_exceeded`, or `spend_cap_reached`). The `code` names the exact condition, so your
  app can self-correct: `invalid_domain` → verify the domain; `scope_missing` → request the
  missing scope; `idempotency_key_mismatch` → fix the reused key.
</Warning>

## The full code list

Every code an API key request can return, with its typical HTTP status:

| `code`                     | Status        | Meaning                                                                             |
| -------------------------- | ------------- | ----------------------------------------------------------------------------------- |
| `invalid_request`          | `400`         | Malformed request (bad JSON, missing or blank required field).                      |
| `invalid_domain`           | `400` / `422` | The domain is unknown, unverified, or not usable for this action.                   |
| `idempotency_key_mismatch` | `400`         | The `Idempotency-Key` header and body `idempotencyKey` are set to different values. |
| `validation_failed`        | `422`         | The request was well-formed but a field failed validation.                          |
| `duplicate_resource`       | `409`         | The resource already exists (domain, webhook URL, inbox, and so on).                |
| `scope_missing`            | `401` / `403` | The key lacks the scope this endpoint requires.                                     |
| `not_permitted`            | `401` / `403` | Authenticated, but not allowed to perform this action.                              |
| `resource_not_found`       | `404`         | No such resource for this account, or the route does not exist.                     |
| `link_no_longer_available` | `410`         | A share link was revoked, expired, or used up.                                      |
| `payload_too_large`        | `413`         | The request body or an attachment exceeds the size limit.                           |
| `plan_limit_reached`       | `429` / `413` | A plan quota was hit (monthly email cap, storage cap, resource count).              |
| `rate_limited`             | `429`         | Too many requests; retry after the `Retry-After` window.                            |
| `budget_exceeded`          | `429`         | A spend budget for the scope was exceeded.                                          |
| `spend_cap_reached`        | `429`         | The account spend cap was reached.                                                  |
| `provider_unavailable`     | `502`         | Delivery is temporarily unavailable; safe to retry with backoff.                    |
| `service_unavailable`      | `503`         | The service is temporarily unavailable; safe to retry with backoff.                 |
| `internal_error`           | `500`         | An unexpected error; retry, then contact support.                                   |

Some endpoints return a more specific `code` for a particular condition, in the same
envelope: `letter_not_cancelable` (`409`, a letter already dispatched),
`upload_link_consumed` (`409`, a one-time upload link reused), and
`cascade_confirmation_required` (`409`, the domain-delete case above).

## The type classes

`type` groups codes into a coarse class, so you can switch at a high level before drilling
into `code`:

| `type`                  | Covers                                                                                                    |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `invalid_request_error` | `invalid_request`, `invalid_domain`, `idempotency_key_mismatch`, `validation_failed`, `payload_too_large` |
| `authentication_error`  | `scope_missing`, `not_permitted` (on a `401`)                                                             |
| `permission_error`      | `scope_missing`, `not_permitted` (on a `403`)                                                             |
| `not_found_error`       | `resource_not_found`, `link_no_longer_available`                                                          |
| `conflict_error`        | `duplicate_resource`, and the condition-specific `409` codes                                              |
| `rate_limit_error`      | `rate_limited`, `plan_limit_reached`, `budget_exceeded`, `spend_cap_reached`                              |
| `api_error`             | `provider_unavailable`, `service_unavailable`, `internal_error`                                           |

## Handling errors in the SDKs

The SDKs raise a typed `DairoError` that surfaces `status`, `type`, `code`, `param`, and
(when present) `requestId`. Branch on `code`:

<CodeGroup>
  ```ts title="TypeScript" theme={null}
  import { DairoError } from "dairo";

  try {
    await dairo.messages.send({ inboxId: "inbox_123", to: "x@e.com", subject: "Hi", text: "…" });
  } catch (err) {
    if (err instanceof DairoError) {
      switch (err.code) {
        case "invalid_domain":   /* verify the sending domain */ break;
        case "scope_missing":    /* this key can't send mail */ break;
        case "rate_limited":     /* back off and retry */ break;
        default:                 console.error(err.type, err.code, err.message, err.param);
      }
    }
  }
  ```

  ```python title="Python" theme={null}
  from dairo import DairoError

  try:
      dairo.messages.send(inbox_id="inbox_123", to="x@e.com", subject="Hi", text="…")
  except DairoError as err:
      if err.code == "invalid_domain":
          ...  # verify the sending domain
      elif err.code == "scope_missing":
          ...  # this key can't send mail
      else:
          print(err.code, err.status_code, err.message)
  ```
</CodeGroup>

<Note>
  The status column above carries each code's HTTP status; for the `201`-on-create and
  `204`-on-delete conventions, see [Response shape](/concepts/the-envelope).
  `provider_unavailable`, `service_unavailable`, and `internal_error` are the codes that
  carry no actionable detail; all three are safe to retry with backoff. A reused
  idempotency key is handled separately from errors; see
  [Idempotency](/concepts/idempotency).
</Note>
