Error codes

Reference for all API error codes and their meanings.

The AY Inbox API uses standard HTTP status codes and returns structured error responses in JSON format.

Error response format

All errors follow this structure:

{
  "error": "Error Type",
  "message": "Human-readable description of what went wrong.",
  "code": "machine_readable_code",
  "request_id": "req_or_uuid_for_support"
}

error and message are for people. Client logic should use code, HTTP status, and endpoint-specific fields such as retry_after. Include request_id when contacting support. A few codes also carry an optional reason, a finer key for when the same code has more than one next step; each is listed with its code, and a client should treat an unknown or missing reason as the plain code.

HTTP status codes

Send-admission diagnostics

These codes appear in campaign activity and task failure metadata, not as HTTP response codes:

CodeMeaning
AUTO_REPLY_HELDAn automatic reply was refused at reservation or worker start because the auto-reply gate no longer allows it: the instance or workspace switch is off, the campaign left automatic mode, a person wrote in the thread, the recipient changed or was suppressed, or a cap was reached. The failure message names the reason. The answer is handed to a person, never retried.
CAMPAIGN_DAILY_LIMIT_REACHEDThis campaign has reached its configured UTC-day maximum for this sending mailbox. Other mailboxes have separate campaign allowances.
MAILBOX_SENDING_PLAN_DENIEDThe mailbox's enabled local-day sending plan is unavailable, outside working hours or on a break, or its daily/hourly quota is exhausted. The scheduler retries when capacity opens.
MANUAL_SEND_LIMIT_REACHEDA unibox send (reply, forward or compose, including POST /unibox/* and POST /emails/:id/send) would exceed the mailbox's fair-use allowance: recipients on one email, in an hour, in 24 hours, in new conversations over 24 hours, or, across all the workspace's mailboxes, the 24-hour allowance its plan gives. The failure message names which. Unibox sends never count toward campaign or warmup limits. See sending controls.
RECIPIENT_SUPPRESSEDA recipient is on the workspace suppression list, directly or through its domain. The failure message names the address. A unibox forward is never refused for this.
SEND_ADMISSION_DENIEDAnother current send-authority, recipient or mailbox capacity check denied admission.

Admission denial before dispatch releases the send reservation without spending a failed-send attempt. A refused unibox send ends with status failed, is not retried, and is reported to the mailbox owner. See campaigns and sending behavior for the separate configurable ceilings and daily targets.

Client errors (4xx)

CodeErrorDescription
400Bad RequestInvalid request syntax or parameters, or a quota that would be passed (storage_limit_reached)
401UnauthorizedMissing or invalid authentication
402Payment RequiredOut of credits (insufficient_credits), or no free placement tests left this month (placement_quota_exceeded, placement_not_entitled)
403ForbiddenAuthenticated but lacks permission, or the workspace's mailbox allowance is full (mailbox_allowance_reached)
404Not FoundResource doesn't exist
409ConflictResource already exists
422UnprocessableValidation failed
429Too Many RequestsRate limit or AI usage cap exceeded (rate_limit_exceeded, usage_cap_exceeded, agent_signup_rate_limited, agent_claim_rate_limited), or too many placement tests or batches running at once (placement_too_many_running, placement_too_many_batches)

Server errors (5xx)

CodeErrorDescription
500Internal Server ErrorUnexpected server error
501Not ImplementedFeature not available
503Service UnavailableService temporarily down

Error details

400 Bad Request

Returned when the request cannot be processed due to invalid syntax.

Common causes:

  • Invalid JSON in request body
  • Missing required fields
  • Invalid field types
  • Values outside allowed ranges

Example:

{
  "error": "Bad Request",
  "message": "The request body is missing or has invalid fields: \"to\" is required; \"subject\" is required.",
  "code": "bad_request",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

When the body cannot be read, the message says why:

The bodymessage
is emptyThe request body is empty. ...
is not valid JSONThe request body is not valid JSON: <reason> (at byte N).
is the wrong JSON type at the top levelThe request body must be a JSON array, not a JSON object.
has a field of the wrong JSON typeField "to" must be a JSON array, not a JSON string.
has a time that is not RFC 3339The request body has a time that is not RFC 3339 (such as 2026-10-01T09:00:00Z): "tomorrow".
has a text field containing a NUL characterField "<name>" contains a NUL character, which is not supported.
is missing required fields or has values out of rangeThe request body is missing or has invalid fields: "to" is required; ... (up to five fields, then a count of the rest)

Fields are named by their JSON key, with nested fields as a dotted path (inner.name). The code is bad_request in every case, so branch on code and show or log message.

Versioned API routes check every query parameter name and value before the endpoint runs, including endpoints that read filters directly. The legacy /invitations/lookup alias applies the same validation. NUL characters, invalid UTF-8 and malformed query encoding return 400 rather than sending unsupported text to the database. For example, ?q=%00 returns Query parameter "q[0]" contains a NUL character, which is not supported. Valid Unicode text and properly encoded punctuation remain unchanged.

Every route takes each query parameter at most once. A parameter repeated in the query string returns 400 with code: "duplicate_query_parameter" and a request_id, whatever the values, and the message names it: The query parameter "status" appears more than once. Send it once per request. The parameters documented as lists accept repeats: status on GET /v1/advisor/recommendations, and campaign_ids and folder_ids on GET /v1/analytics/dashboard. Send any other list as one comma-separated value where the endpoint documents one.

A request carrying more than one Authorization, X-Ayinbox-Token, Idempotency-Key or Origin header line returns 400 with code: "duplicate_header", and the message names the header. Send each of them once. Repeated X-Forwarded-For lines are read as one list in arrival order, and for X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port and X-Real-IP the last line is used, the one the nearest proxy wrote. Forwarded headers are believed only from TRUSTED_PROXIES.

The public forms service answers its /api/forms/ routes with error and code holding the same slug, and a message to show:

codeStatusMeaning
duplicate_query_parameter, duplicate_header400A query parameter or the X-Ayinbox-Render header was sent more than once. The message names it
stale_page403The page was open long enough for its submission token to lapse. Reload it and submit again
not_found404The form does not exist or is not published, or no forms API route matches the path
unavailable503The forms service could not reach the backend. Try again in a few minutes
form_submit_failed400, 413, 502The submission was refused (the message names the field or rule), was too large, or could not be passed on. Fix and submit again, or retry a 502
{
  "error": "stale_page",
  "message": "This page has been open too long. Reload it and try again.",
  "code": "stale_page"
}

Resource IDs on email, campaign and contact routes must be UUIDs. Email tags and campaign email_tags and folders arrays also require UUID values. Malformed values return 400 with code: "bad_request" and a request_id before a database write. Valid UUIDs are normalized and repeated relation IDs are deduplicated; workspace ownership checks still apply.

How to fix:

  • Check that your JSON is valid
  • Verify all required fields are present
  • Ensure field values match expected types
  • Compare the body's shape with the endpoint's reference page: some endpoints take an array, most take an object

Specific 400 codes:

codeMeaning
duplicate_query_parameterA query parameter appeared more than once on a route that reads it as a single value
duplicate_headerAuthorization, X-Ayinbox-Token, Idempotency-Key or Origin was sent on more than one header line
invalid_lead_statusPOST /contacts/search or POST /contacts/export was given a lead_status that is not one of the documented values
invalid_sort_byPOST /contacts/search, POST /contacts/export or a bulk action's all selection was given a sort_by of the form custom:<key> whose key could never be a custom-field name (letters, numbers, underscores, spaces or dashes)
invalid_column, duplicate_column, too_many_columns, invalid_sortPUT /me/views/:view was given a column id that view cannot render, the same column twice, more than 64 columns, or a sort that names neither a sortable contact column nor a well-formed custom:<key> (unibox_rail has no columns or sort at all)
invalid_layoutPUT /me/views/:view was given a layout on a view that has none, or a unibox_rail layout with an unknown field, an empty or over-long key (200 bytes), more than 500 keys in a list or 32 sections, or a favorite name over 40 characters
invalid_mail_hostPOST /contacts/search, POST /contacts/export or a bulk action's all selection was given a mail_hosts entry that is not a documented provider value
invalid_lead_gradePOST /contacts/search, POST /contacts/export or a bulk action's all selection was given a lead_grades entry other than A, B, C, D or unscored
invalid_engagementPOST /contacts/search or POST /contacts/export was given an engagement that is not one of the documented values
lead_filter_requires_campaignlead_status or engagement was set without exactly one campaign_ids entry; both filters describe a contact inside one campaign
unknown_verification_statusA contact's verification_status is not a value any known verification service writes
unknown_verification_providerA contact's verification_provider names a vocabulary the platform cannot read
invalid_actionPOST /contacts/verification was given an action other than verify, mark_deliverable or mark_undeliverable
no_contactsPOST /contacts/verification selected no contacts: neither contacts nor a campaign_id with refused leads
list_bounce_riskPOST /campaigns/:id/start refused the launch on the list's projected bounce rate. Clean or verify the list, or repeat the request with acknowledge_list_risk: true
leads_undeliverablePOST /campaigns/:id/start found nothing to send because address verification refused every remaining lead; the campaign is parked at paused_undeliverable until they are re-verified or marked deliverable
empty_step_bodyPOST /campaigns/:id/start found an email step with nothing in either body, so it would send a blank message to every lead it reached. Write the step's body and start again
no_leadsPOST /campaigns/:id/start on a campaign that has never had a lead, with continuous off. Add contacts, or set continuous so it starts empty and waits for them. A campaign whose leads have all finished is a different case: it starts and waits
no_remaining_leadsA platform-initiated restart of a campaign with nothing left to send and continuous off found nothing to do; the campaign is completed again. A start you request never answers this: it turns continuous on and waits
too_many_tasksPATCH /crm/tasks or DELETE /crm/tasks was given more than 1000 ids in one request, or more than 50,000 exclusions. Split it into batches
too_many_contactsA contact bulk action was given more than 10,000 contact ids in one request, or more than 250,000 exclusions. Split it into batches, or send a filter selection instead of ids
selection_too_largeA "all": true bulk selection resolved to more than its limit: 250,000 contacts, or 50,000 CRM tasks. Narrow the filter and run it in parts; nothing was changed
invalid_filterA list filter could not be read. A task filter carried an id that is not one: assigned_to, contact_id and deal_id name records, and are matched against id columns. Sent by POST /crm/tasks/search, POST /crm/tasks/summary, and the filters of a "all": true bulk selection. GET /unibox sends it for a campaign_id or contact_id that is not a UUID, a since or until that is neither a YYYY-MM-DD date nor an RFC 3339 timestamp; the message names the parameter
invalid_settingPATCH /outreach/settings (or a campaign's advanced settings) carried a value outside the documented vocabulary, for example a reply_intent.crm_task_intents entry that is not a reply intent, or an inbox_tagging.questions entry with no question text, a label that is missing, repeated or built in, or a choice question with fewer than two options, or an inbox_tagging.languages entry that is not a supported language code
invalid_slugPATCH /organization/current was given a slug that is not 2 to 80 lowercase letters, numbers or dashes starting and ending with a letter or number
org_grounding_too_longPATCH /organization/current was given a product_description, icp_notes or voice_profile over 2,000 characters. The message names the field. A value already stored over the limit keeps working until it is next written
toolset_unknownGET /ai/tools?toolsets= named a toolset that does not exist. The message lists the names. On MCP the same refusal is JSON-RPC -32602
invalid_sync_folderPUT /emails/:id/sync was given a folder the sync always follows (INBOX, or a sent, drafts, spam, trash or archive folder by attribute or name), a name that is empty after trimming, longer than 255 characters or carrying a control character, more than 50 names, or a mailbox that is not IMAP. The message names the entry refused
invalid_tls_pinAn SMTP/IMAP connect or reconnect carried a tls_pin that is not a SHA-256 fingerprint (64 hexadecimal characters; colons, spaces and a sha256: prefix are ignored), or POST /emails/onboarding/smtp-imap/:id/certificates/confirm was missing the fingerprint of a server the mailbox trusts on its own certificate
mailbox_certificate_not_pinnedPOST /emails/onboarding/smtp-imap/:id/certificates/confirm on a mailbox whose servers are both verified by a certificate authority, so there is no certificate to confirm
no_organizationThe request needs a workspace and the caller has none selected. Every entitlement, limit and suppression rule is scoped to a workspace, so a write that would run unscoped is refused rather than run without those checks. API keys always carry their workspace; a dashboard session picks one at sign-in, so this normally means the session predates the workspace being chosen. Select a workspace and retry
lead_cc_limitSet a lead's CC was given more than 2 contacts
lead_cc_selfSet a lead's CC named the lead itself as a copy
invalid_recipientPOST /emails/:id/send, POST /unibox/reply or POST /unibox/compose was given a to, cc or bcc entry that is not exactly one email address ([email protected] or Ana <[email protected]>), or no to address at all. Put each address in its own entry
recipient_suppressedPOST /emails/:id/send, POST /unibox/reply or POST /unibox/compose named a to, cc or bcc address on the workspace suppression list, directly or through its domain. The message names the address and the entry. Nothing was queued. A forward (POST /unibox/reply with forward_message_id) is not checked against the list
invalid_message_idPOST /emails/:id/send or POST /unibox/reply was given an in_reply_to entry that is not one Message-ID: printable characters with no spaces or line breaks, with or without the surrounding < and >

Image upload refusals

Avatars, OAuth app logos, form logos, covers and backgrounds, and images placed in an email are checked by what the file contains, not by its name or the type the browser sends.

codeStatusMeaning
image_unreadable400The file is empty or does not decode as an image: it is damaged, or it is a different format than its name suggests
image_format_not_allowed400The file is an image, but not a format this upload takes
image_too_large400The file is over the size limit
image_dimensions_too_large400The image is wider or taller than the limit
image_too_small400An OAuth app logo is under 32px on a side
UploadFormatsSizeEach side
Avatar, OAuth app logoPNG or JPG2 MB1024px
Form logoPNG or JPG1 MB1024px
Form cover or backgroundPNG or JPG4 MB2560px
Email imagePNG, JPG, GIF or WebP5 MB4000px
{
  "error": "Bad Request",
  "message": "The avatar could not be read as an image. The file looks damaged, or it is a different format than its name suggests. Upload a PNG or JPG file up to 2 MB and 1024px on each side.",
  "code": "image_unreadable",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Password refusals

codeStatusMeaning
password_breached400The password appears in a public list of breached passwords and was refused. Choose one that does not
sso_link_expired400The pending token from a link_required sign-in is unknown, expired (ten minutes), already used, or has had three wrong passwords. Start the provider sign-in again
password_reused400POST /auth/login/initial-password was given the issued initial password itself. Choose a password of your own
password_change_expired400The pending token from a password_change_required sign-in is unknown, expired (ten minutes) or already used, or the issued password it was for has run out. Sign in again; an issued password past its 48 hours is refused like a wrong password, so ask whoever issued it for a new one
password_change_required403POST /auth/sso/link was given an issued initial password. Sign in with email and password first, which asks you to choose your own, then link the provider
reset_link_invalid401POST /auth/reset-password/session or /confirm was given a reset link or reset session that expired, was already used, or was issued before the password last changed. Request a new link
api_credentials_revoke_unsupported400POST /auth/reset-password/confirm or POST /auth/me/password asked to end connected apps and API keys (revoke_api_credentials) on a server that cannot. The password was not changed. Send it again without that option, then remove apps and keys from Settings
sessions_not_ended503The same two routes could not sign out every device on the account, so the password was not changed. From DELETE /auth/me/identities/:provider it means the provider was removed but not every device that signed in with it was signed out; repeating the request finishes it. The response carries Retry-After; retry after it
identity_not_found404DELETE /auth/me/identities/:provider named something other than google, apple or oidc
last_sign_in_method409DELETE /auth/me/identities/:provider or DELETE /auth/passkey/credentials/:id would leave the account with no password, no passkey and no other provider. Nothing was removed. Set a password (POST /auth/me/password/link) or add a passkey first
password_login_disabled403POST /auth/me/password/link was called on a server that turned password sign-in off (DISABLE_PASSWORD_LOGIN), so a password could never be used. No link was sent

A password must be 8 to 128 characters. There is no composition rule, but it is checked against the 100,000 most commonly breached passwords published by the UK National Cyber Security Centre, case-insensitively.

{
  "error": "Bad Request",
  "message": "This password appears in a public list of breached passwords. Choose one that does not.",
  "code": "password_breached",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

App Attest refusals

The native app proves itself with App Attest in place of the captcha on sign-in, registration and password reset.

codeStatusMeaning
app_attest_failed400The proof did not verify: the challenge expired or was already used, the proof came from another app or from a development build the server does not accept, the assertion's counter did not advance, the key id was already registered, or the object was malformed. Fetch a fresh challenge and make a new proof; if it keeps failing, update the app
app_attest_key_unknown400The key_id was never registered on this server. Register the key again with POST /auth/app-attest/keys, then retry the call
app_attest_unavailable404POST /auth/app-attest/challenge or /keys on an instance with App Attest off. When GET /auth/config reports a captcha without app_attest, email and password sign-in is unavailable in the app; Sign in with Apple, Google and passkeys still work

Each registered key may make 30 protected calls per hour. Past that the call answers 400 with code bad_request and the message Too many attempts, please try again later., the same answer as the other sign-in attempt budgets.

{
  "error": "Bad Request",
  "message": "We couldn’t verify this copy of the app. Update it from the App Store and try again.",
  "code": "app_attest_failed",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Cloud connection refusals

Not available in this build

The cloud link and the pool link are switched off in this build (config.CloudLinkEnabled and config.PoolLinkEnabled) and their routes are not registered, so these codes are not returned. They stay for reference; see AY Inbox Cloud.

codeStatusMeaning
cloud_link_workspace_required409Disconnecting a shared server connection requires explicit confirmation of the server-wide scope; it affects all local workspaces using that connection
pool_link_workspace_required409Cloud refused a new connection request without local workspace scope. Update the self-hosted instance before creating a new connection; existing connections can still enroll mailboxes and start placement tests
pool_link_workspace_connected409The Cloud workspace already has an active self-hosted connection, including a legacy link. Use another Cloud workspace and plan, or disconnect the existing link first
cloud_link_upgrade_required409This Cloud deployment does not support workspace-scoped connections. Upgrade Cloud first
organization_cloud_connected409Disconnect the workspace's Cloud connection before deleting it or scheduling deletion of it or its owner account
cloud_link_connected409The workspace is already connected to AY Inbox Cloud. Disconnect first to link a different Cloud workspace
cloud_link_not_connected409The workspace is not connected to AY Inbox Cloud, so there is nothing to change or disconnect
cloud_link_no_key409The instance has no CREDENTIALS_ENCRYPTION_KEY, so it cannot store the AY Inbox Cloud token. Set one and restart the backend before connecting
cloud_link_url400The Cloud URL does not start with https://
cloud_link_no_pending404No connection is in progress for this workspace. Start the connection again
cloud_link_code_expired404The connection code expired before it was approved in AY Inbox Cloud. Start again
cloud_link_mailbox_inactive422Only active mailboxes can be enrolled in AY Inbox Cloud warmup
cloud_link_oauth_mailbox422A Google or Microsoft mailbox connected with this instance's own OAuth app cannot be warmed by AY Inbox Cloud, because its refresh grant is bound to that app. The message says how to connect it instead
cloud_link_lifecycle400A pause or resume request named an action other than pause or resume
cloud_link_executor400An enrollment named an executor other than cloud or instance
cloud_link_self_execution_not_granted403Keeping a mailbox's credential on this server needs a permission AY Inbox grants to the linked Cloud workspace, and it has none or it expired. Ask AY Inbox support, or enroll with executor cloud
cloud_link_self_execution_unavailable409The connected AY Inbox Cloud cannot run warmup for a mailbox whose credential stays on this server. Upgrade Cloud, or enroll with executor cloud
cloud_link_self_execution_managed422The mailbox signs in through AY Inbox Cloud or is associated with its app-password copy there, so its credential already lives on Cloud and cannot stay on this server

Not available in this build

The cloud link and the pool link are switched off in this build (config.CloudLinkEnabled and config.PoolLinkEnabled) and their routes are not registered, so these codes are not returned. They stay for reference; see AY Inbox Cloud.

These come from AY Inbox Cloud's side of the connection (/v1/pool-link/*), which a self-hosted instance calls on your behalf. The instance passes the code and message through, so they can reach you from the instance's own settings and mailbox screens too.

codeStatusMeaning
pool_link_request400The connection request is missing the instance name
pool_link_credential400Enrolling a mailbox needs a credential that matches the mailbox's provider
pool_link_code_not_found404The connection code is unknown or has expired. Start the connection again from the instance
pool_link_code_used409The connection code has already been used
pool_link_denied403The connection was declined in AY Inbox Cloud
pool_link_revoked401The instance's link has been revoked. Reconnect from the instance's settings
pool_link_instance_not_found404The linked instance no longer exists in AY Inbox Cloud
pool_link_mailbox_not_found404The mailbox is not enrolled in AY Inbox Cloud
pool_link_mailbox_limit402The Cloud workspace has used its free allowance of linked mailboxes. Upgrade to the pool plan for unlimited mailboxes
pool_link_already_adopted409The mailbox is already linked to an instance
pool_link_not_adoptable422Only active Google and Microsoft mailboxes in the Cloud workspace can be linked to an instance
pool_link_not_managed403The mailbox's credential is held by the instance, not by AY Inbox Cloud, so Cloud cannot act on it
pool_link_mailbox_blocked403AY Inbox Cloud blocked the mailbox for hurting the pool. Sending from it is suspended until it is reviewed
pool_link_mailbox_inactive403The mailbox is not active in AY Inbox Cloud. Reconnect it to keep sending
pool_link_warmup_unavailable403Warmup is not available on the Cloud workspace
pool_link_oauth_pending409A mailbox sign-in started through AY Inbox Cloud has not completed yet. Poll again
pool_link_oauth_session404The mailbox sign-in session is unknown or has expired. Start the sign-in again
pool_link_executor400On AY Inbox Cloud's linked-instance API: an enrollment named an executor other than cloud or instance
pool_link_executor_credential400On AY Inbox Cloud's linked-instance API: an enrollment for a mailbox the instance runs carried a credential. Those mailboxes are enrolled without one
pool_link_executor_conflict409On AY Inbox Cloud's linked-instance API: a credential was sent for a mailbox whose credential stays on the instance, without executor cloud. Update the instance, or switch the mailbox to Cloud explicitly
pool_link_executor_managed422On AY Inbox Cloud's linked-instance API: a mailbox Cloud connected through its own sign-in cannot be run by the instance
pool_link_self_execution_not_granted403On AY Inbox Cloud's linked-instance API: the Cloud workspace has no active permission to keep mailbox credentials on the instance
pool_link_self_execution_unavailable409On AY Inbox Cloud's linked-instance API: this Cloud deployment cannot run warmup through the instance
pool_link_mailbox_not_self_executed409On AY Inbox Cloud's linked-instance API: Cloud runs this mailbox's warmup, so it takes no warmup reports for it from the instance
pool_link_command_not_found404On AY Inbox Cloud's linked-instance API: no command with that ID belongs to the calling instance
pool_link_command_not_leased409On AY Inbox Cloud's linked-instance API: a command was answered before it was leased
pool_link_command_answered409On AY Inbox Cloud's linked-instance API: the command already has a different answer, expired, or was withdrawn. Repeating the stored answer returns 204
pool_link_command_result_invalid422On AY Inbox Cloud's linked-instance API: the outcome does not fit the command kind, or a sent result has no valid message_id
pool_link_arrival_invalid400On AY Inbox Cloud's linked-instance API: a warmup report is missing its message handle, Message-ID, sender or received time
pool_link_arrival_pending503On AY Inbox Cloud's linked-instance API: Cloud has not recorded the send this report matches yet. Report it again shortly
pool_link_arrival_not_found404On AY Inbox Cloud's linked-instance API: an observation named a message Cloud never accepted from this mailbox
pool_link_observation_invalid400On AY Inbox Cloud's linked-instance API: the observation kind is not removed, moved_to_spam or owner_activity

CLI sign-in

ayinbox auth login signs the CLI in with a one-time code (POST /auth/cli/code, POST /auth/cli/poll, and the dashboard's approval page). See the CLI.

codeStatusMeaning
cli_auth_request400POST /auth/cli/poll was sent without a device_code
cli_auth_scopes400The CLI asked for scopes that include permission bits this instance does not grant
cli_auth_code_not_found404The code is unknown or has expired. Run ayinbox auth login again for a fresh one
cli_auth_code_used409The code has already been used
cli_auth_forbidden403Approving a CLI sign-in needs the manage_api_keys permission in the workspace, because the approval mints an API key

AY Inbox Cloud warmup refusals

Not available in this build

The cloud link and the pool link are switched off in this build (config.CloudLinkEnabled and config.PoolLinkEnabled) and their routes are not registered, so these codes are not returned. They stay for reference; see AY Inbox Cloud.

A mailbox warmed by AY Inbox Cloud has its warmup settings changed through the cloud (see warmup settings). PATCH /emails/:id and the warmup start, pause and resume routes answer with these when that cannot happen. In every case the rest of the patch is not applied on the instance. After cloud_warmup_not_applied, the mailbox's copy of its AY Inbox Cloud settings is still refreshed to what AY Inbox Cloud reported. A replay with the same Idempotency-Key returns the stored answer, so retry with a new key.

codeStatusMeaning
cloud_warmup_value_unsupported422AY Inbox Cloud does not take 0 from an instance for the starting volume, maximum, daily increase or reply rate. Use 1 or more, or change it in AY Inbox Cloud
cloud_warmup_not_applied409AY Inbox Cloud answered but did not apply the named settings. The mailbox now shows the values AY Inbox Cloud reported
cloud_warmup_managed409Participation, the shared daily and recipient limits, and stopping warmup are changed with the mailbox's AY Inbox Cloud controls, not on the mailbox
cloud_link_disconnecting409The workspace is disconnecting from AY Inbox Cloud. Retry once it has finished
cloud_link_unreachable503AY Inbox Cloud could not be reached
cloud_link_bad_response503AY Inbox Cloud answered without the mailbox's warmup settings
codeStatusMeaning
invalid_country_code400PUT /outreach/consent listed something other than an assigned ISO 3166-1 alpha-2 country code (DE, MA), or more than 300 of them. The message names the value. Withdrawn and reserved codes such as UK are refused; use GB

Name refusals

codeStatusMeaning
invalid_name400A first name, last name, workspace name, company name, OAuth app name or mailbox display name broke the naming rules. The message names the field and the rule

Names are shown to other people, including in invitation and notification emails, so they carry plain text only. The same rules apply to the dashboard, the API, the setup page and ayinboxctl:

  • a first or last name is at most 50 characters and contains a letter; a workspace or company name is at most 64 characters and contains a letter or a number; a mailbox display name keeps its own 2 to 100 character bound
  • no links, web addresses or email addresses: nothing with @, //, www. or a scheme such as https:, no hostname such as example.com, and no IP address. Full-width and ideographic dots count as dots
  • no control characters, invisible formatting characters (zero-width and bidirectional overrides), <, >, ` or \, and no more than three stacked combining marks
  • leading and trailing spaces are trimmed and runs of spaces become one, and that is the form stored
  • an OAuth app name cannot include AY Inbox, spaced out or spelled with look-alike characters included. An app that already had such a name keeps it until the name is changed
{
  "error": "Bad Request",
  "message": "First name cannot contain a link, web address or email address.",
  "code": "invalid_name",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

A name from Google, Apple or a single sign-on provider that breaks these rules is dropped rather than refusing the sign-in, and you are asked for it during onboarding.

Redirect destination refusals

codeStatusMeaning
redirect_target_not_own_domain400A form's redirect_url, a sending domain's redirect target_url, or a vendor forwarding url is not on one of the workspace's own domains. Bulk setup answers it on the refused domain's row

A redirect may only send visitors to a page on one of the workspace's own domains, or a subdomain of one:

  • a domain the workspace has a mailbox on, other than a shared email provider such as gmail.com
  • a verified tracking host of one of its mailboxes
  • the workspace's custom forms domain, once it is verified
  • a website the workspace confirmed by publishing its _ayinbox TXT record on the website's registrable domain. Saving looks for the record before refusing, and POST /emails/domains/:domain/redirect/website returns the record to publish. See confirm your website

Hosts are compared in lowercase, without a trailing dot and in their ASCII form, so bücher.de and xn--bcher-kva.de are the same domain. A subdomain matches its parent (shop.acme.com is on acme.com), but a name that only starts or ends the same way does not (acme.com.example.net and notacme.com are not on acme.com). An empty value still means no redirect.

{
  "error": "Bad Request",
  "message": "The redirect has to go to a page on one of this workspace's own domains: a domain it has a mailbox on (not a shared provider such as gmail.com), its verified tracking domain, its verified forms domain, a website it verified with its TXT record, or a subdomain of one of them. Verify the website under Sending domains > Redirect, or use an address on one of those.",
  "code": "redirect_target_not_own_domain",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

A destination saved earlier that no longer passes, because the workspace removed its last mailbox on that domain for example, is kept but not used: see the forms guide and root redirects.

Mailbox import refusals

The mailbox import routes (/emails/imports) answer these. A row that fails to connect is not an HTTP error: the import carries it, with its reason in the row's cause (listed in the guide).

codeStatusMeaning
mailbox_import_empty400No file and no pasted text, or an input with no rows, or a header row and nothing under it
mailbox_import_too_large400More than 5,000 mailboxes, a file over 10 MB, a pasted list over 2 MB, or a body that is not multipart form data. Split the file and import the rest separately
mailbox_import_no_email_column400POST /emails/imports with a mapping in which no column is email
mailbox_import_row_incomplete400PATCH /emails/imports/:id/rows/:line left the row without an SMTP and IMAP server or without a password. The message says which
invalid_cursor400GET /emails/imports or GET /emails/imports/:id/rows was given a cursor this API did not issue. Start again without one
mailbox_import_row_not_failed409Only a row that did not connect can be fixed; this one is queued, running, connected, updated, skipped, cancelled or waiting for sign-in
mailbox_import_credentials_expired409The row's credentials were deleted 7 days after the import finished, so it cannot be retried. Import it again
mailbox_import_nothing_to_retry409POST /emails/imports/:id/retry matched no failed row that still holds its credentials
{
  "error": "Conflict",
  "message": "This row's credentials were removed after the retry window. Import it again.",
  "code": "mailbox_import_credentials_expired",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Mailbox source refusals

The inbox vendor routes (/emails/vendors), the admin grant routes (/emails/grants) and the sending domain routes (/emails/domains) answer these. Inside an import, the vendor and grant failures land on the row instead, as its cause (see the guide).

codeStatusMeaning
mailbox_vendor_unauthorized400The vendor did not accept the API key. A saved connection is marked invalid until the key is updated
mailbox_vendor_no_workspace400The key is accepted but the vendor lists no workspace or organization for it, so there is nothing to read. Create one at the vendor, then connect again
mailbox_vendor_invalid_fields400A field the vendor requires is empty, or a field holds control characters. GET /emails/vendors/catalog lists each vendor's fields
mailbox_vendor_unknown400vendor is not one of the supported vendors
mailbox_vendor_rate_limited429The vendor is rate limiting AY Inbox's requests. Try again in a minute
mailbox_vendor_unavailable400The vendor answered with something unexpected, did not answer, or no longer has the mailbox. Try again shortly
mailbox_grant_not_configured400This instance has no Google service account or no Microsoft app for admin grants. An operator sets them up; see configuration. GET /emails/grants/config names the settings still unset in google_missing and microsoft_missing
google_delegation_unauthorized400Google refused the domain-wide delegation, or accepted it and refused the call: the client ID or a scope is missing from the Admin console entry, an OAuth client ID was entered instead of the service account's numeric one, or the admin address is not a super administrator. Admin console changes can take a few minutes to apply. See when Google refuses
microsoft_consent_missing400The Microsoft 365 organization has not granted admin consent, does not exist, or the consent lacks Mail.ReadWrite, Mail.Send or User.Read.All; or the consent sign-in did not complete or name the organization. A Global Administrator connects it again, and again after the app's permissions change. See when Microsoft refuses
mailbox_grant_state_invalid400The Google sign-in or Microsoft consent state expired (15 minutes), was already used, or was started by another member or workspace, or Google did not accept the sign-in. Start again
mailbox_grant_proof_missing400The workspace has not proved it controls the Google Workspace domain: the _ayinbox.<domain> TXT record does not hold this workspace's ayinbox-verify= value yet, or the Google sign-in was not the admin address entered on that domain. Also returned when the directory does not list the admin address as a super administrator, and for a Microsoft 365 consent signed in by someone without the Global Administrator or Privileged Role Administrator role. Publish the record (DNS can take a while to appear) or sign in as that administrator, then finish again
mailbox_grant_unavailable503Google, Microsoft or the DNS lookup for the proof did not answer. Nothing was recorded or changed, and a grant's status is left as it was. Try again in a minute
mailbox_grant_domain_mismatch400The admin address is not on the domain entered, or the mailbox is on a domain the grant does not cover
mailbox_grant_mailbox_unreachable400Google or Microsoft has no mailbox for the account: it does not exist, is suspended or disabled, or has no Exchange Online license
mailbox_grant_inactive400The grant failed its last check, so it connects nothing. Check the grant (POST /emails/grants/:id/check), then retry
mailbox_vendor_domain_not_found404PUT /emails/domains/:domain/vendor-forwarding or POST /emails/domains/:domain/vendor-tracking on a domain none of the workspace's active vendor accounts holds
mailbox_vendor_domain_unsupported400The vendor holding the domain cannot do this through its API: it has no forwarding removal, writes no DNS records for AY Inbox, or the existing record is one the vendor manages itself. See domains held by an inbox vendor
mailbox_reauth_delegated409POST /emails/onboarding/oauth/reauth/:id on a mailbox connected through an admin grant. It has no sign-in of its own to renew; check its grant instead, from Add account > Google or Microsoft > Whole domain or with POST /emails/grants/:id/check
sending_domain_not_in_workspace404The workspace has no mailbox on this domain. Also 400 when no domain was given
sending_domain_shared_provider400The domain belongs to a shared email provider, such as gmail.com, and cannot be redirected
domain_redirect_invalid_target400target_url (or a vendor forwarding url) is not an http or https address on a host, carries credentials, is longer than 2,048 characters, or points back at the same domain or its www. An import's redirects option answers the same. Also returned by vendor-tracking when host is not a subdomain of the domain
domain_redirect_taken409Another workspace on this instance already serves a verified redirect for this domain, or already has AY Inbox Cloud serve it. The redirect is saved but does not verify. On AY Inbox Cloud's linked-instance API, the linked workspace already has its own redirect for the domain
domain_redirect_linked409On AY Inbox Cloud: this domain's redirect is served for a linked self-hosted instance, and only that instance changes it
domain_redirect_cloud_unavailable409served_by is cloud, but this instance is not linked to AY Inbox Cloud, or Cloud does not serve redirects for it
domain_redirect_cloud_unreachable503AY Inbox Cloud could not be reached while saving, moving or removing a redirect it serves, so nothing changed. Retry
domain_redirect_limit409AY Inbox Cloud already serves 200 redirects for this linked instance
pool_link_redirect_not_found404On AY Inbox Cloud's linked-instance API: Cloud serves no redirect for this domain for the calling instance
pool_link_cleartext_mailbox422Enrolling a mailbox in AY Inbox Cloud, or updating its credential there: SMTP or IMAP uses security none. Cloud reaches the mail server over the internet and needs TLS or STARTTLS on both legs. Switch the mailbox to TLS, or keep warming it on the instance
pool_link_mailbox_conflict409Enrollment found the address already connected in the linked Cloud workspace and could not attach it. Automatic SMTP/IMAP association requires an active SMTP/IMAP Cloud mailbox, exactly matching credentials, no active Cloud campaign using it and no other instance enrollment. For an existing Google or Microsoft OAuth mailbox, or a delegated Google Workspace mailbox, an instance administrator uses Add account > In your AY Inbox Cloud workspace > Connect instead. This explicit adoption keeps Cloud history and can switch an existing local app-password mailbox in place. Nothing is attached by address alone. Contact support for other associations; do not delete or recreate the Cloud mailbox
pool_link_not_adoptable422The selected Cloud mailbox is not active in the linked workspace, is not Google or Microsoft, or uses a Microsoft whole-organization grant. Google Workspace delegated mailboxes and per-mailbox Google or Microsoft OAuth mailboxes can be adopted
pool_link_instance_url422Starting a Google or Microsoft sign-in through AY Inbox Cloud from an instance that registered no address of its own when it was linked, so Cloud has nowhere to return the sign-in to. Set APP_URL on the instance, disconnect it and link it again
pool_link_return_url400The same sign-in named a return address that is not on the instance's registered address (same scheme and host)
pool_link_oauth_browser400The brokered sign-in was not continued from the AY Inbox Cloud page that started it, or returned to a different browser. Nothing was connected. Start again from the instance
cloud_link_managed_protocol409Cloud does not advertise durable managed-consent protocol 1. Upgrade all Cloud control-plane replicas before opening a new managed connection or adoption. Existing mailboxes are unchanged
cloud_link_smtp_association_protocol409Cloud does not advertise smtp_association_protocol: 1. Update all Cloud replicas before connecting existing app-password copies. No local association intent or credentials have changed
cloud_link_smtp_association409The explicitly selected existing Cloud SMTP/IMAP mailbox could not be associated. Check the linked workspace and whether an active Cloud campaign or another instance uses that mailbox. Do not delete it or replace passwords to repair the association
pool_link_oauth_unknown409The provider exchange has no confirmed committed mailbox. Do not retry the authorization code or infer success; remove the pending connection before starting new explicit consent
cli_auth_scopes_role403Approving a CLI sign-in: your role in the chosen workspace allows none of the scopes the CLI asked for. Pick another workspace, or ask for narrower scopes with --scopes
sending_domain_bulk_invalid400A bulk setup named no domain, more than 100, neither a tracking_label nor a redirect_url, a tracking_label that is not one DNS label, or a served_by other than instance or cloud
forms_domain_taken409Another workspace on this instance has already verified this domain, as its forms domain, a tracking domain or a redirect. Returned when saving or verifying the workspace's custom forms domain. Nothing changes
lead_sync_target_not_found400Creating or editing a lead sync source named a target campaign or a category that does not exist in this workspace. Nothing is saved
tracking_domain_taken409Another workspace on this instance has already verified this tracking domain, on a mailbox, as a campaign's override, or as its forms domain. Returned when saving or verifying a mailbox's tracking domain, applying one to a sending domain (including bulk setup), and verifying a campaign's override. Nothing changes
tracking_host_not_configured400This instance has no tracking host (TRACKING_DOMAIN), so it cannot serve a tracking domain or a redirect
{
  "error": "Bad Request",
  "message": "This sign-in expired or belongs to someone else. Start again.",
  "code": "mailbox_grant_state_invalid",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

App password switch refusals

POST /emails/onboarding/app-password/:id moves a mailbox off per-mailbox Google sign-in onto an app password. Besides these, a password Gmail refuses answers with the same codes as an SMTP/IMAP connect, such as mailbox_auth_refused. Nothing is changed on any refusal.

codeStatusMeaning
app_password_invalid400app_password is not 16 letters once spaces are removed. Create one at myaccount.google.com/apppasswords and send it as Google shows it
mailbox_not_google_signin409The mailbox is not connected with per-mailbox Google sign-in: it is already on an app password or an admin grant, is an Outlook or SMTP/IMAP mailbox, or its sign-in is held by AY Inbox Cloud. A repeat of a switch that succeeded answers this

Placement test refusals

The inbox placement test routes (/placement/* and /campaigns/:id/placement-monitor) answer these. A refused test sends nothing: every check runs before the first copy is scheduled.

codeStatusMeaning
placement_not_entitled402The workspace has no active trial or subscription, so it cannot start a test. Only the hosted product answers this
placement_quota_exceeded402The workspace has used its free tests for the month on the instance panel, or the linked AY Inbox Cloud workspace has used its free tests on AY Inbox Cloud's panel. A tracking comparison needs two. On the instance panel of a paid workspace the message names the price in credits: send max_credits of at least that much to pay it. Monitors and trials cannot pay, and wait for the next month. Tests on the workspace's own seed inboxes are never counted
placement_too_many_running429The workspace already has 3 tests running, counting each half of a tracking comparison. Wait for one to finish
placement_sender_busy409The sending mailbox still has copies of another test waiting to be sent. One test sends from a mailbox at a time
placement_sender_unavailable409The sending mailbox is not connected and active, or it is itself a seed inbox, which receives tests and cannot send one
placement_daily_budget409The mailbox has too little of its daily limit left today for a useful test (at least five seeds per test, ten for a comparison), or for every seed chosen with seed_ids. A test that fits otherwise is sized down to what is left rather than refused. The message gives both numbers
placement_no_seeds409The panel has no seed inbox the test could use: none were added, none is connected, or every one is on the sender's own domain, which is always skipped
placement_panel_unavailable409panel is cloud on an instance that is not linked to AY Inbox Cloud
placement_invalid_seeds400seed_ids names a mailbox that is not a seed inbox of the workspace, or one that is not connected and running right now; the message names it
placement_invalid_tracking400tracking is on or compare for a campaign that sends plain text, which carries no tracking
placement_not_running409POST /placement/tests/:id/cancel on a test that has already finished or been cancelled
placement_seed_unavailable409The mailbox cannot be marked as a seed inbox: it is on the instance panel, or a placement test is still sending from it
placement_seed_limit409The workspace already has 50 seed inboxes. Unmark one first
mailbox_is_seed409Warmup was started or resumed on a seed inbox. A seed never warms up; unmark it first with PUT /placement/seeds/:email_account_id
placement_batch_empty400POST /placement/batches resolved to no sending mailbox: the scope, its filters and the sample left nothing
placement_batch_too_large400The batch would hold more mailboxes than the instance allows in one batch (10,000 by default, an operator setting). The message gives both numbers; narrow the selection or take a sample
placement_too_many_batches429The workspace already has 5 batches running. Wait for one to finish, or cancel one
placement_batch_not_running409POST /placement/batches/:id/cancel on a batch that has already finished or been cancelled

A batch runs every check that applies to the whole batch (the copy, the panel, the tracking, the allowance and the credits agreed) before it is created, and refuses with the codes above. The checks for each mailbox run when its turn comes: a mailbox refused then is deferred or skipped, and its sender row in GET /placement/batches/:id/senders carries the code as reason with its sentence as detail. Besides the single-test codes, a sender row can carry placement_sender_deleted (the mailbox was removed from the workspace), placement_batch_retry_expired (still unavailable when the seven-day retry window closed), placement_batch_start_failed (the test could not be started after several tries) and placement_batch_copy_unavailable (the campaign or step being tested was deleted, which skips every mailbox not started yet).

{
  "error": "Conflict",
  "message": "This test sends 40 emails from [email protected], which has 12 left of its daily limit today.",
  "code": "placement_daily_budget",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

A 400 without one of these codes is a malformed request: an unknown panel or tracking, a sequence_id without its campaign_id, a missing subject or body on an ad-hoc test, a monitor interval_days outside 1 to 30 or an alert_below outside 0 to 100, or on a batch pace quick, both or neither of sender_account_ids and sender_scope, an unknown sender_scope.type, sample.mode or on_unavailable, or sample.stratify on a sample that is not random or percent.

OAuth app and community directory refusals

POST /oauth/applications registers an app and PUT /oauth/applications/:id/listing publishes or edits its community directory listing. Nothing is saved on any refusal.

codeStatusMeaning
invalid_listing400A listing field breaks a rule: the slug is not 3 to 48 lowercase letters, numbers or single dashes, or is reserved for a built-in integration; the tagline is empty or over 120 characters; the description is over 2,000; either holds control, invisible or bidirectional-override characters; the category is unknown; or install_url, support_url or privacy_url is not an https address on a host without credentials. The message names the field
app_not_listable400The app cannot be listed: it is disabled or suspended, or it is a client created by dynamic registration, which has no publishing workspace
developer_access_blocked403The instance's operators have blocked this workspace or person from registering and publishing apps. GET /oauth/applications reports the reason in developer_access. Also returned by POST /oauth/applications
invalid_website400website on POST or PATCH /oauth/applications is not an http or https address on a host without credentials
invalid_logo400logo_url on POST or PATCH /oauth/applications is not an address this instance issued for the workspace. Upload the logo with POST /oauth/applications/{id}/logo instead
listing_slug_taken409Another app on this instance already uses the slug. Choose another
listing_hidden409The instance's operators hid this listing, so DELETE /oauth/applications/{id}/listing cannot remove it. The hide note is on the listing
app_suspended409The instance's operators suspended this app. It cannot be edited or given a new logo until it is unsuspended

Credential management refusals

codeStatusMeaning
oauth_token_not_allowed403An OAuth app token called a route that manages credentials: creating, editing, revoking or deleting API keys, or anything under /oauth/applications and /oauth/application-logo. Use the dashboard or an API key
api_key_permissions_exceed_caller403POST /api-keys or PATCH /api-keys/{id} would give a key more than its creator has: a permission the member's role does not cover, or, for an API key caller, a permission, mailbox or IP address the calling key does not have (a mailbox- or IP-limited key can only manage keys limited to a subset of its own). See what a role can delegate
api_key_holder_left401The member who created this API key is no longer in the workspace, so the key no longer works. Create a new key from a current member
api_key_mailbox_limited403An API key limited to some mailboxes called a surface that acts across the workspace: /ai/tools or /mcp, a campaign sender pool resolved from mailbox tags, starting a campaign whose pool is not an explicit list of the key's mailboxes, or a workspace-wide route: /organization/current and its limits and risk, /organization/members and /roles, /subscription and its credits, the /folders, /tags and /categories lists, /emails/domains and the Slack settings under /integrations/slack. See email account restrictions

The OAuth consent and token endpoints answer in the RFC 6749 shape (error, error_description) instead. access_denied from GET /oauth/authorize/details or POST /oauth/authorize means the approving member's role covers none of the requested scopes. invalid_grant on a refresh means the token is unknown, expired, revoked, was already used (which also revokes the grant), or its member no longer holds any of its scopes. 400 invalid_request from POST /oauth/token or POST /oauth/revoke includes a form that sends any parameter more than once (RFC 6749 section 3.2). A failure inside the server is 500 server_error with a fixed description and a request_id.

CRM provider refusals

Returned by the CRM endpoints while a workspace runs its CRM on HubSpot or Pipedrive. A change is written to the provider before it is saved in AY Inbox, so each of these means nothing was changed on either side.

codeStatusMeaning
crm_not_connected409GET /crm/metadata, POST /crm/sync, POST /crm/backfill, a /crm/contacts/:id or a /crm/lists call on a workspace that is not in HubSpot or Pipedrive mode. Connect the provider and choose it as the CRM with PUT /crm/settings first
crm_reauth_required409The provider revoked AY Inbox's access, the connection was removed, or the connection lacks a permission provider mode needs (in HubSpot deals, companies, owners, the contact schema, or the optional list permission for a list import; in Pipedrive deals, people, activities and users, or the connected user may not make the change). Reconnect and approve every permission
crm_managed_externally409A create, update or delete of a pipeline, stage or task type. They are managed in HubSpot or Pipedrive while it is the CRM; change them there and they update in AY Inbox within minutes
crm_stage_unknown400The pipeline_id or stage_id given is not a mirrored pipeline or stage of the provider, or (in HubSpot) a deal was marked won or lost in a pipeline with no closed stage of that kind. Also 409 from a copy into the provider when it has no deal pipeline to copy deals into
crm_owner_unmapped400A deal or task was assigned to a member who is not matched to a HubSpot owner or Pipedrive user. Match them with PUT /crm/owners/:externalId
crm_contact_missing404The contact is not in the provider, and creating contacts there is turned off in the workspace's CRM settings
crm_record_missing404The record no longer exists in the provider; it was deleted there
crm_provider_rejected422The provider refused the change. The message carries its reason, such as a required property HubSpot enforces or a Pipedrive plan without the feature
crm_sync_running429POST /crm/sync within 30 seconds (HubSpot) or a minute (Pipedrive) of the last one. The running sync finishes in a minute or two
crm_unavailable503The provider did not answer or failed, including a Pipedrive company out of its daily API budget. Nothing was changed; retry shortly
{
  "error": "Conflict",
  "message": "Pipelines are managed in HubSpot. Edit them there and they update here within minutes.",
  "code": "crm_managed_externally",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Salesforce sync refusals

The Salesforce endpoints answer with their own codes. When Salesforce itself refused, message carries its errorCode and text, such as Salesforce: INVALID_FIELD: No such column 'Tier__c' on entity 'Lead'.

codeStatusMeaning
invalid_salesforce_domain400The custom domain given to POST /integrations/oauth/start is not a Salesforce My Domain (*.my.salesforce.com)
invalid_salesforce_settings400PUT /integrations/salesforce/:id/settings named something the sync cannot run: an unknown field, a related field to write, an engagement field to read, a field mapped twice, or a fixed owner with no user
salesforce_error400Salesforce refused the request. The message is Salesforce's own
salesforce_unreachable400Salesforce did not answer. Retry later
salesforce_sync_off400POST /integrations/salesforce/:id/sync-now on a connection with sync turned off
salesforce_reconnect_required409The Salesforce session ended (a revoked app, a password reset, a deactivated user). Reconnect the integration
import_running409POST /integrations/salesforce/:id/import-sources/:sourceId/run or POST /integrations/apollo/:id/import-sources/:sourceId/run while that import is already running
salesforce_rate_limited429The Salesforce org has used its API requests for today

Apollo and Clay refusals

The Apollo and Clay endpoints answer with their own codes.

codeStatusMeaning
apollo_enrich_too_many400POST /integrations/apollo/:id/enrichments selected more than 5,000 contacts
clay_webhook_url400PATCH /integrations/clay/:id set a webhook_url that is not a Clay table webhook
clay_webhook_missing400PATCH /integrations/clay/:id chose events to send with no table webhook saved
apollo_key_rejected422Apollo refused the saved API key. Replace it with PUT /integrations/apollo/:id/key
looot_leads_disabled503The instance has not turned on Looot lead jobs
looot_invalid_domain400POST /integrations/looot/lead-jobs named something that is not a company domain
looot_no_domains400The lead job named no domains
looot_too_many_domains400The lead job named more than 100 domains
looot_too_many_titles400The lead job named more than 20 job titles
looot_title_too_long400A job title was 80 characters or longer
looot_per_domain400per_domain was outside 1 to 25
looot_budget400max_cost_usd was outside $0.05 to $50
looot_budget_too_small400The budget cannot cover one domain at the requested people per domain
looot_campaign_send_required403The lead job named a campaign that is sending, and the caller does not hold SEND_CAMPAIGNS (send_campaigns for a member). See looot_campaign_send_required
apollo_key_forbidden422The API key works but cannot reach an endpoint the integration uses. Use a master key, or one with lists, contact search and people enrichment
apollo_key_missing422The Apollo connection has no API key
apollo_rate_limited429Apollo's rate limit for the account was reached. Retry later

Lead scoring refusals

The lead scoring endpoints answer with their own codes.

codeStatusMeaning
lead_scoring_invalid_profile400PUT /lead-scoring/profile was missing enabled or criteria, or a criterion is out of range. The message names the field, such as grades: must satisfy A >= B >= C or employees.ideal: must sit inside employees.acceptable
lead_scoring_budget400POST /lead-scoring/enrichment-runs set a max_cost_usd outside $0.05 to $50
lead_scoring_not_enabled409An enrichment run was asked for while lead scoring is off or the workspace has not saved a profile
lead_scoring_run_active409The workspace already has an enrichment run queued or running
lead_scoring_enrichment_disabled503The instance has not turned on company enrichment with Looot (AYINBOX_LOOOT_SCORING)

Campaign brain refusals

The campaign brain routes and the drafts that read a brain answer with their own codes.

codeStatusMeaning
brain_invalid_fields400A field, sample, kind, visibility or a page's title, body, type or path is over its limit or malformed. The message names the field, such as pricing: at most 800 characters or booking_url: a link must be a full https address
brain_material_type400The file is not .txt, .md, .csv, .html, .htm or .docx, or its content does not match its extension. A PDF answers this code with the sentence "PDF upload is not available yet. Paste the text, or save it as .docx."
brain_material_too_large400The file is over 10,000,000 bytes. An upload sent with an Idempotency-Key is read by the idempotency layer first, and a body too large for it answers that layer's generic error instead
brain_url_refused400The address failed the outbound check (it must be a public https page), or this instance turned URL material off (AYINBOX_BRAIN_URL_FETCH=false)
brain_internal_restricted403The source or page is internal, and reading its text needs manage_campaigns (WRITE_CAMPAIGNS for a key)
brain_limit409The workspace already holds 50 brains
brain_material_limit409The brain already holds 30 sources or 500,000 characters of material
brain_material_duplicate409The same text is already a source in this brain with the other visibility (internal or OK to quote). Change that source's visibility instead of adding it again. A refresh of a web page source answers it too when the page now reads the same as another source in the brain, and the source keeps its old text
draft_flagged409An API key or OAuth caller approved an agent draft that carries flags without "acknowledge_flags": true. The body lists the flags the caller may read: without WRITE_CAMPAIGNS (and the member's manage campaigns permission) a figure from the internal rules shows as unverified_figure and a quote of them is left out, and a draft with no other flag is not held. Read them, then approve again with the acknowledgement and a new Idempotency-Key (or none), since the same key with a different body is refused. The draft is left pending
brain_material_unreadable422No text could be read from the file, or the page has almost no readable text (it may need a browser to render). Paste the text instead
brain_url_unreachable422The page could not be fetched: it answered an error, timed out, or redirected somewhere refused
brain_url_rate_limited429The workspace fetched 60 pages in the last hour. Retry later
brain_disabled503The campaign brain is turned off on this instance (AYINBOX_BRAIN=false)
brain_extract_busy503Every file reader on this instance is in use. Retry in a moment
brain_unavailable503The brain could not be read, so no draft was written and nothing was charged. Retry
brain_ai_unavailable503POST /ai/brains/:id/sample-reply or POST /ai/brains/:id/organize was called on an instance with no AI provider
brain_page_path_taken409Another page in this brain already has that path
brain_page_conflict409The page was saved by someone else, or by the AI, since you read it. Read it again and send the new revision
brain_page_limit409The brain already holds 300 pages or 400,000 characters of pages
brain_organize_empty400The brain has no offer, audience, pricing or material to organize into pages
brain_organize_unreadable422The AI's answer could not be read as pages, so nothing changed and the credits were refunded. Retry
brain_pages_unavailable503This instance does not store brain pages

Auto-reply refusals

The auto-reply routes and their agent tools answer with these codes.

codeStatusMeaning
auto_reply_invalid_settings400A setting is malformed: an unknown intent, max_per_thread outside 1 to 5, a link that is not a full https:// address or points at an IP, a figure with no digit or over 40 characters, more than 20 links or figures, notes over 2,000 characters, or a disclosure line over 300 characters or holding an unapproved link, address or phone number. The message names the problem
auto_reply_requires_plan403The workspace's plan does not include the inbox agent, which auto-reply needs
auto_reply_send_required403Automatic mode was asked for through PUT /campaigns/:id/auto-reply, or a setting other than the mode was changed on a campaign that is on automatic. Both go through POST /campaigns/:id/auto-reply/enable, which needs SEND_CAMPAIGNS
auto_reply_run_not_found404No run with that id in this workspace, or it is in a mailbox outside the caller's grant
auto_reply_unavailable409This instance cannot run auto-reply. reason is the first of instance_disabled (AUTO_REPLY_ENABLED is off), inbox_tagging_off, no_ai_provider or plan, and the message lists them all
auto_reply_needs_stop_on_reply409The campaign does not stop on reply. Turn it on first
auto_reply_has_reply_flow409A reply branch of the campaign leads to an email step
auto_reply_needs_grounding409The workspace has no product description and the campaign's brain has no public facts to answer from
auto_reply_run_already_sending409The run's send has been reserved or started, so it can no longer be cancelled
auto_reply_run_finished409The run already finished. Cancelling a cancelled run succeeds instead

Calendar booking refusals

The calendar booking routes answer with these codes. A provider's own response never reaches the caller; it stays in the server log against the request id.

CodeStatusMeaning
booking_disabled503AYINBOX_BOOKING is off on this instance
booking_auto_send_required403mode: "auto" was sent to PUT /booking/settings. Automatic booking goes through POST /booking/auto, which needs SEND_CAMPAIGNS
booking_auto_unavailable409Automatic booking cannot be turned on. details.reasons lists what is missing (ai_provider, plan, booking_type, typed_tagging, auto_reply, automatic_booking_unavailable). Also returned by PUT /booking/settings when draft mode is missing something
booking_provider_not_connected409The connection has no booking key, is gone, or no usable booking type is set up
booking_provider_reauth_required409The provider refused the key, or it lacks a permission (details.missing)
booking_provider_plan_required403The provider's plan has no booking API
booking_provider_unavailable503The provider is down or rate limited; retry_after and Retry-After carry the wait when known
booking_provider_refused409The provider refused a booking for another reason; a person decides, and nothing is re-posted
booking_event_type_unsupported400The event type requires confirmation, email verification, a required question, a phone number, seats or an invitee location (blockers), or it is gone
booking_invalid_timezone400timezone is not a usable IANA zone
booking_slot_not_available400A slot_id not on the offer, or a start_at or slot_starts time not open on the calendar
booking_slot_taken409The provider refused the slot and it is gone; alternatives lists the times written into a new draft
booking_offer_closed409The offer is booked or stopped, or not in a state the call accepts
booking_offer_open409Another open offer holds the thread or the contact
booking_new_reply409A reply on the offer has not been read yet; message_id names it
booking_attempt_in_flight409A booking on this offer or type is being confirmed; also the answer to deleting such a type
booking_already_booked409The contact already has an upcoming meeting
booking_contact_mismatch400The thread's counterpart is not a contact in this workspace
recipient_suppressed400The attendee is suppressed or unsubscribed
api_key_mailbox_limited403A key limited to some mailboxes called a workspace-wide booking write (settings, automatic mode, booking types, the credential)

Company signal refusals

The company signal endpoints answer with their own codes.

codeStatusMeaning
signals_disabled503The instance has not turned on company signals (AYINBOX_LOOOT_SIGNALS)
signals_invalid_domain400POST /signals/companies named something that is not a company domain, or a free-mail host such as gmail.com
signals_no_companies400The add request named no domains, contacts or segment, or the check found no watched company to run
signals_too_many_companies400The add request named more than 500 companies or 1,000 contacts, or the check named more than 25 companies
signals_limit400limit on POST /signals/companies was outside 1 to 100
signals_watch_list_full409The workspace already watches 500 companies
signals_budget400max_cost_usd was outside $0.05 to $50, or daily_budget_usd outside $0 to $50
signals_budget_too_small400The check's budget cannot cover one company with the enabled lookups
signals_daily_budget_reached409Today's (UTC) signal budget cannot cover another lookup
signals_no_lookups400Every lookup is turned off in the signal settings
signals_invalid_lookup400lookups named something other than hiring, funding or news
signals_invalid_filter400GET /signals or /signals/domains got a types, domain or days value it does not accept

First-line job refusals

POST /contacts/first-lines starts a first-line job and answers these codes, as does the list endpoint for its limit. A job that started and then stopped (out of credits, a spend limit, an empty Looot balance) is not a refusal: it ends with status: "error" and says why in last_error.

codeStatusMeaning
first_lines_disabled503The instance has not turned on first-line jobs (AYINBOX_LOOOT_FIRST_LINES), or has no Looot credential or AI provider. Every first-line endpoint answers it
first_lines_selection400The request named a segment_id and a contact selection. Send one of them
first_lines_no_contacts400The selection named no contacts, or none of them exist anymore
first_lines_too_many_contacts400The selection resolved to more than 5,000 contacts
first_lines_budget400max_cost_usd was outside $0.05 to $100
first_lines_budget_too_small400The budget cannot cover one contact at the worst case: a company's lookups plus the model call's estimate
first_lines_limit400GET /contacts/first-lines was given a limit outside 1 to 100

Automation refusals

codeStatusMeaning
action_provider_mismatch400POST /automations, PATCH /automations/:id or POST /integrations/connections/:id/events paired an action with a connection whose provider does not run it, such as a Slack message on a HubSpot connection. Each action runs only on the integration it belongs to; the provider's actions are listed under capability.actions in GET /integrations/catalog

mailbox_oauth_return_origin

A 400 with code: "mailbox_oauth_return_origin" means a Google or Microsoft mailbox connect or reauthorization came from a dashboard origin the backend does not allow. Add its exact HTTP(S) origin to CORS_ALLOW_ORIGINS, as in shared-backend dashboard configuration. A wildcard is not an OAuth return allowlist.

401 Unauthorized

Returned when authentication fails.

Common causes:

  • Missing Authorization header
  • Invalid API key format
  • Expired API key
  • Revoked API key

Example:

{
  "error": "Unauthorized",
  "message": "Token not found.",
  "code": "unauthorized",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Include the Authorization: Bearer aybox_... header
  • Verify your API key is correct
  • Check if your key has expired or been revoked
  • Generate a new key if necessary

session_ended means the dashboard session behind the token was signed out, revoked, or ended by a password change. Refreshing will not help; sign in again.

{
  "error": "Unauthorized",
  "message": "Your session has ended. Sign in again to continue.",
  "code": "session_ended",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Two more 401 variants are not about API keys at all. setup_token_invalid is returned by the first-run claim endpoint when the setup link is invalid, already used or expired. Print a new one with ayinboxctl setup-link, described in first run.

sso_wrong_browser is returned by POST /auth/sso/exchange when the handoff code is collected without the binding secret that POST /auth/<provider>/begin handed the browser that started the sign-in. The handoff is deliberately non-transferable: a sign-in link that was forwarded, or opened in another browser, cannot sign the recipient in. Start the sign-in again in the browser you want to use.

402 Payment Required

Returned when an AI action, or a placement test paid in credits, is requested but the organization does not have enough credits. The response carries the stable code insufficient_credits.

{
  "error": "Payment Required",
  "message": "You're out of AI credits. Add more to keep using the assistant.",
  "code": "insufficient_credits",
  "request_id": "req_..."
}

How to fix:

  • Wait for the monthly allowance to reset, or buy a top-up pack (see AI credits)
  • Related: a 429 with code usage_cap_exceeded means a short-term AI usage cap was hit; retry later

Placement tests answer 402 too, with placement_not_entitled or placement_quota_exceeded; see placement test refusals. A paid placement test past the credit balance answers insufficient_credits, and one past a spend limit answers 429 with usage_cap_exceeded; neither charges anything.

403 Forbidden

Returned when authenticated but lacking necessary permissions.

Common causes:

  • API key lacks required permission
  • Request IP not in allowlist
  • Email account not in allowlist
  • Organization access restricted
  • The member is restricted to selected resources (member_access_restricted)

Example:

{
  "error": "Forbidden",
  "message": "You don't have access to this feature.",
  "code": "forbidden",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Check your API key's permissions
  • Verify IP restrictions if configured
  • Request additional permissions if needed

mailbox_gmail_oauth_disabled

A 403 whose code is mailbox_gmail_oauth_disabled comes from POST /emails/onboarding/oauth/start with provider: "gmail". The backend has no complete Google mailbox client credentials, or explicitly disables new Google OAuth connects through BOX_GOOGLE_OAUTH_CONNECT; GET /auth/config announces it as gmail_oauth_connect: false. Nothing about the caller's permissions is wrong. Re-authorizing an existing Gmail mailbox (POST /emails/onboarding/oauth/reauth/:id) is never refused this way.

{
  "error": "Forbidden",
  "message": "New Gmail mailboxes connect with an app password over IMAP and SMTP on this deployment, not with Google sign-in.",
  "code": "mailbox_gmail_oauth_disabled",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Connect the mailbox through POST /emails/onboarding/smtp-imap with smtp.gmail.com:465 and imap.gmail.com:993, both TLS, and a Google app password. See Gmail and Google Workspace
  • An instance with its own Google app must set both BOX_GOOGLE_CLIENT_ID and BOX_GOOGLE_CLIENT_SECRET. Leave BOX_GOOGLE_OAUTH_CONNECT unset or set it to true, then enable the dashboard visibility flag on the deployments that should offer Google sign-in

looot_campaign_send_required

A 403 whose code is looot_campaign_send_required comes from POST /integrations/looot/lead-jobs, and the find_leads tool returns the same refusal as a tool error. The job named a campaign_id whose campaign mails the contacts it gets without anyone starting it: one that is active, one that finished (it restarts for new leads), or one parked because its leads were undeliverable (it resumes once they verify). Adding found leads there is sending, so it takes the right to send campaigns as well as the lead job's own: SEND_CAMPAIGNS for an API key or app, and send_campaigns for a member, including the member behind an app or the member who created the API key. A self-registered MCP client is never granted SEND_CAMPAIGNS. Nothing is started or charged. A retry with the same Idempotency-Key as a request that did start a job returns that job, even if its campaign has started sending since.

{
  "error": "Forbidden",
  "message": "This campaign is sending, so adding leads to it starts mailing them, which needs the right to send campaigns. Choose a draft or paused campaign, or leave the campaign out.",
  "code": "looot_campaign_send_required",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Pick a draft or paused campaign, then start it with POST /campaigns/:id/start once the leads are in, or leave campaign_id out and add the contacts later
  • Grant the key SEND_CAMPAIGNS, or ask a member with Manage team to give the member Send campaigns. See sending campaigns

plan_offer_required

A 403 whose code is plan_offer_required comes from POST /subscription/checkout, POST /subscription/change-plan or POST /subscription/offers/:planId/checkout. Those routes sell public plans, and a private plan only to a workspace AY Inbox has offered it to. The workspace holds no open offer for the chosen plan: it was never offered, or the offer has ended or been withdrawn. The Warmup plan is sold through POST /pool-link/checkout and is refused here too.

{
  "error": "Forbidden",
  "message": "This plan is available by invitation only. Ask AY Inbox to offer it to this workspace.",
  "code": "plan_offer_required",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

Registration and invitation refusals

Signup and invitation refusals carry their own code, so a client can branch on the specific condition instead of matching on text. They describe the deployment's policy and never say anything about whether a given address exists.

codeStatusMeaning
review_off_needs_send403PATCH /campaigns/:id or the update_campaign tool set review_first_emails to false on a campaign that reviews first emails. That releases every waiting first email, so it takes SEND_CAMPAIGNS (for a member, Send campaigns)
registration_invite_only403The server runs DISABLE_REGISTRATION=invite_only. Creating an account requires an invitation link, which carries the token that permits the signup
registration_closed403The server runs DISABLE_REGISTRATION=true. Signups are off and invitations do not override it
invitation_invalid403The invitation is expired, cancelled, already used, or was issued for a different email address
invitation_expired400POST /invitations/accept on an invitation older than 48 hours. It stays on the team page, where it can be sent again
invitation_used_or_expired400POST /invitations/accept on an invitation that was accepted or expired while the request was in flight. Ask for it to be sent again
invitation_roles_removed400POST /invitations/accept on an invitation whose roles were all deleted since it was sent. Ask for a new invitation
setup_already_complete403The first-run claim was attempted on an instance that already has an account
{
  "error": "Forbidden",
  "message": "This server is invite only. Ask an administrator to invite you, then open the link in the invitation to create your account.",
  "code": "registration_invite_only",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix: on a self-hosted deployment these are configuration, not faults. See accounts and access.

Account and workspace standing

An account or workspace that support or the instance's administrators have restricted answers with its own code. These are not permission problems, and a role change does not lift them.

codeStatusMeaning
account_suspended403The account is suspended and cannot sign in, by password, passkey or two-factor confirmation
account_sending_suspended403The account may not send mail. A manual send (POST /emails/:id/send, POST /unibox/*) is refused; nothing else is affected
account_workspace_create_suspended403The account may not create workspaces. It can still join one it is invited to
workspace_sending_suspended403Sending is paused for the whole workspace while it is under review. Contact support to resolve the review
{
  "error": "Forbidden",
  "message": "Sending is paused for this workspace while it is under review. Contact support to resolve the review; nothing else in the workspace is affected.",
  "code": "workspace_sending_suspended",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Confirmation required

codeStatusMeaning
reauth_required403The action needs a proof of identity newer than the session. Confirm with POST /v1/auth/reauth, then retry
reauth_limited400Too many failed confirmations for this account within the hour. Wait, or sign out and sign in again: a sign-in counts as a confirmation for five minutes
reauth_no_factor400POST /auth/reauth was sent a password or code for an account that has neither. Confirm with a passkey or the linked sign-in provider listed by GET /auth/reauth/methods; an account with none of those signs out and in again (a sign-in counts as a confirmation for five minutes)
reauth_identity_mismatch400The Google, Apple or single sign-on account verified during a confirmation is not the one linked to this AY Inbox account. Continue with the account you sign in with
reauth_expired400The confirmation's code or nonce expired or was already used. Start the confirmation again
admin_mfa_required403An admin route was reached by a session that did not present a second factor. Turn on 2FA or add a passkey, then sign in again
passkey_user_verification_required400A passkey sign-in came from an authenticator that did not verify the user with a PIN or biometric. Set a PIN on the security key, or sign in another way
two_fa_invalid_code400The authenticator or recovery code did not match. Wait for a fresh code and try again; the login challenge allows five attempts per pending session
password_changed_sign_in_again409POST /auth/me/password stored the new password but could not issue the calling device a new session. Every earlier token is invalid; discard them and sign in again with the new password

reauth_required guards the changes that hand out a durable credential, reveal a signing secret, change who can get in, move money, cannot be undone, or reach a whole domain's mail. The full list is under actions that need a recent confirmation. List the ways the account can confirm with GET /v1/auth/reauth/methods (a passkey and a linked Google, Apple or single sign-on identity are among them; see endpoints). With a password or a current two-factor code:

curl -X POST "https://api.ayinbox.com/v1/auth/reauth" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"password": "..."}'
{ "valid_for_seconds": 300 }

The confirmation is recorded on the session and lasts for the window returned, so a run of related changes only asks once.

Turning on two-factor authentication (POST /auth/2fa/enroll/start and /confirm) is guarded too, since a confirmed authenticator is itself a way to confirm. An account with a password confirms as above. An account with neither a password nor 2FA (created through Google, Apple or single sign-on) confirms with its passkey or linked identity. One with none of those is allowed for five minutes after a sign-in like every confirmed action; an older session gets reauth_no_factor and the fix is to sign out and sign in again. API keys and OAuth tokens have no session to confirm, so these routes are reachable only with a signed-in session; that is deliberate for key creation, which otherwise lets one leaked key mint more.

{
  "error": "Forbidden",
  "message": "Confirm it is you before making this change.",
  "code": "reauth_required",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Member access scope

A member restricted to selected resources, and any API key or OAuth app acting for one, reaches only the read routes that filter to their grants. The scope's own refusals:

codeStatusMeaning
member_access_restricted403The caller's selected-resource scope does not cover this route. Resource writes and workspace-wide surfaces such as contacts and settings are refused. Workspace creation is refused to accounts with only restricted memberships, except instance administrators; limit-request operations require an unrestricted membership in the target workspace
invalid_access_resource400PUT /organization/members/:id/access or an invitation named a campaign folder, campaign or mailbox that is not in this workspace
invalid_access_scope400The scope is neither workspace nor restricted
too_many_access_grants400More than 500 folders, campaigns or mailboxes in one list
owner_access_unrestricted403The workspace owner always reaches the entire workspace and cannot be restricted

A campaign or mailbox outside a restricted caller's grants answers 404, the same as one that does not exist, so a scope does not reveal what else the workspace holds. GET /unibox/thread/labels likewise returns the same 404 for an unknown thread, a thread outside the selected workspace, or a thread with no message in an accessible mailbox. An accessible thread with no labels returns an empty list.

Failure to resolve a restricted scope or its scoped counts fails the request rather than returning workspace-wide data. Realtime authorization lookup errors stop campaign, mailbox and organization channels; user-channel events whose authorization cannot be verified are withheld. See bounded realtime authorization.

{
  "error": "Forbidden",
  "message": "Your access to this workspace is limited to selected campaigns and mailboxes, and this is outside it.",
  "code": "member_access_restricted",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix: ask a member with Manage team to widen the access under Settings > Members.

A 403 whose code is slack_link_email_mismatch comes from POST /integrations/slack/link sent without a Sign in with Slack result. Confirming a Slack link that way needs the email on the Slack account's profile to be the signed-in user's AY Inbox email, compared without regard to case. The code stays usable: confirm it again with slack_code and state from POST /integrations/slack/link/verify. GET /integrations/slack/link/:code reports the check ahead of time as email_matches, and whether Sign in with Slack is available as verify_available.

{
  "error": "Forbidden",
  "message": "This Slack account's email is not the one you sign in to AY Inbox with. Continue with Slack to confirm the account is yours.",
  "code": "slack_link_email_mismatch",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Sign in to AY Inbox with the address on your Slack profile, then confirm again
  • If the addresses already match, a member with manage_settings reconnects Slack under Integrations > Slack so the connection grants users:read and users:read.email

404 Not Found

Returned when the requested resource doesn't exist.

Common causes:

  • Invalid resource ID
  • Resource was deleted
  • Resource belongs to different organization
  • Typo in endpoint URL

Example:

{
  "error": "Not Found",
  "message": "Resource not found.",
  "code": "not_found",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Verify the resource ID is correct
  • Check that the resource hasn't been deleted
  • Ensure you're using the correct endpoint

Specific 404 codes:

codeMeaning
unknown_view/me/views/:view was given a view name other than contacts, campaign_leads or unibox_rail
app_attest_unavailablePOST /auth/app-attest/challenge or /keys on an instance with App Attest off. See App Attest refusals
lead_cc_contact_not_foundSet a lead's CC named a contact that is not in the workspace
review_not_foundA review queue call named a message that is not in this campaign's queue
slack_not_connectedA Slack route was called for a workspace that has not connected Slack, or whose connection was removed. Connect it under Integrations > Slack
slack_link_invalidThe Slack link code is unknown, expired or already used. Mention or message the bot in Slack for a new link button
slack_verify_failedPOST /integrations/slack/link with a Sign in with Slack result that is unknown, expired, already used, started by someone else or for another code. Start again from POST /integrations/slack/link/verify
slack_verify_wrong_accountPOST /integrations/slack/link after Sign in with Slack signed in a different Slack account than the one the link code names
slack_verify_unavailablePOST /integrations/slack/link/verify on an instance whose Slack app has no client credentials
slack_not_linkedPATCH /integrations/slack/link from a member who has not linked a Slack account in this workspace

409 Conflict

Returned when the request conflicts with existing data.

Common causes:

  • Trying to create a resource that already exists
  • Duplicate unique values
  • Deleting something whose state does not allow it yet, such as an API key that can still authenticate

Example:

{
  "error": "Conflict",
  "message": "resource already exists",
  "code": "conflict",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Creating an account for an address that already has one answers 409 with the generic conflict code and a message pointing to sign-in and password reset. With email verification on it comes from POST /auth/register/confirm, after the emailed code has shown the address is the caller's; without it, POST /auth/register answers it directly.

A few conflicts carry their own code. The mailbox import's are under mailbox import refusals. A contact's email address has to be free, so changing one to an address another contact already holds is refused rather than merging the two:

codeStatusMeaning
contact_email_taken409The address given to update a contact already belongs to another contact
lead_cc_lead_is_copied409Set a lead's CC on a lead that is itself copied on another lead in the campaign, so it sends nothing of its own to copy anyone on
lead_cc_has_copies409Set a lead's CC names a contact that has copies of their own in the campaign
mailbox_is_seed409Warmup was started or resumed on a placement seed inbox. See placement test refusals
mailbox_not_google_signin409POST /emails/onboarding/app-password/:id on a mailbox that is not connected with per-mailbox Google sign-in. See app password switch refusals
scheduled_send_not_pending409Cancel a scheduled send named a send that is no longer pending: it has started sending, was sent, failed, or was already cancelled. Nothing changed
approval_not_pending409POST /ai/sessions/:id/approve named a tool call the conversation is not waiting on: it was already decided, in another tab or in Slack, or a newer message replaced it
review_text_changed409A review queue approve or edit sent a text_hash that no longer matches the message: it was rendered again or edited after the caller read it. List the queue and review the new version
review_not_pending409The review queue message was already approved or rejected. In a bulk-approve answer this is a per-item refusal, not an error
review_rendering409The review queue message is being rendered again (its mailbox can no longer send it, or the step changed). It returns to the queue with a new text_hash within a campaign pass
idempotent_secret_withheld409A retry, with the same Idempotency-Key, of a call that already ran and whose response held a secret shown once (creating an API key, a webhook endpoint, an OAuth app or an integration connection, or rotating one of their secrets). Its response is never stored, so it cannot be replayed. List the resource, and rotate the secret if you lost it
idempotency_outcome_unknown409The request that first used this Idempotency-Key (or agent tool idempotency_key) ended in a server error or never finished, so whether it took effect is unknown and it is not run again while the key is kept (24 hours). Retry with a new key only once you have established that the change did not happen. See retry safety
tool_call_in_progress409An agent tool retry with the same idempotency_key while the first call still runs. Wait, then read the result or retry. On MCP it is an isError result whose text starts with the code
idempotency_key_reused409An agent tool idempotency_key sent before with different arguments, or by another credential, or an agent sign-up Idempotency-Key sent before with a different body. Use a new key for a new request
mailbox_cloud_unenroll_failed409The mailbox is linked to AY Inbox Cloud and its link could not be released, so DELETE /emails/{id} would leave AY Inbox Cloud holding its credential or its claim on it. The mailbox record remains, and restoration onto its worker is attempted

mailbox_cloud_unenroll_failed is a self-hosted instance losing contact with AY Inbox Cloud mid-delete. Deleting an enrolled mailbox has to revoke its enrollment before the record goes, because the pool holds the mailbox's own SMTP/IMAP credentials and that record is the only thing that knows the enrollment exists. The same call releases a cloud-managed mirror before the record goes, so the mailbox returns to the cloud workspace and can be adopted again instead of staying claimed by an instance that no longer keeps it. The mailbox record remains. AY Inbox attempts to restore it onto its worker immediately, and the worker reconciler may restore it later if that attempt fails. Retry the delete once the instance can reach the cloud again. Unenrolling under Settings > AY Inbox Cloud first does not help: it makes the same call.

422 Unprocessable

Returned when validation fails on the request data.

Common causes:

  • Invalid email format
  • String exceeds maximum length
  • Number outside valid range
  • Invalid enum value

Example:

{
  "error": "Unprocessable",
  "message": "validation failed",
  "code": "unprocessable",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}
codeStatusMeaning
idempotency_key_required422A paid agent tool, such as generate_reply_draft, was called with no idempotency_key. Nothing ran or was charged. Pass a key so a retry is charged once

500 Internal Server Error

Returned when an unexpected error occurs on the server.

Example:

{
  "error": "Internal Server Error",
  "message": "Something went wrong.",
  "code": "internal_error",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Retry the request after a short delay
  • If persistent, contact support with request details

A 500 always carries this same message. The underlying detail is not returned, because it is usually database or provider output naming tables, columns and hosts, none of which helps a caller. It is logged against the request_id in the response, so quoting that id in a support request is what connects the two.

One internal_error variant is worth distinguishing. When an authentication endpoint cannot send its email, the message names that specifically rather than reporting a generic fault, because on a self-hosted instance the person reading it is often the one who can fix it:

{
  "error": "Internal Server Error",
  "message": "We couldn't send the email. If you administer this server, check the mail transport configuration.",
  "code": "internal_error",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

On a self-hosted deployment that means MAIL_TRANSPORT and the SMTP_ variables. See self-hosting.

503 Service Unavailable

Returned when the service is temporarily unavailable.

Every endpoint that checks a password, an emailed or two-factor code, or a sign-in link charges an attempt to its budget before it compares anything. When that budget cannot be read, the request is refused with code: "attempt_check_unavailable" and a Retry-After header, nothing is compared and the attempt is not counted, so a sign-in can briefly fail during a cache outage. Retry after the header. Looking up a CLI or AY Inbox Cloud pairing code (/auth/cli/codes/:code, /pool-link/codes/:code) answers the same code when its lookup budget cannot be read.

{
  "error": "Service Unavailable",
  "message": "We couldn't check this attempt right now, so it was not counted. Wait a few seconds and try again.",
  "code": "attempt_check_unavailable",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Other temporary failures answer the generic form:

Example:

{
  "error": "Service Unavailable",
  "message": "service unavailable",
  "code": "service_unavailable",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Wait and retry with exponential backoff
  • Check status page for incidents

mailbox_provider_not_configured

A 503 whose code is mailbox_provider_not_configured is not transient and retrying will not help. It means the deployment has no OAuth client for the mailbox provider the request asked for, which only happens on a self-hosted install.

{
  "error": "Service Unavailable",
  "message": "Gmail is not configured on this deployment. Set BOX_GOOGLE_CLIENT_ID and BOX_GOOGLE_CLIENT_SECRET in your .env, then restart.",
  "code": "mailbox_provider_not_configured",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Set BOX_GOOGLE_CLIENT_ID and BOX_GOOGLE_CLIENT_SECRET, or BOX_OUTLOOK_CLIENT_ID and BOX_OUTLOOK_CLIENT_SECRET, in the .env at your install root, then restart
  • Or connect the mailbox over SMTP and IMAP instead, which needs no configuration
  • Full walkthrough: connect mailboxes

ai_not_configured

A 503 whose code is ai_not_configured is not transient and retrying will not help. It comes from POST /templates/analyze and means the deployment has no AI provider set up at all. It is how a client tells "there is no AI here" apart from "the provider is having a bad minute", which returns the generic service_unavailable and is worth retrying.

{
  "error": "Service Unavailable",
  "message": "AI analysis is not configured on this deployment.",
  "code": "ai_not_configured",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Set AI_PROVIDER and AI_API_KEY in the .env at your install root, then restart. See the configuration reference
  • Hide the AI affordance in your client rather than retrying: nothing about the request will make it succeed

slack_not_configured

A 503 whose code is slack_not_configured is not transient and retrying will not help. The instance is not set up for Slack, which only happens on a self-hosted install. The three Slack request URLs (/api/v1/integrations/slack/events, /interactivity and /commands) answer it whenever SLACK_SIGNING_SECRET is unset.

{
  "error": "Service Unavailable",
  "message": "Slack is not set up on this AY Inbox instance.",
  "code": "slack_not_configured",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Create the instance's Slack app and set SLACK_OAUTH_CLIENT_ID, SLACK_OAUTH_CLIENT_SECRET and SLACK_SIGNING_SECRET on the backend and the consumer, then restart. See Slack app
  • In a client, read app_configured and interactive_configured from GET /integrations/slack/status and hide the Slack affordances rather than retrying

Those request URLs answer 401 unauthorized to a request whose Slack signature does not verify. Linking a Slack account to a workspace you are not a member of is 403 forbidden, and linking one whose email is not yours without signing in to it with Slack is 403 slack_link_email_mismatch.

mailbox_allowance_reached

A 403 whose code is mailbox_allowance_reached comes from every path that connects a mailbox: POST /emails/onboarding/oauth/start, POST /emails/onboarding/oauth/finish, POST /emails/onboarding/smtp-imap, and per row inside POST /emails/onboarding/smtp-imap/bulk. A mailbox import does not answer it over HTTP: the rows past the allowance fail with the cause allowance_reached and can be retried once it is raised. It is not a permission problem: the workspace holds its whole mailbox allowance, which on a paid plan is one mailbox for every send a day the plan includes, and 10 on a free workspace. Nothing was connected.

{
  "error": "Forbidden",
  "message": "This workspace holds 15000 of its 15000 mailboxes. Request an increase, or move to a plan with more daily sends.",
  "code": "mailbox_allowance_reached",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Read GET /emails/allowance first: remaining says how many connects will succeed, and pending_request whether an increase is already asked for
  • Submit a limit-increase request for max_email_accounts via POST /organization/:orgId/limit-requests, or move to a plan with more daily sends. An approved request raises the allowance immediately; retry the connect then
  • Reconnecting an existing mailbox never returns this code

storage_limit_reached

A 400 whose code is storage_limit_reached comes from POST /campaigns/:id/attachments, from POST /email-images, and from a campaign duplicate that would copy attachments. The workspace's stored bytes, its attachments across every campaign plus its email image library, would pass the quota. The check and the write happen together under one per-workspace lock shared by both, so two uploads racing for the last of the quota cannot both get in. Nothing was stored.

{
  "error": "Bad Request",
  "message": "Storage limit reached: 51190 MB of 51200 MB used, 12 MB to add. Remove attachments or images, or upgrade your plan.",
  "code": "storage_limit_reached",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • GET /organization/current/limits reports storage.used_bytes and storage.limit_bytes, and storage.over_quota when a plan change left the workspace above the quota. Existing attachments keep sending either way
  • Delete attachments you no longer need (DELETE /campaigns/:id/attachments/:attachmentId) or images (DELETE /email-images/:id), or move to a paid plan for the larger quota

mailbox_validation_timeout

A 400 whose code is mailbox_validation_timeout comes from the SMTP and IMAP connect and re-authorize endpoints. AY Inbox proves credentials by opening a real connection to the mail server before it stores anything, and a connection stayed silent inside that window. The message names the leg that did not answer and says whether the other one signed in. Nothing was saved and no mailbox was created.

{
  "error": "Bad Request",
  "message": "SMTP (smtp.gmail.com:465) did not answer in time. IMAP (imap.gmail.com:993) signed in. Nothing was saved. Check the host and port, then try again. Port 465 did not answer and the worker could not use 587 with STARTTLS on the same server either. Some networks block outbound mail ports; check that the server is reachable from outside and which port it listens on.",
  "code": "mailbox_validation_timeout",
  "request_id": "6f3c0a1e-2d47-4c6b-9a1f-2b7c5f0e8d31"
}

The same code with a message that begins AY Inbox's worker did not report back means no worker answered the check at all, so the mail server was never tested. The check is offered to each healthy worker in turn and waits on at most two that take it without answering, so this means the fleet as a whole is not answering. On the hosted product, retry and contact support if it persists. On a self-hosted instance, check that a worker is running, heartbeating, and reaching the same Redis as the backend: the check reaches the worker over Redis and its answer comes back the same way.

How to fix:

  • Read which leg stayed silent. One leg passing and the other hanging is the network between the worker and that port, not the password: a port that nothing listens on, or that a firewall drops, takes the full timeout rather than refusing straight away
  • When 465 never answers, the worker also tries 587 with STARTTLS on the same server and uses it if it connects; a passing check is then stored with port 587. This error means neither port answered, so check that the server is reachable from outside your network and which port it listens on
  • Retry. A mail server under load can be slow once and answer immediately on the next attempt
  • Confirm the server accepts connections from outside your network, and that the security setting matches the port: tls for 465 and 993, starttls for 587 and 143

mailbox_auth_refused

A 400 whose code is mailbox_auth_refused comes from the SMTP and IMAP connect and re-authorize endpoints. The mail server answered and refused the sign-in. The message names the leg and the server, quotes the server's own reply, and for Google's servers says what an app password is. Nothing was saved.

{
  "error": "Bad Request",
  "message": "SMTP (smtp.gmail.com:587) refused the sign-in: 535 5.7.8 Username and Password not accepted. IMAP (imap.gmail.com:993) refused the sign-in: imap: NO [AUTHENTICATIONFAILED] Invalid credentials (Failure). Nothing was saved. Google itself refused this address and password. Use a 16-letter app password from myaccount.google.com/apppasswords, created while signed in to the Google account that owns this address, and enter the account's own sign-in address rather than an alias or a group. A deleted app password stops working at once, and on Google Workspace the administrator can block app passwords or IMAP.",
  "code": "mailbox_auth_refused",
  "request_id": "6f3c0a1e-2d47-4c6b-9a1f-2b7c5f0e8d31"
}

How to fix:

  • Read the server's reply in the message: it says which leg refused and usually why (a wrong password, an app password required, IMAP switched off for the domain)
  • The username is normally the full address. The password is the provider's app password when the account has two-step verification on, never the account password
  • Whitespace is not the problem: AY Inbox trims the password on every path, and removes every space from one bound for a Google server
  • On Google, a refusal with a real app password nearly always means the address is not the account that issued it: enter the account's own sign-in address rather than an alias or a group, create the app password while signed in to that account, and check it has not been deleted since. On Google Workspace the administrator can also block app passwords or IMAP for the organization

mailbox_unreachable

A 400 whose code is mailbox_unreachable comes from the same endpoints. The worker could not open a connection to the server at all: the name did not resolve, the port refused, a firewall dropped the packets, or the host name points to a private network address. The message says which server and which of those it was. Nothing was saved.

{
  "error": "Bad Request",
  "message": "SMTP (smtp.example.com:465) could not be reached from the worker: the port refused the connection. Nothing was saved.",
  "code": "mailbox_unreachable",
  "request_id": "6f3c0a1e-2d47-4c6b-9a1f-2b7c5f0e8d31"
}

How to fix:

  • Check the host and port for typos
  • The connection leaves from the worker, not from your browser or from the API host: the worker's network has to reach the server on that port. Some hosting providers block outbound mail ports until asked
  • A host name that points to a private network address (such as 10.x, 192.168.x or a loopback address) is refused, because AY Inbox only connects mailboxes to public addresses. Use the host name your provider gives for connecting from outside your network. On a self-hosted instance, the message also names MAIL_ALLOW_PRIVATE_HOSTS=true, which the operator can set to connect mail servers on a private network (see configuration)

mailbox_tls_failed

A 400 whose code is mailbox_tls_failed comes from the same endpoints. The server was reached but no secure connection could be made: the certificate did not verify for that host name, the handshake failed, or the port expects a different mode (tls on a STARTTLS port, or the reverse) and no STARTTLS was offered. Nothing was saved.

{
  "error": "Bad Request",
  "message": "IMAP (mail.example.com:143) did not complete a secure connection: the server offers no STARTTLS on this port, and AY Inbox only signs in over an encrypted connection. Use the server's SSL/TLS port (usually 993) or a port that offers STARTTLS (usually 143). Nothing was saved.",
  "code": "mailbox_tls_failed",
  "request_id": "6f3c0a1e-2d47-4c6b-9a1f-2b7c5f0e8d31"
}

When the certificate is what failed, the message says why (issued for a different host name, expired, or not vouched for by a recognised certificate authority) and names the certificate the server presented: who it is for, who issued it (or that it is self-signed) and the SHA-256 fingerprint of its public key. When the server can be trusted on its own certificate instead, the message ends by saying so and the response carries "reason": "certificate_untrusted"; the dashboard's connect and reconnect forms then offer Trust this server's own certificate.

How to fix:

  • Match the security setting to the port: tls for 465 and 993, starttls for 587 and 143, unless the provider documents otherwise
  • Use the host name the certificate is issued for, which is the one in the provider's documentation rather than an alias or an IP address
  • For a server with a self-signed or company certificate, compare the fingerprint with the one the server's owner gives you, then connect again with tls_unverified: true on that server (in the dashboard, Trust this server's own certificate). The mailbox is then trusted on that certificate only. See servers with their own certificate

Mailbox errors TLS_CERTIFICATE_CHANGED and TLS_CERTIFICATE_UNCONFIRMED

These are not responses to a request. They are the error a connected SMTP/IMAP mailbox reports (in the mailbox drawer, GET /analytics/accounts/:id, and the EMAIL_SERVER_ERROR notification) when it trusts a server on its own certificate and AY Inbox did not connect:

CodeMeaning
TLS_CERTIFICATE_CHANGEDThe server presented a public key other than the one confirmed for it, for example after a certificate was replaced with a new key
TLS_CERTIFICATE_UNCONFIRMEDThe mailbox trusts the server on its own certificate but no fingerprint has been confirmed for it yet

Both are warnings: the mailbox is not deactivated, its credentials are not marked invalid, and nothing it was due to send or sync is dropped. It retries on its usual interval.

How to fix:

  • Check with the server's owner that the change is expected
  • Open Settings > Server certificates on the mailbox and choose Re-confirm, or call POST /emails/onboarding/smtp-imap/:id/certificates/check and then .../certificates/confirm with the fingerprints it reported. The error clears once the confirmed certificates connect
  • If the confirm call answers mailbox_tls_failed saying the server did not present the certificate with the fingerprint you confirmed, a fingerprint was mistyped or the certificate changed after the check. Nothing was changed; run the check again and confirm what it reports

mailbox_server_declined

A 400 whose code is mailbox_server_declined comes from the same endpoints. The sign-in could not be completed for a reason other than the credentials: the server asked for a retry later (a 4xx reply, often a login rate limit), it offers no sign-in method AY Inbox implements, it answered with a 5xx that is not about the password (a mechanism it does not support, a STARTTLS it insists on), it only offers sign-in without encryption, the conversation broke, or the probe itself failed before a reply arrived. The message carries the server's reply when there is one. Nothing was saved.

{
  "error": "Bad Request",
  "message": "SMTP (smtp.example.com:587) declined the sign-in for now and asks for a retry: 454 4.7.0 Too many login attempts, please try again later. Nothing was saved.",
  "code": "mailbox_server_declined",
  "request_id": "6f3c0a1e-2d47-4c6b-9a1f-2b7c5f0e8d31"
}

How to fix:

  • For a 4xx reply, wait a few minutes and retry
  • For a server that offers only mechanisms AY Inbox does not implement (NTLM, GSSAPI, XOAUTH2), use the provider's documented SMTP submission host, which normally offers LOGIN or PLAIN; a different password does not add a mechanism. The message lists what the server offered
  • A server that only offers sign-in over an unencrypted connection is refused before any password is sent. Use its SSL/TLS port (usually 465) or a port that offers STARTTLS (usually 587)

mailbox_worker_unreachable

A 503 whose code is mailbox_worker_unreachable comes from DELETE /emails/{id}. Disconnecting a mailbox has to reach the machine that syncs it before the record goes, because once the record is gone nothing can tell that machine to stop. When the instruction cannot be delivered, nothing is removed and the mailbox is left exactly as it was.

{
  "error": "Service Unavailable",
  "message": "This mailbox could not be disconnected right now because the machine syncing it could not be reached. Nothing was removed, so try again in a moment.",
  "code": "mailbox_worker_unreachable",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Retry the delete. It is safe to repeat: a mailbox that is already gone returns 404, and one that is still there is untouched

mailbox_send_as_unsupported

A 400 whose code is mailbox_send_as_unsupported comes from POST /emails/{id}/identity/refresh and from a PATCH /emails/{id} that sets send_as_email. Only Gmail and Google Workspace mailboxes publish the addresses they are allowed to send as; an Outlook or SMTP/IMAP mailbox has no such list, so there is nothing to refresh and nothing to choose from.

{
  "error": "Bad Request",
  "message": "This mailbox's provider does not expose send-as addresses. Only Gmail and Google Workspace mailboxes do.",
  "code": "mailbox_send_as_unsupported",
  "request_id": "9f0a6f21-2c7e-4f2d-9d0e-1f7b9a2c4e55"
}

How to fix:

  • Read GET /emails/{id}/identity first. Its supported field is false for these mailboxes, and identities is empty
  • Send from the mailbox's own address, which is what an empty send_as_email means

mailbox_send_as_unknown

A 400 whose code is mailbox_send_as_unknown comes from a PATCH /emails/{id} that sets send_as_email to an address the provider has not verified for that mailbox. The choice is refused here rather than at send time, where the provider's own refusal arrives days later against a campaign step and names nothing you could act on.

{
  "error": "Bad Request",
  "message": "That address is not one your provider has verified this mailbox to send as. Refresh the list, or add and verify the address in your provider first.",
  "code": "mailbox_send_as_unknown",
  "request_id": "b71c3f88-0b3e-4a41-9a52-2f4f1fd0c0aa"
}

How to fix:

  • Add the alias in Gmail (Settings, Accounts, "Send mail as") and finish its verification
  • Call POST /emails/{id}/identity/refresh so AY Inbox re-reads the list, then set send_as_email to an address whose verified is true

mailbox_signature_too_large

A 400 whose code is mailbox_signature_too_large comes from POST /emails/{id}/identity/refresh with import_signature set. The provider's signature is larger than AY Inbox stores (20000 characters of HTML), and it is refused rather than truncated: half a signature is worse than none. The send-as list is not stored either, so the call changes nothing.

{
  "error": "Bad Request",
  "message": "The signature on this mailbox is larger than AY Inbox stores (20000 characters). Shorten it in your provider and import it again.",
  "code": "mailbox_signature_too_large",
  "request_id": "2d5e9a13-7c41-4f9b-bb17-a0f1d9c6e332"
}

How to fix:

  • Shorten the signature in Gmail, usually by linking an image rather than embedding it, and import again
  • Or write the signature in AY Inbox directly with PATCH /emails/{id}

mailbox_identity_unavailable

A 503 whose code is mailbox_identity_unavailable comes from POST /emails/{id}/identity/refresh. Reading a mailbox's sending addresses is an account operation, so it runs on the worker holding that mailbox, never from the API itself. It is unavailable exactly when that machine is: while the mailbox is being moved between workers, just after a worker restart, or before a newly connected mailbox has been placed. Nothing was changed.

{
  "error": "Service Unavailable",
  "message": "AY Inbox could not reach the machine running this mailbox, so its sending addresses were not refreshed. Nothing was changed; try again in a moment.",
  "code": "mailbox_identity_unavailable",
  "request_id": "6e2f1a90-5d13-4a77-9c0b-73f0b5a2e118"
}

How to fix:

  • Retry. Placement happens within moments, so a second attempt usually succeeds
  • GET /emails/{id} reports the mailbox's status; an inactive mailbox is not placed on a worker at all and will keep refusing until it is reactivated

Error handling best practices

Implement retry logic

For transient errors (5xx, 429), implement exponential backoff:

async function requestWithRetry(url, options, maxRetries = 3) {
  const backoff = (attempt) =>
    new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));

  for (let attempt = 0; ; attempt++) {
    let response;
    try {
      response = await fetch(url, options);
    } catch (networkError) {
      if (attempt === maxRetries - 1) throw networkError;
      await backoff(attempt);
      continue;
    }

    if (response.ok) {
      return response.json();
    }

    // Retry server errors and rate limits; every other 4xx is final
    const retryable = response.status === 429 || response.status >= 500;
    if (retryable && attempt < maxRetries - 1) {
      await backoff(attempt);
      continue;
    }

    // Carry the machine-readable code, e.g. idempotency_outcome_unknown
    const body = await response.json().catch(() => ({}));
    const error = new Error(body.message || `HTTP ${response.status}`);
    error.status = response.status;
    error.code = body.code;
    throw error;
  }
}

A retry of a write that carries an Idempotency-Key answers with whatever the first attempt recorded. A 429 leaves the key as it was, so the same key runs the request unless an earlier attempt already recorded an outcome. A 5xx from a request that reached the operation makes the same key answer 409 idempotency_outcome_unknown instead of running it again; a 5xx from a gateway the request never got past leaves the key unused, so the retry runs it for the first time. On idempotency_outcome_unknown, stop retrying and use a new key only once you have established that the change did not happen. See retry safety.

Parse error responses

Always parse and handle error responses:

async function apiRequest(url, options) {
  const response = await fetch(url, options);

  if (!response.ok) {
    const error = await response.json();
    throw new ApiError(response.status, error.error, error.message);
  }

  return response.json();
}

class ApiError extends Error {
  constructor(status, type, message) {
    super(message);
    this.status = status;
    this.type = type;
  }
}

Rate limiting

When you exceed rate limits, you'll receive:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Please retry after 60 seconds.",
  "code": "rate_limit_exceeded",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

Use the Retry-After header to determine when to retry.

Looking up a CLI or AY Inbox Cloud pairing code is budgeted per signed-in person, and past it answers 429 with code: "code_lookup_limited" and Retry-After: 60. Check the code shown on the other device before trying again.

Admin device request errors

The admin device flow adds admin_device_expired (404, indistinguishable invalid/unknown/expired/ consumed request), admin_device_resolved (409, already decided or mismatched/ replayed consent), and admin_device_slow_down (429, poll faster than five seconds). Start again on expiry and never auto-approve on conflict. Attempt budgets use existing rate_limit_exceeded. Auth/MFA/reauth and dependency failures keep existing codes. Redis failures never authorize a single-factor or anonymous admin. Node diagnostics use generic errors and explicit unavailable/stale coverage, not outage inference.

Admin send hold errors

POST /admin/sends/holds/:id/resolve adds three codes:

codeStatusMeaning
send_hold_not_found404The mailbox is not held from sending, or does not exist. Refresh the list
send_hold_changed409The held send or reason differs from the one submitted, because the hold moved or lifted since the list loaded. Refresh and check the new held send
send_hold_not_resolvable409The hold cannot take a verdict right now (the reason is in the message and on the row), or the evidence stopped applying while it was submitted. Nothing was recorded

Agent sign-up errors

POST /agents/register answers these. Nothing is created when any of them is returned.

codeStatusMeaning
invalid_owner_email400owner_email is not one plain address with a dotted domain. Send a bare address such as [email protected], or leave it out. It is checked for shape only; no lookup is made
invalid_name400workspace_name or agent_name holds a link, an address, markup or characters that are not allowed, or is too long (64 characters)
invalid_idempotency_key400The Idempotency-Key header is not 1 to 255 visible ASCII characters
agent_signup_disabled403This instance does not take agent sign-ups: AGENT_SIGNUP is off, registration is not open, password sign-in is off, or the instance has no account yet. A person can create an account and give the agent a key
idempotent_secret_withheld409This Idempotency-Key already opened a workspace from this address. Its API key is shown once and was not stored. Register again with a new key if it was lost
idempotency_key_reused409The Idempotency-Key was used from this address with a different body
registration_in_progress409The first request with this Idempotency-Key is still running. Retry with the same key in a few seconds
agent_signup_rate_limited429A sign-up budget is spent. reason is address (per IPv4 address or IPv6 /64, 24 hours), network (per /24 or /48, 24 hours), network_hourly (one /24 or IPv6 /32 may take a quarter of the instance's hourly budget) or instance (per hour). Retry-After is the time left until that budget reopens. A refused request is not charged to any sign-up budget, and neither is a registration that failed and left nothing behind. Requests to /agents/register are also capped per address (60 per 15 minutes, answered 429 rate_limit_exceeded) on a counter of their own, never the address's sign-in budget
agent_workspace_unclaimed403Not from /agents/register: publishing a form (PATCH /forms/:id or the update_form tool with status: published), an email image or form asset upload, setting a custom forms domain (PUT /forms/domain), an email check (POST /emails/verify, the verify_email tool, also when called through POST /ai/tools/verify_email/call), a verification request, adding or refreshing a web page as brain material (kind: url, the add_brain_material and refresh_brain_material tools), or any route or tool that runs AI or a paid lookup (/generation, the unibox draft routes, POST /templates/analyze, brain sample-reply and organize, contact research and first lines, enrichment, Looot lead jobs, POST /signals/check, and tools that use AI or an integration, on MCP and on POST /ai/tools/:name/call alike), from a workspace an agent opened and no person has claimed yet, on any instance and whatever its billing. Its forms do not render, its contacts and watched companies are not checked, and its import previews place columns without a model. A campaign tracking domain is saved but stays unverified: its check answers the DNS verdict while the records are missing or wrong, and status: agent_workspace_unclaimed once they check out. Any other AI or credit charge refused for the same reason answers 402 insufficient_credits. The person at owner_email claims it with a code from Forgot password that the agent redeems
agent_signup_unavailable503The budgets could not be counted, or the source address is unknown, so the request was refused rather than let through uncounted. Retry after Retry-After. With reason: client_address the instance cannot tell callers apart: requests reach it through a proxy on an internal address that TRUSTED_PROXIES does not name, or TRUSTED_PROXIES covers the whole internet. Its operator fixes that (network and proxy)

POST /agents/claim answers these. Nothing changes and the agent's key keeps working when any of them is returned.

codeStatusMeaning
agent_claim_code_invalid400The code is wrong, expired (30 minutes) or was replaced by a newer one, or it was mailed to another address than the one this key's account names. A code also ends after 10 wrong codes in all from every key whose account names its address. Ask the person for the code in their newest email, or to request a new one
agent_claim_unavailable403The key's account is not an agent workspace waiting for a person: it named no owner_email, it was claimed already, or a person opened it
agent_signup_disabled403The instance no longer accepts new accounts (registration is not open, or password sign-in is off), so the workspace stays held. While that holds, Forgot password mails no claim code either
conflict409The owner address became someone's login meanwhile
agent_claim_rate_limited429The key presented 5 codes in 30 minutes. Wait for Retry-After

See also

On this page

Error response formatHTTP status codesSend-admission diagnosticsClient errors (4xx)Server errors (5xx)Error details400 Bad RequestImage upload refusalsPassword refusalsApp Attest refusalsCloud connection refusalsAY Inbox Cloud pool linkCLI sign-inAY Inbox Cloud warmup refusalsConsent list refusalsName refusalsRedirect destination refusalsMailbox import refusalsMailbox source refusalsApp password switch refusalsPlacement test refusalsOAuth app and community directory refusalsCredential management refusalsCRM provider refusalsSalesforce sync refusalsApollo and Clay refusalsLead scoring refusalsCampaign brain refusalsAuto-reply refusalsCalendar booking refusalsCompany signal refusalsFirst-line job refusalsAutomation refusalsmailbox_oauth_return_origin401 Unauthorized402 Payment Required403 Forbiddenmailbox_gmail_oauth_disabledlooot_campaign_send_requiredplan_offer_requiredRegistration and invitation refusalsAccount and workspace standingConfirmation requiredMember access scopeslack_link_email_mismatch404 Not Found409 Conflict422 Unprocessable500 Internal Server Error503 Service Unavailablemailbox_provider_not_configuredai_not_configuredslack_not_configuredmailbox_allowance_reachedstorage_limit_reachedmailbox_validation_timeoutmailbox_auth_refusedmailbox_unreachablemailbox_tls_failedMailbox errors TLS_CERTIFICATE_CHANGED and TLS_CERTIFICATE_UNCONFIRMEDmailbox_server_declinedmailbox_worker_unreachablemailbox_send_as_unsupportedmailbox_send_as_unknownmailbox_signature_too_largemailbox_identity_unavailableError handling best practicesImplement retry logicParse error responsesRate limitingAdmin device request errorsAdmin send hold errorsAgent sign-up errorsSee also