Skip to main content

Errors — the error envelope and every code

One envelope on every failure — type, code, message, param, request_id. The type → status mapping and the full code list.

Every failed request returns the same shape, with a non-2xx status. There is one envelope to parse, always:

{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_field",
    "message": "Subject is required.",
    "param": "subject",
    "request_id": "req_01KVF46QACE5C361SBYTP6JE0S"
  }
}
  • type — the broad category; it usually determines the HTTP status.
  • code — the specific, stable, machine-readable reason. Branch on this.
  • message — a human-readable explanation. Do not parse it; it can change.
  • param — the offending field for validation errors, otherwise null.
  • request_id — also returned as the X-Request-Id header. Quote it to support.
  • resets_at and retry_after_seconds — on every 429: when the request can succeed (UTC) and how many seconds away that is. Both are null when waiting will not help, for a cap that only an upgrade or a deletion lifts.

Type → HTTP status

The type usually maps to the status, so you can handle whole classes at once:

  • invalid_request_error → 400 — malformed or invalid input. Some of its codes answer another status, such as duplicate_resource and agent_name_conflict (409), body_too_large (413) and unknown_test_address (422).
  • authentication_error → 401 — missing, invalid, revoked, or expired key.
  • permission_error → 403 — the key lacks the scope, or the workspace is not approved to send.
  • payment_error → 402 — payment required, subscription canceled, or a gated feature.
  • resource_error → 404 — the referenced object does not exist.
  • abuse_error → 422 — the request is well-formed but disallowed (suppressed recipient, paused agent).
  • rate_limit_error → 429 — a limit was hit; retry at the body’s resets_at.
  • api_error → 500 / 502 / 503 — an internal error, an upstream provider failure, or a service briefly unavailable.

Common codes

The codes you will branch on most:

  • Auth (401): missing_authorization, invalid_api_key, revoked_api_key, expired_api_key.
  • Scope & approval (403): insufficient_scope (a send or a draft that would create an agent needs manage), workspace_not_approved.
  • Connected apps (403): connected_app_not_allowed — an app connected by signing in asked for something no such app may do (see Connected apps).
  • Test mode (403): live_mode_required — test mode asked for something only live mode may do: trash or delete a thread that holds live mail, assign or unassign one, add or delete a note on one, change, send or delete a draft a live key made, reschedule or cancel a live scheduled send, or run the forwarding test, which sends a real email. A test key, or a dashboard session sending X-Mails-Mode: test, also gets it when it revokes a live key, or changes, pauses or archives an agent that has a live key tied to it, live mail (sent, scheduled or received) or a draft a live key made; do it with a live key or from the dashboard. The reverse is test_mode_required: a live key replying to test mail, or sending or scheduling a draft that answers it, which would mail an address a test made up.
  • Validation (400/422): missing_field, invalid_field, invalid_param_value (a value too long, too short, malformed or not one of those allowed) — check param.
  • Send-time (404/422): agent_not_found, agent_paused, agent_archived, recipient_suppressed, recipient_not_allowed (a recipient outside the agent’s allowlist_domains or on its blocklist_domains), and unknown_test_address (422) for an address at test.mails.ai other than the test address.
  • Limits (429): hourly_limit_exceeded, daily_limit_exceeded, monthly_limit_exceeded, free_tier_exceeded.
  • Billing (402): payment_required, subscription_canceled, feature_not_enabled, resource_cap_exceeded (a plan’s cap on agents, API keys, webhooks, open drafts or the files drafts hold).
  • Idempotency (400): invalid_idempotency_key — a key used again with a different body, or on a different route; use a new key.
  • Conflict (409): duplicate_resource — an idempotency key or a draft already in flight.
  • Agent name (409): agent_name_conflict — two live agents share the name a request used; use the agent’s agt_ id.
  • Upstream (502/503): upstream_error (usually 502) — the email service did not confirm it took the mail, so it is recorded as not sent, and the message names a request id to quote to support; upstream_unavailable (503) — it is not taking more mail right now and nothing was sent, so send it again after Retry-After.

duplicate_resource also answers a change that came too late: editing or deleting a draft, or rescheduling or canceling a scheduled message, after it started sending, and deleting a thread for good while one of its messages is being sent. Read the resource again before you retry. It also answers an If-Match on PATCH /v1/drafts/{id}, DELETE /v1/drafts/{id} or POST /v1/drafts/{id}/send that no longer matches, with param updated_at. A send gets the same answer, header or no header, when a change or a file reaches the draft just as the send begins. A chunk sent to a draft’s file that has finished uploading gets it with param index, and changes nothing.

invalid_field can be a 409 too: sending a draft with a file still uploading, or with a forwarded file that can no longer be read, answers it with param attachments.

An agent named by its name resolves to the calling key’s own agent first, then to a live agent over an archived one. Only the agent’s own routes (reading, changing or archiving it, its forwarding test, its inbox), an agent_id filter, reputation and POST /v1/test/inbound answer agent_name_conflict when two agents still match; a send keeps using the oldest of them. A key tied to one agent never gets that 409, which would name other agents to it. To such a key, a shared name that is not its own agent’s answers as any other agent does: 404 from the agent’s own routes, and 403 from a filter, reputation and POST /v1/test/inbound.

Handling errors well

  • Branch on code, not message. Codes are stable; messages are for humans and may be reworded.
  • Retry a 429 at resets_at. The body says when the request can succeed. A Retry-After header is sent only when that is 60 seconds or less away; an hourly, daily or monthly cap usually means a longer wait and sends none. Back off rather than hammering.
  • Retry only the safe ones. upstream_unavailable (503, after its Retry-After) and 500s are retryable with exponential backoff; pair retries with an Idempotency-Key so a retried send never duplicates.
  • Log the request_id. It is the fastest way for support to trace exactly what happened on a given call.

Every code

The code field takes one of these 68 values, the list in the API’s OpenAPI document: invalid_field, missing_field, missing_param, invalid_param_value, method_not_allowed, invalid_idempotency_key, duplicate_resource, slug_invalid, slug_reserved, slug_taken, agent_name_conflict, body_too_large, missing_authorization, missing_api_key, invalid_api_key, expired_api_key, revoked_api_key, invalid_access_token, email_not_verified, insufficient_scope, connected_app_not_allowed, sso_required, workspace_mismatch, agent_not_owned, rate_limit_exceeded, daily_limit_exceeded, hourly_limit_exceeded, monthly_limit_exceeded, agent_not_found, agent_paused, agent_archived, resource_not_found, recipient_suppressed, recipient_not_allowed, assignee_not_member, domain_not_verified, ses_role_not_assumable, ses_role_external_id_not_required, ses_permission_denied, ses_setup_failed, ses_message_rejected, ses_events_not_confirmed, payment_required, payment_method_failed, subscription_canceled, billing_state_inconsistent, free_tier_exceeded, plan_limit_exceeded, cold_email_prohibited, agent_suspended, new_workspace_fanout_exceeded, resource_cap_exceeded, workspace_cap_exceeded, feature_not_enabled, test_mode_required, live_mode_required, unknown_test_address, workspace_not_approved, spam_pattern_detected, classifier_rejected, high_injection_score, complaint_threshold_exceeded, bounce_threshold_exceeded, internal_error, internal_server_error, upstream_error, upstream_unavailable and service_unavailable.

Was this page helpful?