# Errors and Idempotency (/advanced/errors)

<!-- agent-signals: reading_time_min: 10 · est_tokens: 4424 · updated: 2026-09-06 -->
Related: [Webhooks](/advanced/webhooks.md), [WebSockets](/advanced/websockets.md), [Agent safety](/advanced/safety.md), [Custom domains](/advanced/custom-domains.md), [Deliverability and warmup](/advanced/deliverability.md), [Build a multi-tenant platform](/advanced/multi-tenant.md)



# Handle AgentMail API errors and retry safely

Every AgentMail API error returns a JSON body with a stable `code` to branch on, and creates and sends take idempotency identifiers that make retries replay the original result instead of duplicating it. Use this page to map any error to its recovery action and to make timeouts safe.

## Do this

Branch on the `code` field in the error body, never on `name`, `message`, or the HTTP status alone. Retry only `429` (honor `Retry-After`, then exponential backoff), `500` and `503` (exponential backoff with a bounded budget), and `409` `race_condition` (re-fetch the resource state first). Fix the request for everything else.

Give every create a `client_id` you generate and persist, and reuse the same value on every retry:

```bash
curl -X POST "https://api.agentmail.to/v0/inboxes" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "client_id": "support-inbox-v1" }'
```

The first request creates the resource. Any repeat with the same `client_id` returns `200` with the original resource and creates nothing new, so a timed-out create is safe to retry. The response echoes `client_id` back.

Give every send one `Idempotency-Key` HTTP header per email you mean to send, and retry a timed-out send with the same key and payload. The retry returns the original `message_id` and `thread_id` and sends no second email.

## SDK

```bash
npm install agentmail           # TypeScript
pip install agentmail           # Python
npm install -g agentmail-cli    # CLI
```

* TypeScript: every API failure throws a subclass of `AgentMailError` carrying `statusCode`, the parsed `body`, and `rawResponse` (headers included). Typed subclasses live on the `AgentMail` namespace export, for example `AgentMail.MessageRejectedError`. A request that never got a response throws `AgentMailTimeoutError`.
* Python: every API failure raises a subclass of `ApiError` carrying `status_code`, `body`, and `headers`. Subclasses import from the package, for example `from agentmail import MessageRejectedError`.
* Typed classes by status: `ValidationError` for `400`, `IsTakenError` and `MessageRejectedError` for `403`, `NotFoundError` for `404`, `ConflictError` for `409`, `UnprocessableError` for `422`. Everything else, including `401`, `429`, and `5xx`, surfaces as the base class.
* Per-request retry budget: `maxRetries` in TypeScript, `request_options={"max_retries": ...}` in Python.

More on [/integrations/sdks-and-cli](/integrations/sdks-and-cli).

## Facts

* Error body fields: `code` is a stable snake\_case identifier of the cause, `name` and `message` keep long-standing human wording, `fix` is the concrete next action (omitted when no generic remedy applies), `docs` links to the code's reference entry at `https://docs.agentmail.to/errors`.
* Extra fields by case: `errors` array on validation failures (each entry names the invalid field in `path` and explains it in `message`), `suggestions` on name collisions (up to 3 currently available alternatives), `resource` plus `limit` when known plus `upgrade_url` on plan caps, `suspended_reason` on suspensions.
* A body with no `code`, like `{"message": "Unauthorized"}` or `{"message": "Forbidden"}`, was stopped at the edge before reaching the API: a missing or malformed `Authorization: Bearer` header, an incompletely copied key, or a blocked region.
* API keys start with `am_` and are shown once.
* SDK defaults: up to 2 retries with exponential backoff on `408`, `429`, and any `5xx`. The Python SDK also retries `409`. An error surfaced by an SDK already survived those retries, so slow the whole worker down rather than retrying the one call harder.
* Rate limits apply per API key. A `429` from a daily or monthly send quota only resets with its window.
* Do not automatically retry `400`, `401`, ordinary `403`, `404`, or `422`. Resending the same request reproduces the same error.
* Every create operation accepts `client_id`, including inboxes, pods, webhooks, drafts, and domains.
* Two simultaneous first creates with the same `client_id` can leave one with `409` `race_condition`. Retrying it returns the original resource.
* `client_id` and `Idempotency-Key` values are 1 to 256 characters from `A-Z a-z 0-9 - . _ ~`. `@` is not allowed, so replace it when deriving from an email address (`user_at_example.com`). A disallowed character in `client_id` fails as `400` `validation_error` before anything is created.
* Do not reuse one `client_id` across different resources, like an inbox and a webhook.
* `Idempotency-Key` works on every send: new messages, replies, reply-alls, forwards, and draft sends. One key per email.
* A send's replay window holds for 24 hours after the send completes, then the key is free to reuse. Keys are scoped to the organization.
* The same `Idempotency-Key` with a different message, sending inbox, or send endpoint returns `409` `conflict`.
* A retry while the first send attempt is still in flight returns `409`. Wait briefly and retry the identical request. A first attempt that died without completing frees the key after a short window.
* The `Retry-After` header is reachable on SDK exceptions through `rawResponse.headers` in TypeScript and `headers` in Python.
* `message_rejected` means no email went out. The cases: a recipient on a send block list (the `fix` names the stored entry, which can be a whole domain, and its exact delete path), an active send allow list that does not include the recipient, or an attachment URL that could not be fetched (use a URL that returns `200` without authentication, or send the attachment inline as base64).
* A scoped key receives `404` `not_found` for a real resource outside its organization, pod, or inbox scope, intentionally indistinguishable from a resource that does not exist. List the resource type with the same key, or retry with a broader-scoped key, before treating the identifier as wrong.
* A `404` whose message is `Route not found` means no route matches the path and HTTP method. Every documented route lives under `https://api.agentmail.to/v0`.
* An empty path parameter, like a blank `inbox_id` in the URL, can surface as `403` instead of `400`.
* When logging a failure, record the route, HTTP status, `code`, `fix` when present, the resource ids from the path, your own `client_id`, the retry attempt, and the honored `Retry-After`. Never log API keys, `Authorization` headers, or email content such as bodies, attachments, previews, and recipients.
* For a high-stakes email, create a draft with a `client_id` and send the draft. A sent draft is deleted, so a repeated send fails instead of emailing twice.

## Not supported

* Do not branch on `name` or `message`. They keep long-standing wording for backward compatibility, so a permission denial still reads `Forbidden`.
* A repeat create with a known `client_id` ignores the repeat's other fields. It is not applied as an update.
* A key cannot grant a permission it lacks. When the gate is the key's scope or the organization's state, like pending agent verification, a new key at the same scope cannot help.
* Block list entries added automatically from bounces, complaints, and unsubscribes are read-only. Email [support@agentmail.cc](mailto:support@agentmail.cc) to have one reviewed. Deleting an entry stored at a broader scope than your key, like an organization-level block hit by an inbox-scoped key, needs a key at that scope.
* An explicitly empty `Idempotency-Key` value is rejected with `400`. It does not silently send without protection.
* Retrying `account_suspended`, or retrying a quota-driven `429` before its window resets, cannot succeed.
* On an agent organization that has not completed verification, sending is restricted to the human's email until the verification finishes.

## Errors

| `code`                      | Status | Cause                                                                                                                                                    | Fix                                                                                                                        |
| --------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `missing_authorization`     | 401    | The `Authorization` header is absent, skips the case-sensitive `Bearer ` scheme, or carries a broken key value.                                          | Send `Authorization: Bearer <api_key>`.                                                                                    |
| `invalid_token_type`        | 401    | A console session token was used where an API key is required.                                                                                           | Use an API key. Keys start with `am_`.                                                                                     |
| `unknown_api_key`           | 401    | The key is not recognized or was revoked.                                                                                                                | Copy the full key again or create a new one.                                                                               |
| `unauthorized`              | 401    | Generic authentication failure.                                                                                                                          | Send a valid API key. Do not retry unchanged credentials.                                                                  |
| `missing_permission`        | 403    | The key lacks a permission this operation needs.                                                                                                         | The `fix` names the missing permission and how to get a key that holds it.                                                 |
| `permission_escalation`     | 403    | An API key create or update asked for a permission the calling key does not hold.                                                                        | Remove the extra permissions, or use a key that already holds them.                                                        |
| `unrestricted_key_required` | 403    | Creating or reconfiguring an unrestricted key needs an unrestricted credential.                                                                          | Retry with a dashboard session or an unrestricted key.                                                                     |
| `forbidden`                 | 403    | The key's scope (organization, pod, or inbox) does not cover this action.                                                                                | Use a key whose scope contains the resource.                                                                               |
| `already_exists`            | 403    | A resource with these details already exists in your organization.                                                                                       | Fetch or update the existing resource. On username collisions, `suggestions` lists available alternatives.                 |
| `resource_taken`            | 403    | The requested value, like an inbox username, belongs to another organization.                                                                            | Pick a different value. `suggestions` lists up to 3 available ones.                                                        |
| `limit_exceeded`            | 403    | A resource limit was reached. This check runs before the name-collision check, so at a cap a duplicate username returns this code, not `already_exists`. | Remove a resource, or raise the cap as the `fix` directs.                                                                  |
| `domain_not_verified`       | 403    | The sending domain has not completed DNS verification.                                                                                                   | Add the DNS records for the domain and verify it before sending from its addresses.                                        |
| `message_rejected`          | 403    | AgentMail refused to send the message. No email went out.                                                                                                | Read the `fix`: block list entry, allow list miss, or unfetchable attachment URL.                                          |
| `account_suspended`         | 403    | The account is suspended. `suspended_reason` names the cause. On a send, `name` and `message` still read like a rejected message, so branch on the code. | Email [support@agentmail.cc](mailto:support@agentmail.cc). Retrying keeps failing until the suspension is resolved.        |
| `validation_error`          | 400    | One or more request fields failed validation.                                                                                                            | Correct the fields named in `errors[]` and resend.                                                                         |
| `query_range_too_wide`      | 400    | A metrics query asked for too wide a time range.                                                                                                         | Narrow the range, or increase the period or bucket size.                                                                   |
| `not_found`                 | 404    | No resource with this identifier is visible to your key.                                                                                                 | Check the id, your key's scope, and your label-read permissions.                                                           |
| `conflict`                  | 409    | The request clashes with another request under the same `Idempotency-Key`.                                                                               | Retry the identical request, or use a new key for a new message.                                                           |
| `race_condition`            | 409    | A concurrent modification collided with yours.                                                                                                           | Re-fetch the resource for its latest state, then retry.                                                                    |
| `resource_deleting`         | 409    | The resource is being deleted and cannot be used.                                                                                                        | Wait for the deletion to finish, or use a different resource.                                                              |
| `cannot_delete`             | 409    | Dependent resources still block the deletion.                                                                                                            | Resolve the blocker named in the message, then retry.                                                                      |
| `unprocessable`             | 422    | The request is well formed but cannot be processed as is, like a send with no recipient in `to`, `cc`, or `bcc`.                                         | Adjust the request per the message, then retry.                                                                            |
| `rate_limit_exceeded`       | 429    | Requests came too fast, or a usage quota ran out.                                                                                                        | Honor the `Retry-After` header, then retry with exponential backoff. A daily or monthly quota only resets with its window. |
| `internal_error`            | 500    | A server-side failure, not a problem with your request.                                                                                                  | Retry with exponential backoff. If it persists, email [support@agentmail.cc](mailto:support@agentmail.cc).                 |
| `service_unavailable`       | 503    | A downstream dependency is temporarily unavailable.                                                                                                      | Retry after a short delay with exponential backoff.                                                                        |

## Verify

```bash
curl "https://api.agentmail.to/v0/inboxes/missing@agentmail.to" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
```

A working key returns `404` with a coded body:

```json
{
  "name": "NotFoundError",
  "code": "not_found",
  "message": "Inbox not found",
  "fix": "No inbox with the given identifier is visible to this credential. ...",
  "docs": "https://docs.agentmail.to/errors#not_found"
}
```

A `code` field in the body proves the request reached the API with a usable key. A bare `{"message": "Unauthorized"}` or `{"message": "Forbidden"}` means it was stopped at the edge, so recheck the key and header. The fastest key check is an authenticated read like `agentmail inboxes list`.

## Related

* [/advanced/plans-and-usage](/advanced/plans-and-usage): the plan allowances and send quotas behind `limit_exceeded` and `429` responses.
* [/core/send](/core/send): the `Idempotency-Key` header in action on every send, reply, and forward.
* [/advanced/custom-domains](/advanced/custom-domains): the DNS verification behind `domain_not_verified`.
* [/core/receive](/core/receive): dedupe handled messages by updating labels so the same email is not composed twice.
