Every error — validation, authentication, conflict, server fault — comes back in one shape, with the HTTP status set appropriately:
{
"error": {
"code": "CONFLICT",
"message": "Only draft campaigns can be changed or sent",
"details": { },
"requestId": "0192f1c4-…"
}
}
requestId is also the X-Request-Id header, on every response, not only errors. Quote it
when you contact support: it finds the request in our logs.
code is a stable machine-readable identifier — branch on this. message is
human-readable English intended for logs and developers, not for end users; its wording
may change at any time. details is optional and its shape varies by code.
The catalogue
These are the codes a /v1 client can receive.
The API defines more codes than these, but the rest are reachable only from cookie-session surfaces — the dashboard, the AI console, account management — and a server-to-server client will never see one. They are deliberately left out of this list so no SDK carries a dead exception type.
Scopes, agent keys and IP
restrictions are explained in Authentication, and
IDEMPOTENCY_KEY_REUSED in Idempotency.
| Code | Status | |
|---|---|---|
BAD_REQUEST |
400 / 413 / 415 | A malformed request that is not a schema violation: unreadable JSON, a body too large, an unsupported content type. |
ALREADY_EXISTS |
409 | Another resource in this workspace already holds a value that must be unique. details.field names it: a contact’s email, phone or externalId, a group’s or segment’s name, a sending domain’s domain, a template’s alias, a partner workspace’s externalId. |
CONFLICT |
409 | The resource is not in a state that allows this: editing a campaign that is no longer a draft, unscheduling one that is not scheduled, editing an ended journey, tracking an event name being deleted. message says what to do instead. |
IN_USE |
409 | Something still uses the resource, so it cannot be deleted: a campaign’s audience group or segment, a placed shared block, an event name a journey, segment, campaign or goal reads. details say where. |
FORBIDDEN |
403 | Valid key, wrong scope, or an agent key on a route agents may not use. |
INTERNAL_ERROR |
500 | Unexpected fault. Retry with backoff. |
NOT_FOUND |
404 | No such resource in this workspace. Another workspace’s resource answers the same. |
RATE_LIMITED |
429 | Too many requests. Wait Retry-After seconds; details.retryAfterSeconds carries the same. |
REVISION_CONFLICT |
409 | The template, campaign or journey changed since you read it (expectedRevision is stale). details carry the current revision, updatedAt and who changed it. |
SERVICE_UNAVAILABLE |
503 | A dependency is temporarily down. Retry with backoff. |
UNAUTHORIZED |
401 | Key missing, malformed, revoked or expired. |
VALIDATION_ERROR |
400 | Body, query or path failed validation. details carries the field errors. |
API_KEY_IP_NOT_ALLOWED |
403 | The key is restricted to other IP addresses. |
AGENT_CONFIRMATION_REQUIRED |
409 | An agent wrote or edited this campaign or journey; a person confirms it in the dashboard before it sends, schedules or activates. |
EMBED_ORIGIN_NOT_ALLOWED |
403 / 409 | 403: an embed session used from a page its partner key does not allow. 409: starting an embed session on a partner key with no allowedOrigins. |
WORKSPACE_SUSPENDED |
403 | The workspace is suspended; nothing sends until it is resumed. |
WORKSPACE_DELETION_PENDING |
403 | The workspace is being deleted. |
CONTACT_SEND_IN_PROGRESS |
409 | The contact is part of a send that is running; try again once it finishes. |
EXPORT_UNAVAILABLE |
409 / 410 | 409: the export is not ready yet. 410: it has expired; start a new one. |
NOTHING_TO_EXPORT |
400 | The export would hold no rows. |
EVENT_NAME_LIMIT_REACHED |
409 | The workspace already tracks the most distinct event names it can. Delete unused names, and put what varies (ids, prices) in properties. |
PLAN_LIMIT_REACHED |
403 | A limit of the workspace owner’s plan stops this: emails this month or today, contacts, image storage, workspaces, members, sending domains, active journeys or the Kiro allowance. details.entitlement names the limit, details.limit and details.used say how far it goes, and details.upgradeTo names the plan that lifts it. |
PLAN_FEATURE_UNAVAILABLE |
403 | The workspace owner’s plan does not include this feature. details.entitlement names it and details.upgradeTo the plan that does. |
CAMPAIGN_AUDIENCE_CONFLICT |
409 | Messages already exist for part of this audience from an interrupted send; send again to reconcile. |
EMPTY_AUDIENCE |
400 | The audience resolves to nobody. |
INVALID_CAMPAIGN_CONTENT |
400 | The content snapshot is not valid. |
DRAFT_INCOMPLETE |
409 | The draft is not finished; details.issues names each missing or invalid field. |
INVALID_SCHEDULE |
400 | sendAt is not in the future. |
UNSUPPORTED_CHANNEL |
400 | The channel is not available on this deployment. |
POSTAL_ADDRESS_MISSING |
409 | Marketing email needs the workspace’s postal address; set it first. |
CONSENT_MISSING |
403 | The contact has not consented on that channel. |
SUPPRESSED |
403 | The address is suppressed after a bounce or complaint. |
SIGNUP_BLOCKED |
400 | The address matches the workspace’s signup blocklist, so no contact is created. |
INVALID_SENDER_ADDRESS |
400 | The from-address is not a valid sender. |
SENDING_DOMAIN_DMARC_MISSING |
409 | Marketing email needs a DMARC record on the sending domain (p=none at least). |
SENDING_DOMAIN_IDENTITY_CONFLICT |
409 | The domain conflicts with an existing provider identity. |
SENDING_DOMAIN_UNVERIFIED |
403 | The from-address is not on a verified sending domain; nothing sends from it until DNS verification succeeds. |
IDEMPOTENCY_KEY_REUSED |
409 | Same Idempotency-Key, different payload. |
CONTENT_NOT_RETAINED |
404 | The message exists, but what it sent was never kept or has expired. |
RECIPIENT_EMAIL_MISSING |
400 | The contact has no email address. |
TEMPLATE_NOT_PUBLISHED |
409 | templateId names a template with no published version. Publish it first. |
TEMPLATE_ENVELOPE_INCOMPLETE |
409 | The template and the send together give no subject or no from; details name what is missing. |
PUSH_APP_NOT_READY |
409 | No configured push application. |
PUSH_DESTINATION_MISSING |
409 | The contact has no active push installation. |
Common problems
VALIDATION_ERROR — the body, query or path failed validation (HTTP 400).
Solution: read details for the field errors, fix those fields, and send again.
FORBIDDEN — the key lacks a scope the route needs (HTTP 403, message “API key lacks
the <scope> scope”). Solution: create a key with that scope, or add it where your
key kind allows; an agent key is also refused on send routes whatever its scopes
(see Authentication).
IDEMPOTENCY_KEY_REUSED — the same Idempotency-Key arrived with a different
payload (HTTP 409). Solution: repeat the original request exactly to retry it, or use
a new key for a genuinely different send.
TEMPLATE_NOT_PUBLISHED — templateId names a template with no
published version (HTTP 409). Solution: publish the template, then retry with the
same Idempotency-Key.
RATE_LIMITED — too many requests for the budget (HTTP 429). Solution: wait
Retry-After seconds and retry with backoff; see Rate limits.
SENDING_DOMAIN_UNVERIFIED — the from-address is not on a verified sending domain
(HTTP 403). Solution: verify the domain under Settings → Email → Domains, or send
from an address on a domain that is already verified.
What to retry
| Retry with backoff | RATE_LIMITED (honour Retry-After), SERVICE_UNAVAILABLE, INTERNAL_ERROR, and transport-level failures |
| Never retry unchanged | Every 4xx other than 429 — the request is wrong, and repeating it will fail identically |
Retrying is only safe when the operation is idempotent. Read Idempotency before building retry logic into a write path.
Mapping errors in an SDK
Model code on the catalogue above. Then:
- Expose
codeas a typed value, not a bare string. - Keep the unparsed
detailsreachable — clients need the field errors inside aVALIDATION_ERROR, and no typed wrapper will anticipate every shape. - Include the HTTP status and, where available, a request identifier on the exception.
- Handle an unrecognised code without crashing. New codes are additive and can appear before your SDK is updated; fall back to a generic error carrying the raw code rather than throwing a parse failure.