Menu

Errors

The envelope, every public code, and which are worth retrying

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 code as a typed value, not a bare string.
  • Keep the unparsed details reachable — clients need the field errors inside a VALIDATION_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.

Next steps