Every message Kirora sends, or decides not to send, is a row in the workspace’s message
log: one row per contact per channel, from campaigns, journeys and transactional sends
alike. This page covers reading the log, what each status means, and taking it as a
file. Every call here needs the messages:read scope.
Reading the log
GET /v1/messages (listMessages) lists the log newest first, a page at a time (see
Pagination). Each row is an OutboundMessage: the same object the
message.* webhooks carry. It never includes the message body.
| Filter | Returns |
|---|---|
status |
Messages in one status (below) |
channel |
email or push |
messageType |
marketing or transactional |
campaignId, journeyId |
Messages from one campaign or one journey |
stepId |
With journeyId, messages from the journey’s step with this id, in every version of its steps |
templateId |
Transactional sends from one stored template. Campaign and journey messages record no template |
correlationId |
Transactional sends that carried this reference of yours, such as an order number |
opened, clicked |
true or false: opened (a click counts) or clicked at least once, or never |
recipient |
The exact address a message went to, and email messages not yet given one whose contact’s current address is this one |
query |
Part of the recipient’s address or name, matched against the contact, up to 100 characters |
createdAfter, createdBefore |
Messages accepted at or after, and before, these times |
GET /v1/messages?campaignId=0192f3d3-5e6f-7071-8c83-9e0f1a2b3c4d&status=bounced&limit=100GET /v1/messages/{id} (getMessage) returns one row, whatever its source. Narrower
reads return the same rows:
| Operation | Returns |
|---|---|
listContactMessages |
One contact’s messages, by Kirora’s id or, with idType=externalId, yours; createdAfter and createdBefore bound them, as on the log |
getMessageContent |
What a transactional send contained, while it is kept. A campaign or journey message answers 404 CONTENT_NOT_RETAINED: its content is the campaign’s, or the journey version’s |
listMessageDeliveries |
For any push message, from a campaign, a journey or a transactional send: what each of the contact’s devices returned |
A campaign’s messages are GET /v1/messages?campaignId=…, and a transactional send’s
are GET /v1/messages?correlationId=…: one log, with every filter.
Lists return no total. To count outcomes, use a campaign’s stats or the reports.
Fields to read
{
"id": "0192f3c0-…",
"messageType": "marketing",
"channel": "email",
"status": "bounced",
"campaign": null,
"journey": { "id": "0192f3a4-…", "name": "Post-purchase thank you", "enrollmentId": "0192f3b0-…", "stepId": "thank-you-email" },
"template": null,
"contact": { "id": "0192f3a1-…", "externalId": "customer-881", "email": "[email protected]" },
"recipientEmail": "[email protected]",
"simulated": false,
"correlationId": null,
"sendAt": null,
"sentAt": "2026-10-24T09:30:58.000Z",
"deliveredAt": null,
"openedAt": null,
"openedByMachine": false,
"clickedAt": null,
"clickedByMachine": false,
"complainedAt": null,
"bounce": { "occurredAt": "2026-10-24T09:31:00.000Z", "type": "mailbox_not_found", "detail": "smtp; 550 5.1.1 user unknown" },
"failure": null,
"skip": null,
"createdAt": "2026-10-24T09:30:57.000Z"
}| Field | Holds |
|---|---|
status |
Where the message is now (below) |
campaign, journey |
Where it came from: { id, name } for a campaign, { id, name, enrollmentId, stepId } for a journey; name is the current one. Both are null for a transactional send |
contact |
Who it was for: { id, externalId, email }, the contact as it is now |
recipientEmail |
The address it went to, fixed at the first attempt; contact.email may since differ |
correlationId |
The reference you sent with a transactional message |
simulated |
true for a reserved test address: nothing was handed to a provider |
sentAt, deliveredAt, openedAt, clickedAt, complainedAt |
When each of those happened |
bounce |
Set on a bounced message: occurredAt, type (mailbox_not_found, mailbox_full, suppressed, or general for any other reason) and detail, the receiving server’s reply |
failure |
Set on a failed message: occurredAt and reason, such as provider_rejected or transient_bounce |
skip |
Set on a skipped message: occurredAt, reason (such as suppressed, consent_missing, test_email or cancelled) and, with suppressed, suppressionReason: why the address is blocked (unsubscribed, bounced, complained, complained_transactional, manual or imported) |
openedByMachine, clickedByMachine |
The open or click came from software, not a person |
Each of bounce, failure and skip is null unless status is its outcome, so a
complaint or a cancel never reads as a send failure. contact becomes null once the
contact is deleted or the message passes the workspace’s retention period: the row stays,
but no longer names anyone, and it no longer matches a recipient filter.
Statuses
status |
Meaning |
|---|---|
queued |
Accepted and stored. Nothing is sent yet, or it waits for sendAt |
sending |
Being handed to the email provider or push service |
sent |
Handed over. For push, the push service accepted it, which does not show the device displayed it |
delivered |
Email only. The recipient’s mail server accepted it, which does not show whether it reached the inbox |
bounced |
Email only. A permanent bounce; the address is now suppressed |
complained |
Email only. The recipient marked it as spam |
failed |
The provider refused it, the email soft-bounced, or a problem on Kirora’s side stopped it (not_sent); nothing in your content, audience or setup caused that, and Kirora is alerted. In rare cases the provider may have delivered it before the problem. It is not sent again |
skipped |
Not sent: a check stopped it or it was cancelled. skip.reason says why |
Opens and clicks do not change status: they set openedAt and clickedAt. Every
skip.reason and failure.reason is listed in
Transactional messages.
To follow outcomes as they happen, subscribe to the message.* webhooks rather than
polling the log.
Exporting
To take the whole send log, or a large part of it, ask for a file instead of paging
GET /v1/messages. The export takes the same filters as the list and applies them the
same way, including query, part of the recipient’s address or name, matched against the
contact, campaignId, journeyId or templateId for one source (templateId
matches transactional sends only: campaign and journey messages record no template), and
correlationId, so a file of receipts joins to your orders. opened and clicked are
the list’s only.
POST /v1/messages/exports { "status": "failed", "createdAfter": "2026-10-01T00:00:00Z" }
GET /v1/messages/exports/{id} → status: queued, generating, then ready, failed or expired
GET /v1/messages/exports/{id}/downloadSend {} to export every message. A filter that matches no message is refused with
400 NOTHING_TO_EXPORT. The file is built in the background, so poll the export until
status is ready. Downloading earlier returns 409 EXPORT_UNAVAILABLE (“not ready
yet”). A failed export returns the same 409 with details.status: "failed" and the
message “The export failed; create a new one”.
The file is CSV, one row per message, newest first, with the message’s fields as columns
and its objects flattened, every cell quoted and "" where a message has no value:
id,messageType,channel,status,campaignId,campaignName,journeyId,journeyName,journeyEnrollmentId,stepId,templateId,templateVersion,copyKind,copyOf,contactId,recipientEmail,simulated,correlationId,sendAt,sentAt,deliveredAt,openedAt,clickedAt,complainedAt,bounceOccurredAt,bounceType,bounceDetail,failureOccurredAt,failureReason,skipOccurredAt,skipReason,createdAt
"0192f3b0-4d5e-7f60-8b72-8d9e0f1a2b3c","transactional","email","bounced","","","","","","","0192f3f2-3c4d-7e5f-8061-7c8d9e0f1a2b","3","","","0192f3a1-7c2e-7d4b-9a10-3e5f6a7b8c9d","[email protected]","false","order-1001","","2026-10-01T09:31:00.000Z","","","","","2026-10-01T09:31:05.000Z","mailbox_not_found","smtp; 550 5.1.1 user unknown","","","","","2026-10-01T09:30:58.000Z"simulated lets you leave test sends out, and correlationId joins a row to your own
records. The file is kept for 7 days, given in expiresAt. After that the export reads expired and the
download returns 410 EXPORT_UNAVAILABLE. An export too large for one file fails with
failureCode EXPORT_TOO_LARGE; narrow the date range and ask again.
Any key of the workspace with messages:read can read an export, not only the key that
asked for it. The file holds recipient addresses, so grant the scope with care.