SendsetsDocs

Error codes

Reference for all API error codes and their meanings.

The SendSets 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",
  "fix": "The one-line remediation, when the API knows one."
}

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.

fix is optional and written for an agent or a script: the command or change that clears the error, in one line. It is omitted when the API has nothing specific to suggest. The sendsets CLI prints it under the error.

Agent-facing codes

The codes an agent branches on while setting a workspace up. Each carries a fix. Endpoints that answer with a checklist (POST /app-connections/:id/test, POST /emails/:id/test, POST /campaigns/:id/validate) put the same code, message and fix inside the 200 body under error or problems[], because a failing app or mailbox is a result, not a transport error.

CodeStatusRaised by
agent_sql_unavailable503the SQL tools on an instance whose database user could not create the sendsets_agent_reader role
agent_sql_busy429the SQL tools while the server is already running its limit of agent queries; retry shortly
unknown_table404sql_schema asked to describe a table that is not in the list
app_connection_failed200 (checklist)POST /app-connections/:id/test when the app cannot be reached or refuses the signed app.ping
app_action_failedstep runan app_action step whose endpoint answered a non-2xx, a non-object, or nothing in time
event_contact_not_found404POST /events for an email or contact id that matches no contact; send an identity object to record it without a contact
invalid_event_identity400POST /events or /events/test without exactly one of contact_id, email, identity, or with an identity that has no user id, email, or anonymous id
invalid_event_name400an event name, goal event, or trend that is not dot-separated lowercase words
invalid_occurred_at400occurred_at more than a day in the future
stripe_signature_invalid400Customer Stripe webhook signature verification failed
stripe_event_invalid400Invalid snapshot event, mismatched mode, or Connect-account event
stripe_event_too_large400Invalid request body or a customer Stripe webhook over 1 MiB
stripe_event_conflict409A customer Stripe event ID was already recorded with different content
event_too_large400event data over 64 KiB, from POST /events or the record_event tool
idempotency_key_required400POST /events without an Idempotency-Key header, or record_event without idempotency_key
idempotency_key_conflict409POST /events reusing an Idempotency-Key with a different event body
invalid_identity_link400POST /identities/link with fewer than two identifiers, or one identifier without an account
invalid_goal400a goal whose name, stage, definition, predicates, or value settings are invalid; a primary custom goal
invalid_predicate400a trend or funnel filter that is not a valid predicate
goal_not_found404a goal id outside the workspace, including funnel goal steps
goal_name_taken409creating or renaming a goal to the name of another live goal
goal_archived409editing or replaying an archived goal
goal_replay_active409starting a replay while one is queued or running for that goal
goal_replay_not_found404GET /goals/:id/replays/:replayId
invalid_range400a report, preview, or replay range that is inverted or longer than 366 days, or GET /analytics/insights/infrastructure with days other than 7, 14, 30, 60, or 90
invalid_window400window_days outside 1 to 365
invalid_dimension400GET /acquisition/breakdown or GET /analytics/insights/breakdown with an unknown dimension
invalid_filter400GET /acquisition/prospects with an unknown filter or dropoff
invalid_funnel400a funnel with fewer than 2 or more than 8 steps, or an incomplete step
invalid_spend400negative spend, or a currency that is not a three-letter code
invalid_attribution_settings400Attribution window outside 1 to 365 days
invalid_attribution_correction400Missing correction reason or a reason longer than 500 characters
attribution_not_found409Correcting a person who has no acquisition yet
attribution_send_not_eligible400Correction names a send not confirmed to this person at or before acquisition
person_not_found404GET or DELETE /people/:id, or an attribution request naming a person outside the workspace
person_has_contact409DELETE /people/:id for a person linked to a contact; delete the contact instead
identity_conflict_not_found404POST /identity-conflicts/:id/resolve
identity_conflict_resolved409resolving a conflict that is already merged or dismissed
identity_merge_contacts409merging two people linked to different contacts
contact_not_found404a prospect timeline or identity link naming a contact outside the workspace
event_not_found404GET /events/:id
event_run_not_found404GET /campaigns/:id/test-runs/:runId
mailbox_auth_failed400, or 200 (checklist)POST /emails when the credential fails validation; POST /emails/:id/test when the stored credential no longer works
mailbox_unhealthy200 (checklist)POST /emails/:id/test when the mailbox is inactive or no worker can check it
mailbox_provider_requires_oauth400POST /emails with a Google or Microsoft provider; use POST /emails/onboarding/oauth/cli-start
mailbox_consent_expired404GET /emails/onboarding/oauth/session/:session after the consent link lapsed
warmup_enable_failed400POST /emails/:id/warmup/start or /resume refused
campaign_invalid400, or problems[]POST /campaigns/:id/start on a campaign with no steps; the summary of a failed validate
campaign_no_mailbox400, or problems[]a campaign with no active mailbox to send from
campaign_no_contactsproblems[]a campaign with no leads
campaign_variable_missingproblems[]a template references a field no contact provides or an .App key no earlier step produces
campaign_variable_incompleteproblems[]a template renders a field with no fallback that some of the campaign's leads leave blank; the message gives the count and a sample
plan_required403POST /campaigns/:id/start when the workspace's plan does not include campaign sending; the key's scopes are not the cause
step_no_connection, step_unreachable, wait_unbounded, wait_timeout_unroutedproblems[]structural problems in the step graph
run_failedtest runa single-prospect campaign test that ended in failure
app_connection_not_found400a run file's app_action.connection names no connection of the workspace
mailbox_provisioning_unavailable503managed mailboxes on an instance that does not offer them (every self-hosted instance)
mailbox_provisioning_failed503, or order errorthe mailbox provider could not be reached, or could not fulfil the order
pricing_unavailable503live provider pricing failed, a domain price was invalid, or the quoted line totals were incomplete
domain_verification_pending409the external domain's ownership TXT record is missing or does not match
provider_delayed, domain_registry_delayed, provider_acceptance_unresolved, provider_refused, provider_failedorder errora paid purchase is being reconciled or needs support; the order remains visible
registration_contact_missing, domain_registry_unresolved, provider_workspace_unresolved, provisioning_delayedorder errorpaid infrastructure needs support because registration details, the domain registry, the provider workspace, or activation could not be completed
provision_provider_invalid, provision_count_invalid, provision_domain_invalid, provision_domains_invalid400a quote request outside what can be ordered, including more domains than mailboxes
domain_unavailable400the requested domain cannot be registered
provision_over_allowance403a quote or order for more mailboxes than the workspace's mailbox allowance has room for, counting mailboxes still on order
quote_not_found, quote_expired, quote_consumed404, 409a provision request naming a quote that is missing, older than fifteen minutes, or already ordered
payment_expired, payment_failed, subscription_endedorder errorthe checkout lapsed, a renewal failed, or the subscription behind the mailboxes ended
renewal_not_eligible, renewal_payment_required, renewal_request_failed, renewal_refused, renewal_failedrenewal errora domain renewal that needs a person: the provider will not renew it, the card on file failed, or the provider refused after payment
redirect_not_allowed400a checkout or billing-portal request whose return URL is not on an origin this deployment owns

HTTP status codes

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 AI credits (insufficient_credits)
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)

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": "invalid request body",
  "code": "bad_request",
  "request_id": "4bbbd1b2-8f86-47dd-8a7f-9476501ad20e"
}

How to fix:

  • Check that your JSON is valid
  • Verify all required fields are present
  • Ensure field values match expected types

Specific 400 codes:

codeMeaning
invalid_lead_statusPOST /contacts/search or POST /contacts/export was given a lead_status that is not one of the documented values
query_refusedAn agent SQL query was not a single read-only SELECT, or used something no read needs; the message names it
query_failedPostgres rejected an agent SQL query (a syntax error, an unknown column); the message is Postgres's, or the 30-second limit
query_too_costlyAn agent SQL query's estimated cost is over the ceiling; filter it or aggregate before joining
invalid_lead_queryA lead database request has an unknown match, a condition the segment rules reject, or a q longer than 200 characters
invalid_sortA lead database list was given a sort it does not support
invalid_cursorA lead database list was given a cursor it did not issue
invalid_limitA lead database list was given a limit outside 1 to 200
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
selection_too_largeA "all": true bulk selection resolved to more than 50,000 rows, or a sql_bulk_contacts query selected more than 10,000 contacts. Narrow the filter and run it in parts; nothing was changed
selection_needs_idA sql_bulk_contacts query did not return the contacts' ids as a uuid column named id or contact_id
invalid_filterA 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
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
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

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 ssk_... header
  • Verify your API key is correct
  • Check if your key has expired or been revoked
  • Generate a new key if necessary

Two 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 sendsetsctl 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 is requested but the organization is out of 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

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

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

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
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
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.

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

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"
}

One conflict carries its own code. 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

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"
}

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

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:

{
  "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.

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

gmail_oauth_disabled

A 400 whose code is gmail_oauth_disabled means the request tried to connect a Gmail or Google Workspace sending mailbox through Google sign-in. Gmail sending mailboxes connect with a Google app password over IMAP and SMTP instead. Google sign-in to SendSets itself is unaffected.

How to fix:

  • Create an app password at https://myaccount.google.com/apppasswords (2-Step Verification must be on)
  • Connect with POST /emails and provider: smtp_imap (imap.gmail.com:993, smtp.gmail.com:465, TLS), or sendsets mailbox add --provider gmail --email ADDRESS --smtp-pass APP_PASSWORD

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

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. 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_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 SendSets 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 SendSets 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 SendSets 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 SendSets 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": "SendSets 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) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

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

      // Don't retry client errors (4xx) except rate limits
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        throw new Error(`Client error: ${response.status}`);
      }

      // Retry server errors and rate limits
      if (attempt < maxRetries - 1) {
        const delay = Math.pow(2, attempt) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
    }
  }
}

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.

See also

B2B database

Catalog errors use the standard envelope with code and request_id. See the B2B database reference for invalid_b2b_filter, invalid_limit, invalid_cursor, invalid_selection, b2b_email_required, b2b_unavailable, and b2b_query_failed.

On this page