Troubleshoot your integrationMenu
On this page

Troubleshoot your integration

Updated

Go from a symptom to its first checks and the evidence to collect, using the request id, the request log and the error codes.

Start from what you see, then follow the evidence in order: did the request reach Kirora, what did it return, was the work accepted, and what happened to it afterwards. Each step below names the first things to check and where the evidence is.

Find the symptom

You see Go to
No HTTP response at all Cannot connect
401 401 Unauthorized
403 403 Forbidden
404 for something that exists 404 for a resource you know exists
202, but the email never arrived Accepted, but not received
A batch returned 2xx, but some data is missing Part of a batch is missing
Duplicate contacts, or 409 from identify Duplicates and identity conflicts
A retry did the work twice, or returned 409 Retries
A push did not arrive Push not received
Webhooks do not arrive, or fail verification Webhooks
A campaign or journey does not send Campaigns and journeys
An embedded builder cannot send a test Embedded builders
413, 415 or 429 Size, format and rate

The request id and the request log

Every response, success or error, carries an X-Request-Id header, and every error body repeats it as error.requestId. Log it with each call. It is the one value that finds the request on Kirora’s side.

In the dashboard, Developer → Request log lists the workspace’s API requests for the last 31 days, with their status and a short reason for each failure, and finds one by its exact request id. It is open to owners and admins, who also see each key’s own calls under Developer → API keys.

Cannot connect

No status code means the request never reached Kirora, so there is no request id.

  • First checks: the base URL is https://api.kirora.io; your server resolves it; your outbound firewall or proxy allows it; your HTTP client’s TLS settings are the defaults. Do not turn off certificate checks to make an error go away.
  • Evidence: the time with its timezone, the hostname you called, and the error your HTTP client reported.

401 Unauthorized

The key is missing, malformed, unknown, revoked or expired. Errors are 401 UNAUTHORIZED with a WWW-Authenticate: Bearer header.

  • First checks: the header is Authorization: Bearer kirora_… with nothing else in it; the key has not passed its expiry date (new keys expire after 90 days unless you chose otherwise); it was not rotated without an overlap; you are not sending the web SDK’s client key (kirora_pk_…), which is not a /v1 credential.
  • Test the key with GET /v1/me, which any valid key may call: it returns the workspace and the key.
  • Evidence: the X-Request-Id and the key’s name or id. Never share the key itself.

403 Forbidden

A 403 always carries a code. Read it before changing anything:

code Cause Fix
SCOPE_MISSING The key lacks the operation’s scope. details.requiredScope names it Create a key that holds the scope
AGENT_KEY_REFUSED An agent key called an operation agent keys may not perform, whatever their scopes None needed: the refusal is by design, and a person does it in the dashboard. See Authentication
FORBIDDEN This caller may not do this: a key that is not a partner key sent Kirora-Workspace, a partner key was used from a browser, or an embed session called outside its routes Call from a server with the right key, or drop the header
API_KEY_IP_NOT_ALLOWED The request came from outside the key’s allowedIps Add the address, or call from an allowed server
PLAN_FEATURE_UNAVAILABLE The plan does not include the feature, such as goals on Free. details.entitlement names it Change plan
PLAN_LIMIT_REACHED A plan limit is used up. details.entitlement, limit and used say which; for events_per_month, the event is not stored and is listed in the event log Top up the prepaid balance, change plan, or wait for a monthly or daily limit to reset
WORKSPACE_SUSPENDED, WORKSPACE_DELETION_PENDING The workspace is suspended, or being deleted Ask its owner, or the partner that manages it, to resume it or cancel the deletion
EMBED_ORIGIN_NOT_ALLOWED An embed session was used from a page the partner key does not allow Add the page’s origin to the key’s allowedOrigins

A sending domain that is not ready is a 409, not a 403: SENDING_DOMAIN_UNVERIFIED (the from address is not on a verified domain: verify it, or send from one that is) and SENDING_DOMAIN_DMARC_MISSING (marketing email needs a DMARC record on the domain, p=none at least).

404 for a resource you know exists

A resource in another workspace returns 404, never 403.

  • First checks: the key belongs to the workspace that holds the resource (a staging key cannot read live data); a contact path uses Kirora’s id, or your own with ?idType=externalId; a partner request names the right child in Kirora-Workspace.
  • For a tracked event: the contact must already exist. Identify it first.

Accepted, but not received

A 202 means Kirora accepted and stored the message. It does not mean the message was sent or delivered.

  1. Read the message. Keep the id from the send’s response and call GET /v1/messages/{id}, or find it by your reference with GET /v1/messages?correlationId=….
  2. Read status, and skip or failure for why.
    • queued or sending: not handed over yet. A timed send waits for sendAt.
    • skipped with test_email: the address is a reserved test address (example.com and the like). It is never delivered.
    • skipped with another reason, such as suppressed or recipient_email_missing: a check stopped it. Every reason is in Transactional messages.
    • failed: the provider refused it, or a problem on Kirora’s side stopped it (not_sent, never caused by your side); failure.reason says which. It is not sent again.
    • bounced or complained: the address is now suppressed. bounce.type and bounce.detail say why a bounce bounced.
    • sent or delivered: it left Kirora. delivered means the recipient’s mail server accepted it, which does not show whether it reached the inbox or the spam folder.
  3. Check simulated. true means nothing was handed to a provider.
  • Evidence: the message id, its status, skip, failure or bounce, and the message.* webhook deliveries for it.

Part of a batch is missing

A batch returns 2xx when it was accepted as a whole. Each item has its own result.

  • First checks: read every entry of results; an entry with error was refused, with the same code a single call would have returned. The entry’s position is the item’s position in your request.
  • Evidence: the failed indexes and codes, your own ids for those items, and the X-Request-Id of the batch. See Batches.

Duplicates and identity conflicts

  • 409 CONFLICT from identify: the email or phone matched a contact that has a different externalId. Your two systems disagree about who this is.
  • 409 ALREADY_EXISTS with details.field: the value you are setting belongs to another contact.
  • Duplicates: usually a contact identified by a new email address without its externalId, which finds nobody and creates a second contact. Identify by externalId and merge the duplicate with mergeContacts.
  • Evidence: the identifiers you sent, details.field, and both contacts’ ids. See IDs and identity.

Retries

  • The retry did the work twice: the retry carried no Idempotency-Key, or a new one. Generate the key once, before the first attempt, and reuse it for every retry.
  • 409 IDEMPOTENCY_KEY_REUSED: the same key arrived with a different body, path or query. Repeat the original request exactly, or use a new key for a different action.
  • 409 IDEMPOTENCY_KEY_IN_USE: the first request with the key is still running. Wait a moment and retry with the same key.
  • 409 IDEMPOTENCY_KEY_OUTCOME_UNKNOWN: the first request with the key created or sent something, and failed or never returned, so it may have happened. Check (list the resources, or the messages), then retry with a new key if it did not.
  • A response with Idempotency-Replay: true is, for most writes, the stored first response, not a fresh read: read the resource for its state now. A transactional replay returns the message as it stands now. See Idempotency for each operation.
  • Evidence: the request ids of the original and of the retry, and the key with most of its characters hidden. See Idempotency.

Push not received

  • First checks: the contact’s consent.push is true (the device’s permission alone does not set it); the push project has a ready app; the contact has an active installation.
  • Installations: GET /v1/contacts/{id}/installations lists the contact’s devices with permission, active, lastSeenAt and invalidatedAt. A device not seen for 60 days is treated as gone.
  • The send: 409 PUSH_DESTINATION_MISSING means no active installation; 409 PUSH_APP_NOT_READY means no app can send; a skipped message with push_no_eligible_installations means no device could take it.
  • Each device: GET /v1/messages/{id}/deliveries shows what each device’s push service returned. accepted is not proof the device displayed it; invalid means the device is gone; rejected often means the app’s key or the payload was refused.
  • Evidence: the message id, the installation’s externalInstallationId, and the delivery results. See Push notifications.

Webhooks

Deliveries do not arrive:

  • GET /v1/webhooks/deliveries (listWebhookDeliveries) lists every attempt with its result; ?status=failed lists only what failed.
  • A paused or disabled webhook receives no new events. A webhook whose last retry fails, with nothing else accepted meanwhile, is disabled, and a 410 from your endpoint disables it at once.
  • While an endpoint keeps failing, deliveries are held; each pending delivery’s nextAttemptAt says when it is tried next.
  • Your endpoint must respond with a 2xx within 5 seconds. Redirects are not followed.
  • POST /v1/webhooks/{id}/test sends a webhook.test event; redeliverWebhookDelivery sends a past delivery again.

Signatures do not verify:

  • Compute the HMAC over the raw body exactly as received, not over parsed and re-serialised JSON.

  • Use the secret from the latest createWebhook or rotation response. After a rotation, the header carries a signature per active secret: accept any that verifies.

  • Sign ${X-Webhook-Timestamp}.${raw body}, and allow for your clock: refuse only timestamps more than five minutes away.

  • Evidence: the X-Webhook-Event-Id and X-Webhook-Attempt headers, your endpoint’s response, and the delivery’s entry in listWebhookDeliveries. See Webhooks.

Campaigns and journeys

A campaign does not send:

  • GET /v1/campaigns/{id}/eligibility lists every blocker and who is left out, without sending.
  • A large audience or an A/B test is queued in the background: the campaign reads sending until every message is queued, then sent. An A/B test reads testing once its versions are out, until a winner is chosen.
  • 409 AGENT_CONFIRMATION_REQUIRED: an agent wrote or edited it, and a person must confirm it in the dashboard.
  • 409 DRAFT_INCOMPLETE lists each missing field; 400 EMPTY_AUDIENCE means the audience has nobody in it.

A journey does not activate: GET /v1/journeys/{id}/activation-check lists every problem activation would refuse, with the field to fix and the error code, without activating.

A journey does not start for a contact:

  • The journey’s status is active, not draft or paused.

  • The event name matches the trigger’s name exactly, including case, and its properties meet the trigger’s where conditions.

  • The event was tracked within 72 hours of its occurredAt. Older events are recorded but start nothing.

  • The trigger’s reentry allows the contact in again (waitAfterEntry has passed since their last entry started), and maxEntries is not reached.

  • simulateJourney says whether the contact would enter, and why not; GET /v1/contacts/{id}/journeys lists their enrollments.

  • Evidence: the campaign’s or journey’s id and status, the eligibility or simulation result, and the contact’s id. See Campaigns and journeys.

Embedded builders

  • First checks: the session was created with testSendTo naming the address (up to 5); without any, the builder hides test sending. The session is less than 15 minutes old: an expired one returns 401, and your server should mint a new one. The page’s origin is in the partner key’s allowedOrigins.
  • 503 EMBED_BUILDER_UNAVAILABLE means the hosted builders are not available; retrying will not help.
  • Do not widen a session’s authority to work around a refusal. Give it the address it needs in testSendTo.

Size, format and rate

  • 413 PAYLOAD_TOO_LARGE: the body or file is over the route’s limit. See Limits.
  • 415 UNSUPPORTED_MEDIA_TYPE: send Content-Type: application/json, or multipart/form-data to an upload route.
  • 429 RATE_LIMITED: wait Retry-After seconds, then retry. A batch counts once per item. See Rate limits.

Asking for help

Send these, and nothing secret:

  • the X-Request-Id of the failing request, and of the original if it was a retry
  • the time, with its timezone
  • the operation (operationId or method and path)
  • your library and runtime versions, if you use one
  • the steps that reproduce it, and a minimal request with personal data removed

Never send an API key, a signing secret or a user token.

Next steps