ErrorsMenu
On this page

Errors

Updated

Read the error envelope, look up every public error code, and learn which errors are worth retrying.

Every error — validation, authentication, conflict, server fault — comes back in one shape, with the HTTP status set appropriately:

JSON
{
  "error": {
    "code": "CONFLICT",
    "message": "Only draft campaigns can be changed or sent",
    "requestId": "0192f1c4-…"
  }
}

requestId is also the X-Request-Id header, on every response, not only errors. Quote it when you contact support: it identifies the request in Kirora’s 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. Two members are optional:

  • errors lists each field the request got wrong, on VALIDATION_ERROR and DRAFT_INCOMPLETE.
  • details carries what else a code reports, such as the limit a PLAN_LIMIT_REACHED hit or the field an ALREADY_EXISTS names. Its shape depends on the code, as the catalogue below says.

A VALIDATION_ERROR lists the fields it found wrong the same way, whether the request’s schema or a later rule found them. A refusal about the request as a whole, rather than a field (an expired form token, an address whose domain takes no mail), has only its message:

JSON
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Some request values are invalid. Check the field errors.",
    "errors": [
      { "in": "body", "field": "subject", "code": "too_big", "message": "Use at most 998 characters.", "maximum": 998 }
    ],
    "requestId": "0192f1c4-…"
  }
}

in is body, query, params or headers when known; field is dotted (blocks.0.text) and empty for the part as a whole; code is the schema rule that failed, or custom for a rule checked after the schema (a consent statement missing, an expiry in the past). Request bodies are strict: a field the operation does not take is refused, not ignored.

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 the dashboard, the AI console and 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 Meaning
BAD_REQUEST 400 A malformed request that is not a schema violation, such as unreadable JSON.
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 list’s or segment’s name, a sending domain’s domain, a template’s alias, a topic’s name, a webhook’s url, a conversion goal’s eventName, a partner workspace’s externalId, or a form’s slug (unique across every workspace).
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 list or segment an unfinished campaign targets, a list, segment, form, topic or webhook endpoint a live or paused journey names, a segment another segment reads, a push project something scheduled or live sends to, a topic a campaign sends on or rules name, a placed shared block, an image an email loads, a sending domain email still sends from, an event name a journey, segment, campaign or goal reads, a tag such a rule names, a conversion goal an A/B test not yet decided or a live or paused journey’s exit decides by. details, where present, name what uses it, in the shape of that resource’s usage: ListUsage, SegmentUsage, SharedBlockUsage, AssetUsage, SendingDomainUsage; a journey’s form or webhook endpoint gives details.journeys, a topic details.campaigns, details.journeys and details.segments (up to 50 each, with details.hasMore), an event name, tag or conversion goal details.usedBy.
FORBIDDEN 403 Valid key, but this caller may not do this: a non-partner key sending Kirora-Workspace, a partner key used from a browser, an embed session off its routes, or a role or masked attribute that does not allow it.
SCOPE_MISSING 403 The key lacks the operation’s scope. details.requiredScope names it; issue a key that holds it.
AGENT_KEY_REFUSED 403 An agent key may not do this whatever its scopes: send, activate, publish, change contacts or consent, or change something live. The system working, not a misconfiguration: a person does it in the dashboard.
INTERNAL_ERROR 500 Unexpected fault. Whether a retry is safe depends on the operation: see its retry note (x-retry).
NOT_FOUND 404 No such resource in this workspace. Another workspace’s resource returns the same.
METHOD_NOT_ALLOWED 405 The path exists, but not with this method. The Allow header lists the methods it takes.
PAYLOAD_TOO_LARGE 413 The body, or an uploaded file, is larger than this route takes. message states the limit when the route sets its own.
UNSUPPORTED_MEDIA_TYPE 415 The Content-Type is not one this route reads: send application/json, or multipart/form-data to an upload route.
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, path or a header failed validation. errors lists each problem: in (body, query, params, headers, when known), field (dotted, empty for the whole part), code (when a schema rule failed), message (plain words, meant to be shown beside the field), and minimum or maximum on a too_small or too_big rule.
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_BUILDER_UNAVAILABLE 503 This deployment does not serve the hosted builders yet, so no embed session can start. Retrying will not help.
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 A message to this contact is being handed to a provider; retry in a few seconds.
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. A new account’s daily email ramp returns the same way with upgradeTo null and details.increasesAt, when the ramp next raises the limit: no plan lifts it sooner.
SENDING_REVIEW_REQUIRED 403 The sender’s domain is waiting for a person at Kirora, who reviews each new sending domain before it can send, against the workspace’s sending profile. Until approved it has no provider identity, so nothing sends from it, not even a test. details.domain names it and details.status says where its review stands: not_requested (the workspace’s sending profile is not filled in yet), in_review (usually answered within a day) or declined (the domain’s review.declineReason says why). A subdomain of an approved domain needs no review.
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.
DRAFT_INCOMPLETE 409 The draft is not finished; errors names each missing or invalid field.
INVALID_SCHEDULE 400 A fixed sendAt is not in the future, or a local localDateTime has already passed in every timezone.
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 409 The contact has not consented on that channel.
SUPPRESSED 409 The address is suppressed on this channel, for any reason: a bounce, a spam complaint, an unsubscribe, an import or a block added by hand. A transactional send is refused only for a bounce or a complaint about a transactional message. listSuppressions shows the block and its reason.
TOPIC_UNSUBSCRIBED 409 The contact has unsubscribed from the campaign’s or journey step’s topic on that channel.
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.
TEST_SEND_OUTCOME_UNKNOWN 503 The provider may or may not have accepted the test; check the inbox before retrying.
TEST_SEND_ROUTE_NOT_READY 409 The workspace’s sending route is still being set up; retry the test in a few minutes.
TEST_SEND_UNAVAILABLE 503 The test could not be handed to the provider; retry shortly.
SENDING_DOMAIN_UNVERIFIED 409 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 request.
IDEMPOTENCY_KEY_IN_USE 409 A request with this Idempotency-Key is still running; retry after a short wait to get its response.
IDEMPOTENCY_KEY_OUTCOME_UNKNOWN 409 The first request with this Idempotency-Key failed or never returned, and may have taken effect (operations whose retry kind is unsafe): check, then retry with a new key.
CONTENT_NOT_RETAINED 404 The message exists, but what it sent was removed once sending finished, or has expired.
RECIPIENT_EMAIL_MISSING 409 The contact has no email address.
RECIPIENT_EMAIL_UNSUPPORTED 409 The contact’s email address has non-ASCII characters before the @, which Kirora cannot deliver to yet.
TEMPLATE_NOT_PUBLISHED 409 email.template 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.
FORM_CLOSED 409 The form is closed: closed by hand, past its closing time, or at its submission limit. The message is the form’s closed message.

Common problems

VALIDATION_ERROR — the body, query or path failed validation (HTTP 400). Solution: read errors, fix those fields, and send again. A misspelled or unsupported field is one of them.

SCOPE_MISSING — the key lacks a scope the route needs (HTTP 403; details.requiredScope names it). Solution: create a key with that scope, where your key kind allows it.

AGENT_KEY_REFUSED — an agent key asked for something agents may not do, whatever its scopes, such as sending (HTTP 403). Solution: none in code; a person does it in the dashboard (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 — email.template 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 409). Solution: verify the domain under Settings › Email sending, or send from an address on a domain that is already verified.

What to retry

Retry action When it applies
Retry with backoff RATE_LIMITED (honour Retry-After) and SERVICE_UNAVAILABLE. INTERNAL_ERROR and transport-level failures may have done the work: retry them only as the operation’s retry note (x-retry) allows
Retry once the state changes CONTACT_SEND_IN_PROGRESS (in a few seconds), EXPORT_UNAVAILABLE with 409 (poll the export until status is ready), TEMPLATE_NOT_PUBLISHED (after you publish the template) and TEST_SEND_ROUTE_NOT_READY (the sending route is still being set up; try again in a few minutes)
Read, then send again REVISION_CONFLICT: read the current revision, merge your change and send it with that expectedRevision. The request changes, so it is not a retry of the same one
Never retry unchanged Every other 4xx, including 405, 413 and 415. The request is wrong, and repeating it will fail identically. A 404 or 409 that comes back from your retry of a delete, cancel or create may mean the first attempt worked: see What is safe to retry

A 405 (METHOD_NOT_ALLOWED) lists the methods the path takes in its Allow header. A 413 (PAYLOAD_TOO_LARGE) means the body is over the route’s limit; see Request size. A 415 (UNSUPPORTED_MEDIA_TYPE) means the Content-Type is wrong: send application/json (multipart/form-data for an upload).

A request rejected before it runs, such as a validation, missing-scope or rate-limit error, reserves no Idempotency-Key. A refusal returned once the operation has started can be stored and replayed for that key, so after fixing its cause, send it with a new key, following the operation’s retry note. (A send refused for its recipient may still have created the contact; see Idempotency.) A 5xx or a timeout may have done the work: retry only where the operation is safe to repeat. Each operation’s retry note (x-retry) says whether it is. 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