Errors
Every error type the API returns, what causes it, and whether retrying helps.
Every error has the same shape:
type is stable and safe to branch on, and a type means the same thing, with the same status, on every endpoint. message is for people: it names the field or record at fault and often the call that fixes it, and its wording may change. Two extra fields can appear beside them, and no others:
details, on most validation failures (422 invalid_request): the list of problems, each with thepathof the failing field and amessage.missing, on every422 missing_variables: the names of the template variables that had no value. A failed entry in a batch carries it too.
POST /v1/automations/{id}/enroll is the one response that puts fields beside error rather than inside it; see Not an error. The conventions every endpoint shares are on the API overview.
Retrying safely
- Errors are never stored against an
Idempotency-Key. Only responses below 400 are kept for replay, so after fixing a4xxyou can retry under the same key and the request runs again rather than replaying the error. A5xxis not stored either, which means a retry under the same key also acts again: read what happened before retrying one. - A key is reused only with the identical request. The same key with a different body answers
422 idempotency_key_reusedand does nothing. That covers a changed body after a success, and a changed body while the first request is still running. Use a new key for a new request. 409 idempotency_in_progressmeans the same key is still being processed. A request that finds its key in use waits up to 8 seconds for the other to finish and returns that response; this answer means the other was still running after the wait. Nothing was done by the second request. Wait a moment and retry with the same key and body: if the first request succeeded you get its stored response, and if it failed your retry runs.- A
207is a success, and it is stored. A batch in which some entries failed replays whole under the same key. Resend only the failed entries, under a new key.
The Retry? column below means:
- Yes (after a pause): the condition clears by itself. Wait and send the same request again.
- No (fix the request): the same request fails the same way. Change it first.
- No (a person must act): nothing the caller can put in a request fixes it. Stop and tell someone.
- Sometimes: it depends on the case, as the row explains.
Error types
| Status | Type | Retry? | Meaning |
|---|---|---|---|
| 400 | invalid_json | No (fix the request) | The request body is not valid JSON. Applies to every endpoint that reads a body, and is checked before the credentials. |
| 400 or 415 | invalid_request | No (fix the request) | The body could not be read at all: its size did not match Content-Length, the upload was cut off, or it used a charset or content encoding the API does not accept (415). |
| 401 | unauthorized | No (fix the request) | The Authorization: Bearer header is missing, the API key does not exist or has been revoked, or the OAuth access token is invalid or expired. Checked before the rate limit, the scope and the route itself, so an unknown /v1 path without a valid key answers 401 rather than 404. A revoked key needs a new one from a person. See authentication. |
| 402 | plan_limit_reached | No (a person must act) | The workspace's plan does not allow it. From POST /v1/emails, each entry of POST /v1/emails/batch, and approving a draft with POST /v1/approvals/{id} (the approval stays pending): the workspace is on Free, which includes 3,000 emails a month and stops rather than metering past them, and this send would go past it; GET /v1/usage shows what is left. Nothing is charged and nothing is upgraded automatically — a person has to activate paid sending, or wait for the month to turn over. From POST /v1/domains: the workspace already has as many sending domains as its plan allows, where a root domain counts once whichever subdomains it has, or it is less than 7 days old and already holds 5 root domains, which is the limit for a new workspace on every plan (write to support to lift it sooner). |
| 402 | payment_method_required | No (a person must act) | No outbound email may leave a workspace that has never had a payment method verified, including on Free and including a test send. Nothing an API caller can change fixes it: a person has to add one in the dashboard, at which point the first 3,000 emails a month are still free and nothing is charged until paid sending is explicitly activated. Sent by POST /v1/emails, each batch entry, approving a draft, and campaign actions. Inbound mail is unaffected and keeps arriving. |
| 402 | billing_past_due | No (a person must act) | The workspace's paid subscription is unpaid or canceled — Stripe has already retried the payment and given up. A failed payment on its own does not lock sending; this is what is left after dunning. The payment method has to be fixed in the dashboard. Inbound mail is unaffected. |
| 402 | budget_exceeded | Sometimes | The workspace set its own monthly spend ceiling and has reached it. By default that pauses marketing sending and lets transactional through, so the same workspace can answer this for risk_class: marketing and send normally for transactional; a workspace that chose to pause everything answers it for both. It clears when the period turns over, or when a person raises or removes the budget. |
| 403 | forbidden | No (a person must act) | The credential may not do this, and the message says why. The commonest cause is a missing scope, which the message names, including on a replayed Idempotency-Key whose original request needed a scope this credential lacks. A missing scope is reported before idempotency_key_reused, so a credential without the scope never learns that its key was used with another body. The others: deciding an approval (POST /v1/approvals/{id}) with an OAuth access token, with the key that made the draft, or with any key that has a guardrail of its own (requires_approval, allowed_recipients or daily_send_limit); sending, scheduling, rescheduling or resuming a campaign, or deciding its A/B winner, with a key that has requires_approval or allowed_recipients, which is answered before the campaign's status or the body is checked (keys with guardrails); enrolling someone (POST /v1/automations/{id}/enroll or POST /v1/events) with a key that requires approval; a key editing or revoking itself; and creating or editing a key with scopes or looser guardrails than the calling credential holds (API keys). |
| 403 | recipient_not_allowed | No (a person must act) | The key has an allowlist and an address in to, cc or bcc is not on it, or, when enrolling someone or recording an event, the email is not. Sent by POST /v1/emails, each batch entry, POST /v1/automations/{id}/enroll and POST /v1/events. Do not retry with another spelling of the address; ask a person to change the key's allowed recipients. |
| 404 | not_found | No (fix the request) | The id or key in the path does not exist in this workspace (a record in another workspace is also a 404), and the message names what is missing, such as No such message, No such webhook endpoint or, for DELETE /v1/contact-properties/{key}, the key. When the value you passed is the name, domain or email address of a record rather than its id, the message says so and gives the id to use. Also sent by POST /v1/approvals/{id} when the held message no longer exists. A path or method the API does not have answers the same type with the message No such API route. |
| 409 | conflict | Sometimes | An identifier is already taken in this workspace: the slug on POST /v1/automations, the new slug on POST /v1/templates/{slug}/duplicate, or the key on POST /v1/segments. Choose another; retrying does not help. POST /v1/templates and POST /v1/topics do not answer this, because they update the record with that slug or key. From POST /v1/contact-properties only, it means another request created the same property at the same moment, and retrying updates it. |
| 409 | invalid_state | Sometimes | The record's current state does not allow the action, and the message names the state. Retrying the same call does not help unless the state changes. Sent when: cancelling an email that is not scheduled (DELETE /v1/emails/{id}); deciding an approval that was already decided or has expired, or approving one whose message is no longer a held draft (POST /v1/approvals/{id}); editing a revoked API key (PATCH /v1/api-keys/{id}); resubscribing a contact suppressed by a hard bounce or a complaint, which a person has to lift first (PATCH /v1/contacts/{id}); enrolling someone in an automation that cannot take anyone (see Not an error); deleting a template or topic that an automation or unfinished campaign still uses, where the message lists them (DELETE /v1/templates/{slug}, DELETE /v1/topics/{key}). Campaigns (campaigns): editing one that is not a draft, sending one that is neither a draft nor scheduled, unscheduling one that has started, resuming one that is not paused, deciding a winner on one that is not an A/B test in testing, and deleting one that is sending, which works once it has finished or paused. Resuming a campaign paused on its domain's warm-up allowance answers this until the allowance renews, and the campaign then resumes by itself, so there is nothing to retry. |
| 409 | idempotency_in_progress | Yes (after a pause) | A POST with this Idempotency-Key and body on the same path was still being processed after an 8 second wait. See Retrying safely. |
| 409 | approval_in_progress | Yes (after a pause) | From POST /v1/approvals/{id}: another request is releasing this approval right now. Check again in a moment: once that release succeeds the approval is decided and a repeat answers 409 invalid_state, and if it fails the approval is still pending and can be decided again. |
| 410 | gone | No (fix the request) | From GET /v1/inbound/{message_id}/attachments/{index}: the attachment was listed on the message, but the raw mail it lived in is kept for only 30 days and has expired. See inbound attachments. |
| 413 | payload_too_large | No (fix the request) | The body is larger than the endpoint accepts: 15 MB for POST /v1/emails, 25 MB for POST /v1/emails/batch, 5 MB for everything else. The message gives the limit. Put large files behind a link, or split a batch. |
| 422 | invalid_request | No (fix the request) | The request failed validation. Usually a field is missing, of the wrong type or out of range, and details lists each problem. Also sent when a body field refers to something that does not exist in this workspace (audience_id, identity_id, segment_id, trigger.audience_id, thread_id, reply_to_message_id, a template or step template_slug), in which case the message starts with the field's name (audience_id: …, steps.0.template_slug: …) and, when the value is a record's name rather than its id, gives the id; and when a request breaks a rule of the endpoint, such as more than 50 recipients across to, cc and bcc (the message gives the limit and the count, and is the same whichever fields hold the addresses), a key sent to PATCH /v1/segments/{id}, which cannot change, a webhook URL on a port the API does not deliver to, text: "" on POST /v1/templates (send text: null to remove a template's text body, or leave text out to keep it), or a status on GET /v1/automations/{id}/enrollments other than active, completed, canceled or failed. A marketing message may have only one recipient across to, cc and bcc, because it carries that recipient's own unsubscribe link: a marketing POST /v1/emails naming more is refused with this error and the count, as is approving a held marketing draft that names more (POST /v1/approvals/{id}, where the approval stays pending for a person to reject). Send one message per recipient, or use a campaign. In POST /v1/emails/batch, an entry over the recipient limit, or a marketing entry with more than one recipient, fails only that entry, while an entry that fails schema validation fails the whole batch with this error, and each issue's path starts with the entry's index. |
| 422 | idempotency_key_reused | No (fix the request) | A POST reused an Idempotency-Key with a different request body on the same path. Nothing was done. Answered after the route's scope check, so a credential without the scope gets 403 forbidden instead. Use a new key for a new request. See Idempotency. |
| 422 | missing_variables | No (fix the request) | A template declares variables that the request gave no value for. Always carries missing, the names of those variables. Sent by POST /v1/emails, by each batch entry (whose error also carries missing), by POST /v1/templates/{slug}/render, and by creating or updating a contact_added or tag_added automation whose default_variables leave a placeholder uncovered. Nothing was sent or changed. |
| 422 | unknown_topic | No (fix the request) | A topic key that does not exist in this workspace: topic on POST /v1/emails and each batch entry; topic_key on POST /v1/automations, PATCH /v1/automations/{id}, POST /v1/segments, PATCH /v1/segments/{id} and POST /v1/broadcasts; or any key in topics on POST /v1/topics/preferences, where nothing is saved. The message starts with the field (topic:, topic_key: or topics.<key>:). List topics with GET /v1/topics, or create the topic first. A topic in the path of POST /v1/topics/{key}/import or DELETE /v1/topics/{key} is a 404 instead. |
| 422 | invalid_schedule | No (fix the request) | From POST /v1/emails and each batch entry: scheduled_at is neither a date the API can parse, such as an ISO 8601 time, nor a relative time such as in 3 days. Nothing was stored. |
| 422 | no_verified_identity | No (a person must act) | No verified sending domain can send this. From POST /v1/emails and each batch entry: no verified domain in the workspace matches the domain in from for the send's risk_class. From POST /v1/automations: the domain in from has no verified marketing sending domain, and every automation step sends as marketing. From approving a draft with POST /v1/approvals/{id}: the sending domain the draft was held on has since been deleted, and the approval stays pending. Have the domain added and verified first. |
| 422 | no_postal_address | No (a person must act) | The workspace has no postal address, and marketing email must carry one. Sent by a marketing POST /v1/emails or batch entry, by approving a marketing draft, and by POST /v1/broadcasts/{id}/send and /resume. Add it in Settings. |
| 422 | warmup_limit | Sometimes | The sending domain is warming up and has used today's allowance of marketing mail. Sent by a marketing POST /v1/emails or batch entry that is not scheduled, and by approving a marketing draft that is due to go out now. Not before 00:00 UTC, when the allowance renews; sending the same thing sooner fails the same way. Campaigns are never refused for this: they pause and resume on their own. |
| 422 | workspace_suspended | No (a person must act) | Sending is suspended for the workspace, and the message gives the reason. Sent by POST /v1/emails and each batch entry, approving a draft, and POST /v1/broadcasts/{id}/send and /resume. Contact support. A campaign that paused because of the suspension continues by itself once it is lifted. |
| 429 | rate_limited | Yes (after a pause) | The credential made more than its per-minute allowance of requests. The Retry-After header gives the seconds until a request will be accepted. See rate limits. |
| 429 | daily_limit | Sometimes | The key's daily send limit, which counts recipients sent, scheduled and held for approval since 00:00 UTC, would be exceeded. From POST /v1/emails and each batch entry, and from a campaign action whose recipients do not fit what is left of the limit today, in which case nothing changes: POST /v1/broadcasts/{id}/send on a draft (sending or scheduling it) or on a scheduled campaign (starting it now, or moving it earlier with scheduled_at), PATCH /v1/broadcasts/{id} moving a scheduled campaign's scheduled_at earlier, POST /v1/broadcasts/{id}/resume, and POST /v1/broadcasts/{id}/winner. A key is charged once for each campaign it puts on its way, so the actions on a scheduled, paused or testing campaign count what it still has to send, and count nothing for a key that already paid for that campaign; see keys with guardrails. Not before the next UTC day, or until a person raises the limit: the Retry-After header gives the seconds until 00:00 UTC. An agent should stop, not back off and retry. |
| 500 | internal_error | Sometimes | Something failed on our side, and the request may have done some or all of its work first. Check before retrying: for a send, look for the message in GET /v1/emails. A retry under the same Idempotency-Key runs again, because a 500 is not stored. |
| 502 | ses_error | Sometimes | The email provider refused or failed the request. From POST /v1/emails, a batch entry or approving a draft: throttling is transient, so retry after a pause, while any other refusal marks the message failed with the provider's reason, and sending the same thing again fails the same way. A failed approval stays pending and can be approved again once the cause is fixed. From DELETE /v1/domains/{id}: the provider did not delete the domain's sending identity, and the domain is still in the workspace. Retry after a pause; a retry picks up where the failed attempt stopped. |
| 503 | service_unavailable | Yes (after a pause) | The API is under heavy load and could not get to the request within a few seconds. It is answered instead of leaving the request to hang. The Retry-After header gives the seconds to wait. Usually nothing was done, because the request stopped at the credential check, but a request already under way may have done some of its work first: for a send, look for the message in GET /v1/emails before retrying, as after a 500. |
Not an error
These responses look like failures but are the API doing what was asked. None of them should be retried as they are.
202withskipped: truefromPOST /v1/emails(orok: true, skipped: truein a batch entry): everytoaddress was dropped, so nothing was sent and the message is recorded withstatus: "rejected".reasonsays why:suppressed:<reason>,unsubscribed_from_topic:<topic>, orundeliverable_domain:<domain>for a recipient domain that cannot receive mail. When only some recipients are dropped the rest are sent to, andskippedisfalse. Every accepted send carriesskipped,reasonandapproval_id, so checkskippedfor its value rather than for its presence.202withstatus: "pending_approval"and anapproval_id: the key holds its sends for a person to release. See approvals.200withskipped: truefromPOST /v1/approvals/{id}withdecision: "approve": every recipient unsubscribed or was suppressed while the draft waited. The approval is decided and the message is recorded as rejected.207fromPOST /v1/emails/batchwhen at least one entry failed, even if all did. Each entry indatahasok, and a failed one has its ownerrorwithtypeandmessagefrom the table above.failedcounts them. A batch answers202only when every entry succeeded, and a body that is not an array of 1 to 100 messages, or that has an entry failing schema validation, fails whole with422 invalid_request.- Enrolling someone with
POST /v1/automations/{id}/enroll:200withenrolled: false, areasonandenrollment: nullmeans the person was not enrolled and that is the correct outcome. The reason isalready_enrolled,previously_enrolled(the automation'sreenrollmentisneverand the person has been through it),suppressed,unsubscribed_from_topic,exit_tagorrequired_tag_missing. When the automation itself cannot take anyone, the answer is409withenrolled: false, areasonofnot_active,no_stepsortopic_missing,enrollment: null, and anerrorobject of typeinvalid_statewhose message says what to fix. That one needs the automation changed, not a retry. 200withautomations_started: 0fromPOST /v1/events: no automation waiting for that event enrolled the person, because none is active, or each one excluded them or already had them.202withab_test.decided_by: "auto"fromPOST /v1/broadcasts/{id}/winner: the worker decided the test before this call could. The response is the campaign, and theab_test.winnerin it is the one that stands; avariantyou passed was ignored.200withremoved: falsefromDELETE /v1/suppressions/{email}: there was no suppression for that address in that scope.200instead of201fromPOST /v1/templates,POST /v1/topicsandPOST /v1/contact-properties: the record already existed and was updated.- An
Idempotent-Replay: trueheader: the response is the stored answer to an earlier request with the same key and body, and nothing was done this time.