Browse documentation

Error reference

Every error response carries the same JSON envelope. Branch on code, never on the error prose or the HTTP status alone — code is the stable machine key; error is a human sentence that may change; details is optional structured context.

{
  "error": "This mailbox restricts agent sends to approved recipients.",
  "code": "RECIPIENT_AGENT_CONTAINED",
  "details": {
    "reason_axis": "recipient_containment",
    "remedy": "add_recipient_to_allowlist",
    "cheapest_next_step": "no_cost_remedy"
  }
}
  • HTTP status groups the class of failure (4xx caller, 5xx transient). Use it for a coarse retry decision; use code for the precise cause and remedy.
  • details is present on some codes only. Two families put machine-routable data there: the denial envelope (capability denials) and the RATE_LIMITED variants. Everything else is a bare { error, code }.

This page is the single source for the error-code catalog. For the order gates are checked on a send, see /agents/send-gates; for the four governed send outcomes see /agents/send-outcomes; for message states and scan-verdict vocabulary see /agents/messages; for the attachment staging lifecycle see /agents/attachments; for CLI exit codes see /agents/cli.

Denial envelope (capability 403 / 429)

Capability denials — "your plan/credential/mailbox setting/capacity won't allow this" — emit a denial envelope in details so you can route on the response instead of parsing prose. Detect one by the presence of details.reason_axis:

{
  "error": "This feature requires the pro plan or above.",
  "code": "TIER_LIMIT",
  "details": {
    "reason_axis": "tier",
    "remedy": "upgrade_tier",
    "required_tier": "pro",
    "cheapest_next_step": "pro",
    "upgrade_url": "https://app.replylayer.ai/settings/billing"
  }
}

reason_axis is a closed enum — exactly why the request was denied. remedy is the matching action. Switch on reason_axis first, then read the typed sibling field the remedy points at.

reason_axisMeaningremedyWho fixesupgrade_url?
tierYour plan does not include the featureupgrade_tier (target → required_tier)Human / admin (billing)Yes
key_roleWrong credential — a Bearer/agent key on a human-session-only surfaceuse_sessionHuman (dashboard session)No
mailbox_configThe mailbox setting is offenable_mailbox_mode (target → required_mailbox_mode)Admin (dashboard)No
trust_capacityA rate/volume capacity ceiling was reachedadd_paygo_credits_or_await_trust_rampHuman/admin, or waitYes
account_stateAccount lifecycle (e.g. sandbox trial ended)add_paygo_credits_or_upgradeHuman / adminYes
account_policyThe capability is disabled for API keys on this accountenable_account_policyAdmin (dashboard session)No
recipient_containmentAn agent-origin send was contained to approved recipientsadd_recipient_to_allowlist (ask_account_owner_to_add_recipient on RECIPIENT_WRITE_AGENT_CONTAINED)Admin (add to allowlist / opt out); agent can continue an existing threadNo
sandbox_recipientA Sandbox send's recipient is not one the account may email yet (not your own address, not on the account's recipient list, not a reply to an authenticated sender)add_recipient_or_add_paygo_creditsAdmin, or an uncontained agent when agent recipient writes are on (add the recipient: ask them to confirm, or add them as someone the account knows while attestations_remaining > 0), or human (add pay-as-you-go credit)Yes
accountability_floorRelaxing injection defense needs an accountable billing partyset_up_billingHuman / admin (billing)No

Field notes:

  • upgrade_url attaches only on the monetary axes (tier, trust_capacity, account_state, sandbox_recipient) and only when a public dashboard link base is configured. It is omitted, never null — a non-monetary denial (a session swap, a mailbox toggle) deliberately carries no billing link so it can't misdirect you toward "upgrade".
  • cheapest_next_step is the smallest money that unblocks you: paygo | starter | pro, or no_cost_remedy when the fix is a session/setting/time, not spend. On a tier denial, required_tier is the minimum plan; cheapest_next_step: "paygo" beside required_tier: "starter" means pay-as-you-go credit also unlocks the feature, and the error message names both. A Pro-only feature carries cheapest_next_step: "pro".
  • Typed context may include feature, required_tier, current_tier, required_mailbox_mode, effective_daily_limit, current_count, max_allowed, attestations_limit, attestations_remaining. Every field is an enum, number, or server-derived URL — never free-form operator text.
  • An agent cannot self-serve key_role, account_policy, or accountability_floor denials. These require a human dashboard session (with fresh re-auth for the loosening ones). In particular, loosening instruction trust is a dashboard-session + re-auth action only — an API key can never enable it for itself.

RATE_LIMITED disambiguation

RATE_LIMITED (HTTP 429) is overloaded: it covers failed authentication, your daily send quota, a new-account warm-up burst limit, and generic short-window throttles. Branch on the shape of details, not on the code:

VariantSignalMeaningBackoff
Failed authenticationdetails.reason === "failed_authentication" (with retry_after, plus a Retry-After header)Your key is wrong or revokedFix the key; do not retry. Waiting and resending the same key never succeeds
Daily send budgetdetails.daily_limit present (with sends_remaining: 0, reset_at ISO-8601)You spent today's send allowanceRetry after reset_at (midnight UTC), or add pay-as-you-go credits / upgrade
New-account warm-updetails.reason === "new_account_warmup" (with retry_after_seconds)A send-rate warm-up burst limit; verifying your own sending domain lifts itRetry after retry_after_seconds
Generic short-windowdetails.retry_after (seconds) or no detailsA short-window request throttleWait the window (or a few seconds) and retry

Check the discriminators in this order. First, details.reason === "failed_authentication" on its own → the key is wrong or revoked: stop and fix it (a valid key is never refused by this block). Next, the presence of details.daily_limit → daily quota: wait for the reset or buy capacity. Next, details.reason === "new_account_warmup" → retry after retry_after_seconds. Only what remains (details.retry_after, or no details) is a generic short-window limit, answered by "wait a moment and retry". One no-details case to know: an address that has produced more than 600 failed key lookups in the current minute gets 429 for every key, valid or not, until the minute ends (it carries Retry-After). Tier and quota values live at /docs/limits.

Content blocks are not always a 4xx

A scanner block on a synchronous send/reply is folded into the message's terminal state and returns success-shaped unless you opt into strict outcomes with Prefer: outcome=strict (then it surfaces as EMAIL_EFFECT_REJECTED, below). The draft send path always fails loud (DRAFT_REJECTED_BY_RESCAN, 409). See /agents/send-outcomes for the governed-outcome contract.

Scan findings themselves are not error codes — they live on the message's scan_result_json. Two per-finding fields are agent-relevant:

  • failure_class — "inference_error" marks an infrastructure failure (a scanner timeout/outage), not a content judgment. Such findings also carry retryable: true, and the customer-facing reason collapses to a static infrastructure string. A held or blocked send with failure_class: "inference_error" is safe to retry as-is; a genuine content block ("model_judgment") is not — edit the content or escalate.
  • agent_instructions[] — optional, machine-readable handling guidance computed structurally from typed fields (vendor-free, index-agnostic). Present on non-allow findings that have concrete guidance; absent on inference_error.

Verdict vocabulary (quarantined vs blocked, releasable vs terminal) is documented at /agents/messages.

Code catalog

HTTP status, cause, denial reason_axis (where applicable), who can fix it, and whether a straight retry can succeed. "Who fixes" is agent (your key can recover), admin (an account admin, dashboard), or human (a dashboard session, sometimes with re-auth).

Authentication

CodeHTTPWhen it firesFixRetry
UNAUTHORIZED401Missing, malformed, or unknown API key, or a wrong secretagent (supply a valid key)No
API_KEY_REVOKED401The exact key presented was revokedhuman (create a new key in the dashboard)No
API_KEY_FORMAT_LEGACY401Key is in a retired format and no longer acceptedagent (mint a current-format key)No
EMAIL_NOT_VERIFIED403The account's email is unverified. Also returned by POST /v1/auth/resend-phone-verification: the first SMS is texted when the email is verified, so resending earlier is refusedhuman (verify email)No
PHONE_NOT_VERIFIED403The required signup phone is unverifiedhuman (verify SMS code)No
PHONE_REQUIRED400An OAuth new-account branch omitted the mandatory phone numberhuman (restart signup with a number)No
PHONE_INVALID400Signup/resend phone number is not in a valid international formathuman (correct it)No
PHONE_COUNTRY_NOT_SUPPORTED400The number’s country is outside the SMS verification allowlisthuman (use a number from a supported country)No
NO_PHONE_VERIFICATION_PENDING400No current SMS challenge existshuman (request a new code)No
INVALID_PHONE_VERIFICATION_CODE400The SMS code is wronghuman (re-enter code)No
PHONE_VERIFICATION_EXPIRED410The 5-minute SMS challenge expiredhuman (request a new code)No
PHONE_VERIFICATION_ATTEMPTS_EXCEEDED429Too many wrong codes invalidated the challengehuman (request a new code)No
PHONE_VERIFICATION_RATE_LIMITED429SMS send/check limits were reachedhuman/agent (wait for Retry-After)Yes (after window)
PHONE_VERIFICATION_UNAVAILABLE503SMS verification or its fail-closed limiter is unavailablehuman/agent (retry later)Yes
PHONE_ALREADY_VERIFIED409Resend/correction was attempted after phone verification completedhuman/agent (stop resending)No
PHONE_VERIFICATION_SUPERSEDED409A newer concurrent SMS request replaced this sendhuman/agent (retry deliberately)Yes
ACCOUNT_SUSPENDED403The account is suspendedhuman/admin (reinstate)No
AUTH_TEMPORARILY_BLOCKED429Too many failed auth attempts — a brute-force lockagent (wait, then retry)Yes (after window)
REAUTH_REQUIRES_SESSION403A route needs a dashboard session with fresh TOTP/SMS/password re-auth; a Bearer/agent key is rejectedhuman (dashboard re-auth)No
BILLING_REQUIRES_SESSION403A billing route was called with a Bearer key; billing is session-onlyhuman (dashboard)No

SMS-based MFA & step-up re-auth

SMS is an alternative second factor to an authenticator app, and an SMS code is also the fresh re-auth on the session-only loosening gates. The ceremonies these codes belong to (enroll, method switch, SMS-disable proof, lost-factor recovery, phone change, and the scoped step-up send) are dashboard-session-only — an API key gets REAUTH_REQUIRES_SESSION (above) before any of them fires, so an agent integration never hits them directly. The two transient SMS send failures (MFA_SMS_RATE_LIMITED, MFA_SMS_UNAVAILABLE) can also surface on the public SMS login step. Catalogued here for completeness.

CodeHTTPWhen it firesFixRetry
MFA_SMS_NOT_AVAILABLE400An SMS ceremony or step-up was requested but SMS-based MFA isn't enabled (or the account has no verified phone)human (enable SMS MFA / verify a phone)No
REAUTH_SMS_CHALLENGE_REQUIRED400An SMS step-up or ceremony needs a challenge minted first (POST /v1/auth/reauth/sms/send, or /mfa/disable/send-sms for a current-factor proof)human (mint the challenge, then resubmit)No
REAUTH_INVALID_SMS_CODE400The SMS re-auth or ceremony code is wrong or expiredhuman (re-enter, or resend)No
MFA_SMS_RATE_LIMITED429Too many SMS codes requested (carries Retry-After / details.retry_after)human/agent (wait for the window)Yes (after window)
MFA_SMS_UNAVAILABLE503The SMS provider (or its fail-closed limiter) is momentarily unavailablehuman/agent (retry later)Yes
MFA_SMS_SEND_CONFLICT429A send for this account is already in flight — a concurrent step-up for the same action, or a concurrent sign-inhuman (wait a moment, then retry)Yes (short)
MFA_METHOD_SWITCH_REQUIRED400Tried to enroll a fresh method while one is already enabledhuman (use the method-switch ceremony)No
MFA_RECOVERY_ACK_REQUIRED400Confirmed enrollment/switch before acknowledging the saved recovery codeshuman (acknowledge, then confirm)No
MFA_NO_PENDING_CEREMONY400Acknowledged or confirmed with no staged ceremony (or it expired / was superseded)human (restart the ceremony)No
MFA_RECOVERY_WINDOW_EXPIRED400No usable one-shot recovery grant on this session (expired, consumed, or reserved) — the lost-factor disable / rebind flowshuman (log in again with a recovery code)No
REAUTH_SESSION_TOO_OLD403A provider-only account tried to enroll SMS MFA without a fresh sessionhuman (sign in again, then retry)No
PRIVACY_ACK_REQUIRED400The account's acknowledged Privacy Policy version predates the SMS-verification scope (the DPA was updated alongside); carries details.current_privacy_version / current_dpa_versionhuman (review and acknowledge the current Privacy Policy, resubmit with accept_privacy_version set to details.current_privacy_version)No
PASSWORD_CHANGE_UNAVAILABLE400The self-service password-change endpoint was called on an account with no password (sign-in is managed through Google/GitHub)human (sign in via the OAuth provider instead)No
EMAIL_CHANGE_COOLDOWN429The account's email was changed within the last 24 hours; carries details.retry_after (seconds until the window lapses)human (wait for the cooldown to lapse)Yes (after window)
EMAIL_CHANGE_TAKEN409The new address proved by the confirm code is already in use on another accounthuman (choose a different address and restart)No
EMAIL_CHANGE_INVALID_CODE400The email-change confirm code is wrong, expired, exhausted, submitted from a session other than the one that started the ceremony, or the account's MFA state changed since start — one generic code, no factor oraclehuman (restart the change from Settings)No

Authorization & scope

CodeHTTPWhen it firesAxisFixRetry
INSUFFICIENT_SCOPE403An agent (mailbox-scoped) key called an admin-only surface—admin (use an admin key)No
FORBIDDEN403The credential is valid but not permitted for this resource—agent/adminNo
MESSAGE_DELETE_NOT_PERMITTED403Agent message-delete is disabled for API keys on the accountaccount_policyadmin (enable the policy)No
RECIPIENT_WRITE_AGENT_CONTAINED403An agent key (every hosted-MCP sign-in connection is one) tried to add a Sandbox recipient (POST /v1/recipients, resend, add_recipient) while a mailbox it is bound to only lets it email people the account owner approves, or it is bound to none. Agent writes must also be on, else INSUFFICIENT_SCOPErecipient_containment (remedy: ask_account_owner_to_add_recipient)human (the account owner adds the person)No
RECIPIENT_WRITE_UNAVAILABLE503Adding or re-sending a Sandbox recipient is unavailable (its limiter or confirmation email transport); nothing was written or charged—agent (retry shortly; if it fails again, stop and tell the user)Yes (once)

Send gates — recipient policy

Fired at send/reply/draft time. The evaluation order and preflight pattern are documented at /agents/send-gates.

CodeHTTPWhen it firesAxisFixRetry
RECIPIENT_SUPPRESSED403Recipient is on the account do-not-contact list—admin (remove suppression) or change recipientNo
RECIPIENT_NOT_ON_ALLOWLIST403Mailbox is in allowlist mode and the recipient is not approved—admin (approve the address); agent can continue an existing threadNo
RECIPIENT_AGENT_CONTAINED403Agent-origin send to a recipient that is neither approved nor a thread participant, when containment is enforced for the mailboxrecipient_containmentadmin (add to allowlist / opt out); agent can continue an existing threadNo
ESTATE_CONTINUITY_REPLY_ONLY403The sending mailbox is a trial-era mailbox on a ReplyLayer-managed domain, kept active on a paid account as a reply-only continuity mailbox. It still receives inbound and may reply where the recipient is a visible inbound sender in the same thread and mailbox, but it cannot start a new thread. Binds human and agent sends alike.—agent (reply within the existing thread), or send from a sending mailbox (any mailbox whose continuity is false; set up at app.replylayer.ai/mailboxes)No
RECIPIENT_UNDELIVERABLE422The recipient's domain has no reachable mail servers (no MX/A), or deep verification confirmed the mailbox doesn't exist—agent (fix the address)No
RECIPIENT_ADDRESS_INVALID422The recipient fails a strict syntax check beyond basic email-format validation—agent (fix the address)No
RECIPIENT_DOMAIN_TYPO_SUSPECTED422The recipient domain looks like a single-character typo of a common provider (suggestion in the error message)—agent (fix the typo, or resend as-is if correct)No
RECIPIENT_ROLE_ADDRESS422The recipient is a role/distribution mailbox (noreply@, etc.), not an individual inbox. Exempt on a reply / thread continuation.—agent (use a different recipient)No
RECIPIENT_DISPOSABLE_ADDRESS422The recipient domain is a disposable/temporary email provider. Exempt on a reply / thread continuation.—agent (ask for a permanent address)No
AMBIGUOUS_THREAD_MAILBOX409A thread_id resolves in more than one of your mailboxes—agent (pass the mailbox)No
AMBIGUOUS_THREAD_RECIPIENT422A thread has multiple participants and no recipient was chosen—agent (pass one participant)No
RECIPIENT_NOT_IN_THREAD422The chosen recipient is not a participant of that thread—agent (pick a participant)No
THREAD_HAS_NO_INBOUND_RECIPIENT422The thread is send-only — nobody to reply to—agent (start a new send)No
REPLY_LOOP_DETECTED429The reply-loop guard tripped on repeated back-and-forth—agent (pause, don't auto-retry)No (investigate)

Send gates — mailbox, domain & account state

CodeHTTPWhen it firesAxisFixRetry
MAILBOX_SUSPENDED403The target mailbox is suspended—adminNo
DOMAIN_SUSPENDED403The sending custom domain is suspended—adminNo
DOMAIN_UNAVAILABLE503The sending domain is not currently usable—admin, or retry laterYes (transient)
DOMAIN_UNHEALTHY503The sending domain failed a health check—admin, or retry laterYes (transient)
SANDBOX_RECIPIENT_NOT_VERIFIED403A Sandbox send to a recipient with no admission basis (not your own address, a simulator scenario, a reply to an authenticated sender, or a confirmed or account-added recipient). On a draft send it surfaces as 409 DRAFT_REJECTED_BY_RESCAN with the same code and envelope in details.denialsandbox_recipientadmin or uncontained agent (POST /v1/recipients: ask them to confirm, or attest: true while attestations_remaining > 0), or human (add paygo credits / upgrade)Yes (once the recipient is confirmed or added)
SANDBOX_RECIPIENT_DOMAIN_CAP_EXCEEDED429A Sandbox account made 3 send attempts or confirmation requests/resends to one non-simulator recipient domain in the rolling 24-hour window; exact first-party simulator scenarios are exempt—agent (wait for an earlier attempt to leave the window), or human (add paygo credits / upgrade)Yes (after window)
SANDBOX_TRIAL_BUDGET_EXHAUSTED403The sandbox cumulative send cap is spenttrust_capacityhuman (add paygo credits / upgrade)No
SANDBOX_TRIAL_EXPIRED403The sandbox trial window endedaccount_statehuman (add paygo credits / upgrade)No

Governed send outcomes (strict mode)

Emitted only when the caller sets Prefer: outcome=strict on send/reply and the send did not deliver. Without the header these outcomes surface success-shaped. Full contract: /agents/send-outcomes.

CodeHTTPWhen it firesFixRetry
EMAIL_EFFECT_REJECTED422Content policy rejected the send (terminal)agent (edit content / escalate)No — never retry a block as-is
EMAIL_EFFECT_HELD409Accepted into governance, awaiting human review (releasable)human (review — tell the account owner; details.hold_context.review_url links the review page when present)No (wait for review)
EMAIL_EFFECT_HELD_INFRA503Held by a transient infrastructure erroragent (retry shortly, honor Retry-After)Yes

Content sanitization (outbound)

CodeHTTPWhen it firesFixRetry
OUTBOUND_HTML_ACTIVE_CONTENT_REJECTED400HTML body carried active/unsafe constructs (details.categories names them, e.g. script, iframe, form, event_handler, javascript_url)agent (remove those constructs, or send plain text)No
OUTBOUND_HTML_SANITIZE_FAILED400HTML could not be parsed, or exceeded the input size limitagent (simplify/shrink the HTML)No
BODY_FORMAT_HTML_SESSION_ONLY400Requested an HTML-format read with a Bearer/agent key (session-only)agent (request a non-HTML format)No

Drafts & review

CodeHTTPWhen it firesFixRetry
DRAFT_REJECTED_BY_RESCAN409The authoritative rescan at send time blocked the draftagent (edit content)No
DRAFT_ALREADY_SENT409The draft was already sentagent (stop)No
DRAFT_EDITED_SINCE_HOLD409The draft changed after being held for reviewhuman/agent (re-review)No
DRAFT_NOT_HELD409A review action targeted a draft that isn't in a held stateagent (recheck state)No
DRAFT_WRONG_STATE409The draft is in a state that forbids this actionagent (recheck state)No
DRAFT_HELD_DECISION_MISSING409A held-draft send lacked the required review decisionhuman (record a decision)No
BLOCK_TERMINAL403The message is in a terminal blocked state and cannot be actioned—No
ALREADY_RELEASED409The message was already releasedagent (stop)No
REVIEW_NOTE_REQUIRED400A review action requires a note and none was suppliedagent (supply a note)No
SCHEDULED_SEND_QUOTA_EXCEEDED429Too many scheduled sends queuedagent (reduce/space out)Yes (later)
ATTACHMENTS_REQUIRE_SYNC_SEND400Attachments can't ride an async/scheduled pathagent (send synchronously)No

Attachments

Staging + preview codes. The upload → poll → consume-once lifecycle is documented at /agents/attachments.

CodeHTTPWhen it firesAxisFixRetry
OUTBOUND_ATTACHMENTS_DISABLED403Outbound attachments aren't enabled on the mailbox—human (enable, session + re-auth)No
OUTBOUND_IMAGE_DISCLAIMER_REQUIRED400An image attachment before the image-risk disclaimer is accepted (details.required_version)human (accept disclaimer)No
OUTBOUND_ATTACHMENT_TYPE_NOT_ALLOWED400The file's sniffed family is not an allowed outbound type, or a text file's extension is not .txt, .text, .log, .md, .json or .csvagent (send an allowed type or rename the text file)No
OUTBOUND_ATTACHMENT_FILENAME_INVALID400The declared filename is malformedagent (fix filename)No
OUTBOUND_ATTACHMENT_STAGING_QUOTA_EXCEEDED429Too many staged-but-unsent attachmentsagent (consume/expire some, retry)Yes (later)
ATTACHMENT_TOO_LARGE400The uploaded file exceeds the size limitagent (shrink)No
ATTACHMENT_MULTIPLE_FILES400More than one file part in a single-file uploadagent (one file per request)No
ATTACHMENT_BLOCKED400The attachment type is policy-blockedagent (different type)No
ATTACHMENT_INFECTED400The file failed virus scanningagent (do not send)No
ATTACHMENT_AV_ERROR400The file could not be virus-scanned (fail-closed)agent (re-upload)Yes (re-upload)
ATTACHMENT_SCAN_PENDING409A referenced attachment's content scan hasn't finishedagent (poll, then resend)Yes (after poll)
ATTACHMENT_ALREADY_CONSUMED409A staging handle was already consumed by a prior sendagent (re-upload for a new send)No
ATTACHMENT_ACCESS_DISABLED403Raw attachment access is disabled for this credentialkey_rolehuman (session)No
ATTACHMENT_POLICY_CONFLICT409The mailbox's attachment policy changed while an attachment-access write was processed; nothing was applied—agent (re-read the mailbox, then retry)Yes (after re-read)
ATTACHMENT_NOT_STORED404The raw bytes were not retained—No
ATTACHMENT_IMAGE_RISK_NOT_ACCEPTED403Inline image access before the image-risk disclaimer is accepted—human (accept)No
ATTACHMENT_INLINE_NOT_IMAGE403Inline-data request for a non-image part—agentNo
ATTACHMENT_INLINE_TOO_LARGE413Inline-data part exceeds the inline size limit—agentNo
ATTACHMENT_PREVIEW_PENDING409The safe-text preview is still generating—agent (retry shortly)Yes
ATTACHMENT_PREVIEW_NOT_AVAILABLE404No safe preview exists (metadata-only, non-previewable, or generation failed)—No
ATTACHMENT_PREVIEW_DISABLED403Previews aren't available — mailbox mode off, plan-gated, or key not permitted (axis is mailbox_config or tier)mailbox_config / tieradmin / humanNo
TIER_LIMIT403The action needs a higher plan (details.feature, required_tier)tierhuman (upgrade)No

Idempotency

The immediate-send idempotency contract keys on the Idempotency-Key header. A repeat of a proven send replays the prior outcome (same message_id) rather than erroring.

CodeHTTPWhen it firesFixRetry
IDEMPOTENCY_KEY_REQUIRED400An idempotency probe/route needs the key and none was givenagent (supply the key)No
IDEMPOTENCY_KEY_INVALID400The key is a redaction placeholder such as *** or opencl…n2tx, usually because the agent host masked a field ending in _keyagent (stop; resend with the literal key from a new session; never retry the masked value)No
IDEMPOTENT_REQUEST_IN_FLIGHT409A concurrent request with the same key is still running (carries Retry-After)agent (retry after the window)Yes (short)
IDEMPOTENT_REQUEST_NOT_PROVEN_SENT409A prior attempt's delivery is indeterminate; replay is withheld until provenagent (retry after reconciliation)Yes (after reconcile)
IDEMPOTENCY_KEY_BOUND_TO_DRAFT409The key was already used on a draft pathagent (use a fresh key)No
IDEMPOTENCY_KEY_BOUND_TO_IMMEDIATE_SEND409The key was already used on an immediate sendagent (use a fresh key)No
IDEMPOTENCY_KEY_DRAFT_CONSUMED409The draft this key was bound to is already consumedagent (use a fresh key)No

Rate limits, quota & capacity

CodeHTTPWhen it firesAxisFixRetry
RATE_LIMITED429A limit was hit — see disambiguation—agent (back off per details)Yes
SANDBOX_TRIAL_BUDGET_EXHAUSTED403Sandbox cumulative send cap spent (also listed above)trust_capacityhumanNo
SANDBOX_TRIAL_EXPIRED403Sandbox trial window ended (also listed above)account_statehumanNo
TIER_LIMIT (attestation)403POST /v1/recipients with attest: true when adding people without confirmation is not available (details.attestations_limit: 0), or when none are left this trial (attestations_remaining: 0, plus forfeited: true when a spam complaint ended them)trust_capacityagent (add without attest so the person confirms by link), or human (add paygo credits)No

Billing

Send-path billing gates. Managing billing is a human/admin dashboard action.

CodeHTTPWhen it firesFixRetry
BILLING_LAPSED403Billing lapsed — sends are gatedhuman (restore billing)No
BILLING_REACTIVATION_PENDING403Reactivation is pendinghuman (wait/complete)No
PAYGO_INSUFFICIENT_CREDITS403Pay-as-you-go balance can't cover the sendhuman (top up credits at Settings → Billing)No

Validation & transport

CodeHTTPWhen it firesFixRetry
VALIDATION_ERROR400 / 422Request failed schema (400) or semantic (422) validation; details may name the fieldagent (fix input)No
DISPLAY_NAME_INVALID422Mailbox display_name failed structural validation; details.reason ∈ control_chars / length / address_like / platform_brandagent (fix input)No
INVALID_BODY_FORMAT400The body/format combination is unsupportedagent (fix format)No
INVALID_STATE409The target resource is in a state that forbids the actionagent (recheck state)No
NOT_FOUND404The resource does not exist (or isn't visible to this credential)agent (check id/scope)No
REPORT_OUTBOUND_UNSUPPORTED422Report/block was invoked on an outbound message (inbound-only)agent (only report inbound)No
LEGAL_HOLD_ACTIVE409A legal hold blocks the mutation (e.g. delete)admin/legalNo
WAIT_UNAVAILABLE503The long-poll/wait facility is momentarily unavailableagent (retry)Yes
WEBHOOK_CREATE_RETRY503POST /v1/webhooks kept conflicting with a concurrent change to the account's webhooks; nothing was created (carries Retry-After)agent (retry after details.retry_after)Yes
UPSTREAM_ERROR502A dependency returned an erroragent (retry with backoff)Yes
INTERNAL_ERROR500An unexpected server erroragent (retry with backoff; report if persistent)Yes

Retry semantics in one line

  • 5xx (INTERNAL_ERROR, UPSTREAM_ERROR, DOMAIN_UNAVAILABLE/_UNHEALTHY, WAIT_UNAVAILABLE, WEBHOOK_CREATE_RETRY, EMAIL_EFFECT_HELD_INFRA) and short-window 429s → retry with backoff.
  • IDEMPOTENT_REQUEST_IN_FLIGHT / _NOT_PROVEN_SENT / ATTACHMENT_SCAN_PENDING / ATTACHMENT_PREVIEW_PENDING → poll/retry after the indicated window.
  • Content blocks (EMAIL_EFFECT_REJECTED, DRAFT_REJECTED_BY_RESCAN, the OUTBOUND_HTML_* rejects) → never retry unchanged; edit the content, unless the finding carries failure_class: "inference_error" (retryable: true).
  • Capability denials (any details.reason_axis) → apply the remedy; most need a human/admin, and none can be resolved by re-sending the same request.
  • Authentication failures (UNAUTHORIZED, API_KEY_REVOKED, and the 429 with details.reason: "failed_authentication") → stop and fix the key; never retry.

The RATE_LIMITED daily-budget path and idempotency guards are proven never to double-charge or double-send on a retry.