Agent Mail by Ackuna

Every page here is also markdown — add .md to the URL.

Errors

Every Agent Mail API failure — including framework-generated ones like a bad HTTP verb — uses the same envelope:

{
  "error": {
    "code": "invalid_request",
    "message": "The to field must be a valid email address.",
    "fields": {"to": ["The to field must be a valid email address."]},
    "doc_url": "https://agents.ackuna.com/docs/errors.md#invalid_request"
  }
}

fields is present only for validation failures, keyed by field name with a list of messages. quota is present on 429 quota_exceeded and names the quota that refused the request (outbound_daily, outbound_daily_shared_domain, outbound_monthly), matching the keys of quotas on GET /v1/me. doc_url is always present and points at the markdown source of this page, so a failing agent fetches text it can read rather than a rendered page — no human has to relay the explanation. The fragment names the ### {code} heading in that source; drop the .md for the same anchor on the HTML page a person reads.

Below is the complete, frozen list of codes. If you see a code that is not here, that is a bug: open an issue rather than trying to pattern-match the string.

unauthenticated

HTTP 401. The API key is missing, malformed, revoked, or has passed its expires_at. Check the Authorization: Bearer ak_... header is present and the key is still active.

insufficient_scope

HTTP 403. The key authenticated, but does not carry the scope the endpoint requires. message names the missing scope, e.g. "This API key is missing the required scope: messages:send.". Issue a new key with that scope, or ask an operator to grant it.

agent_mail_not_enabled

HTTP 403. The organization has not enabled Agent Mail (agent_mail_access is none). This is checked before scopes, so such an organization learns this and nothing about which scopes exist. Since 2026-09-11 every paid plan funds Agent Mail and grants access the moment the subscription activates, so in practice this is reached only by an organization on the free plan that has never switched it on. Such an organization enables Agent Mail from Workspace settings; a new one registers at /register?product=agent-mail or over POST /v1/signup. Until 2026-09 this code was named beta_access_required.

organization_suspended

HTTP 403. An administrator suspended the organization's Agent Mail access. Every key answers this until access is restored; inboxes keep receiving. Contact support.

email_verification_required

HTTP 403. No owner of the organization has verified their email address yet, so outbound sending is locked. Inboxes still receive, and keys, threads and webhooks work. A human owner verifies through the link Ackuna mailed at registration; an organization created over POST /v1/signup is verified by its sign-up code and never sees this.

email_already_registered

HTTP 409. POST /v1/signup was called with an email that already belongs to an Ackuna user. Sign in to the console with that address and issue an API key there; a sign-up is never attached to an existing organization. The address is compared by mailbox: a +tag variant, or a Gmail address with different dots, of an existing user's address is the same account.

registration_disabled

HTTP 503. Self-serve sign-up is closed, so POST /v1/signup and POST /v1/signup/verify create nothing. The operator controls it, and nothing in the request can change the answer. Retry much later, or ask a person to create the account in the console.

signup_pending

HTTP 409. POST /v1/signup was called for an address that already has a live code. The earlier code stays valid, so use it with the signup_id you got the first time; a repeat request never replaces it. Retry-After gives the seconds until that code expires, after which you may ask again.

signup_code_invalid

HTTP 422. The code sent to POST /v1/signup/verify does not match. message says how many attempts remain before the sign-up expires.

signup_expired

HTTP 410. The signup_id is unknown, already used, older than its expires_at, or has run out of attempts. Call POST /v1/signup again for a fresh code.

quota_exceeded

HTTP 403 or 429. A per-organization quota was reached.

queue_full

HTTP 429. This workspace already has the maximum number of messages waiting to be sent, so this one was not accepted. A workspace may have one hour of its sending waiting (its hourly limit), and never fewer than 100. Unlike quota_exceeded, nothing has been used up: the queue drains on its own, and the same request can be sent again. Retry-After is a fixed hint of 60 seconds, not an estimate of how long the backlog takes to clear. Sending more slowly is the right response.

message_not_retryable

HTTP 409. POST /v1/messages/{id}/retry was refused for this message. The message says which rule applies: only an outbound message whose status is failed can be retried; a message whose provider outcome was uncertain cannot, because it may already have been delivered; and a message whose inbox has been archived cannot. Read GET /v1/messages/{id} for the current state. To send the content again anyway, send a new message. Use a new Idempotency-Key for each retry decision: a request that repeats an earlier key is answered with that request's stored response, this refusal or an earlier 202, without looking at the message again.

content_rejected

HTTP 409. POST /v1/messages/{id}/retry was refused because Ackuna's pre-send content check did not let this message go: a reviewer rejected it, its review expired, or it repeated content that had been rejected before. The decision is final and the reason is not detailed. If you believe it was a mistake, contact support with the message id.

forbidden

HTTP 403. A refusal with no more specific code applies.

address_taken

HTTP 422. POST /v1/inboxes asked for a local part that is already in use on the target domain. Retry with a different username — this is not a validation problem, so it is its own code rather than folding into invalid_request.

address_reserved

HTTP 409. DELETE /v1/inboxes/{id}?permanent=true was asked to permanently delete an archived inbox on the shared platform domain. Addresses on a shared domain stay reserved for the workspace that had them, so they cannot be freed; restore the inbox instead, or create inboxes on a custom domain you own, where permanent deletion is allowed.

local_part_not_allowed

HTTP 422. POST /v1/inboxes asked for a username that is not available on the shared platform domain. Mail from agentmail.ackuna.com goes out under Ackuna's own identity, so it cannot carry a role address (such as postmaster, support or billing), a role word such as admin, security or no-reply anywhere in the name (security-team), or a brand, payment or sign-in term. The message does not say which rule matched. Choose another name, or create the inbox on a custom domain you own, where these names are not restricted.

display_name_not_allowed

HTTP 422. PATCH /v1/inboxes/{id} asked for a display_name that contains a brand, payment or sign-in term, on a shared-domain inbox. The display name is the From name of the inbox's mail, so PayPal Security <orders@agentmail.ackuna.com> would be the same impersonation as a brand-named address. Role words are fine in a display name ("Support desk"). Choose another name, or use an inbox on a custom domain you own, where display names are not restricted.

domain_not_allowed

HTTP 422. POST /v1/inboxes named a shared domain that no longer accepts new inboxes. Inboxes already on an older shared domain keep working, but new ones are created on the current shared domain, which the message names. agents.ackuna.com is such an older domain: its existing inboxes keep sending and receiving, but it can no longer be chosen for new inboxes, which go on agentmail.ackuna.com. Omit domain, or pass the domain the message names.

domain_taken

HTTP 422. POST /v1/domains asked to register a hostname that is already registered (by this organization or another). Retrying with the same hostname will not succeed.

not_found

HTTP 404. The identifier in the path (or referenced from the request body, such as reply_to_message_id) does not exist, or belongs to a different organization. A cross-organization identifier is always a 404, never a 403 — this API never confirms that another organization's resource exists.

method_not_allowed

HTTP 405. The path exists but does not support the HTTP verb used.

invalid_request

HTTP 422. Request validation failed. fields names every invalid field. This code is also used for a small number of business-rule rejections that are not quota- or identifier-related — for example, redriving a webhook delivery that is not dead, or deleting a receiving domain that still hosts inboxes. Read message for the specific reason.

inbox_paused

HTTP 409. POST /v1/inboxes/{inbox}/messages was called on an inbox whose status is paused. Resume the inbox (PATCH /v1/inboxes/{inbox} with {"status": "active"}) before sending.

domain_not_ready

HTTP 409. Either POST /v1/inboxes named a receiving domain that is not yet active, or a send was attempted from an inbox whose domain has no sending identity, or whose sending domain is reputation-blocked. For a domain that is not yet active, poll GET /v1/domains/{domain} until status is active. Polling does not help with a missing sending identity or a reputation block: a block stays until an operator lifts it after review, so contact support.

idempotency_key_reuse

HTTP 409. The Idempotency-Key header was reused with a request body that does not match the original request to the same method and path. Use a new key for a genuinely different request.

idempotency_key_in_progress

HTTP 409. The Idempotency-Key header matches a request that is still being processed. Retry shortly, or wait for the original response.

recipient_suppressed

HTTP 422. The recipient address is on the suppression list (from a prior bounce or complaint) and cannot be sent to.

recipient_not_allowed

HTTP 422. This workspace has not been reviewed yet. Until it is, it can reply to anyone who has written to one of its inboxes and contact a small number of new addresses a day (10 by default); today's allowance is used. Reply to inbound mail, or ask support to review the workspace.

recipient_undeliverable

HTTP 422. The recipient address cannot receive mail: it is not a valid email address (such as user@localhost), its domain has no mail route, or (for a workspace not yet reviewed) the mailbox does not exist, is disposable, or looks like random input. Nothing was sent. Check the address with the sender.

sending_paused

HTTP 503. Outbound Agent Mail is paused platform-wide by the operator, usually during a deliverability incident. It is also returned for an inbox on a shared Ackuna domain while that domain's Amazon SES account is still in the sandbox. Receiving, inboxes, keys and webhooks keep working. Retry later; no Retry-After is given because the pause has no fixed length.

rate_limited

HTTP 429. Too many requests. Three limits can answer it: the IP address's request limit, the API key's request limit, and, on POST /v1/inboxes/{inbox}/messages and POST /v1/messages/{message}/retry, the workspace's hourly send limit (for a reviewed workspace the plan's figure: 13 send requests an hour on Free, 250 on Builder, 1,250 on Scale and 6,250 on Startup; 100 an hour for a workspace not yet reviewed; counted across every key and shared with the console). The response carries Retry-After, the seconds until the limit that was hit resets, and, where available, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. Unlike quota_exceeded, it has no error.quota.

send_failed

Formerly HTTP 502, when the mail provider rejected or errored on a send.

No longer returned by POST /inboxes/{inbox}/messages. That endpoint accepts the message and answers 202; a provider refusal now surfaces as status: "failed" with error_message, and as an inbox.message.failed webhook. The code stays documented because existing integrations may still handle it.

internal_error

HTTP 500. An unhandled failure on the server. The failure is reported internally. Retry after a delay; if it persists, contact Ackuna with the request's approximate time.