Messages
A message is one email to one recipient. Every send creates exactly one message per recipient: a transactional send, an OTP code, one recipient’s copy of a campaign, one automation send step for one contact, or one address of a template test send. Each message carries a delivery status that moves from acceptance to a final state, and a short event history. GET /messages searches your messages; GET /messages/{id} returns one message together with its events.
Key fields
| Field | Description |
|---|---|
id |
The message identifier. Transactional and OTP responses return it as message_id, and every webhook event for the message carries it. |
campaign_id |
The campaign or automation that produced the message; null for transactional, OTP and template test sends. |
source_type |
What produced the message: blast, automation, transactional or otp. null for a template test send. See Send paths and message class. |
journey_step_id |
For source_type: automation, the id of the send step that produced the message; null otherwise. |
template_id |
The template that rendered the message. null for a message that carries its own content instead of a template. |
status |
The delivery status. See Statuses. |
provider_message_id |
The identifier recorded when the message was handed to the mail provider; null until then. For a domain on kumomta it is the message’s own id. |
error_message |
The last failure or deferral reason as a machine-readable code (for example defer_cap_exceeded), or null. After a bounce it carries the provider’s bounce reason when one was reported. |
send_at |
For a scheduled transactional send, the requested send time; null for an immediate send. |
tags |
Tags supplied on the send request, normalised: trimmed, lower-cased, de-duplicated and sorted. Empty array when none were sent. Filter on them with GET /messages?tag=. |
metadata |
Key-value strings supplied on the send request and returned verbatim. Empty object when none were sent. |
created_at |
When the message was accepted. |
Tags and metadata are accepted by POST /transactional/send, POST /transactional/send-batch (for the whole batch, not per item) and POST /otp/request. A request may carry at most 10 tags, each matching ^[a-z0-9_.:-]{1,64}$ after normalisation, and metadata of at most 4096 bytes when serialised as JSON; anything else is 422. Both are returned on the message and in its webhook events.
Send paths and message class
The send path decides a message’s source_type and its message class, and each path requires a template of the matching class:
| Send path | source_type |
Message class | Template must be |
|---|---|---|---|
POST /transactional/send, POST /transactional/send-batch |
transactional |
transactional |
active, class transactional |
POST /otp/request |
otp |
critical |
active, class critical |
Campaign (blast) |
blast |
marketing |
active, class marketing |
Automation send step |
automation |
marketing |
active, class marketing |
POST /templates/{id}/test-send |
null |
the template’s own class | any status or class |
A template of the wrong class is refused with 422 and the validation message template_class_mismatch; a template that is not active is refused with template_not_active. See Templates.
The class changes how the message is treated:
- Priority. When sending capacity is contended,
criticalmail goes first, thentransactional, thenmarketing. - Tracking and unsubscribe. Only
marketingmail gets open and click tracking and a signed unsubscribe link ({{unsubscribe_url}}). Transactional and OTP mail is not tracked, so it never producesopenorclickevents. - Suppression.
marketingmail is blocked by every suppression reason.transactionalandcriticalmail ignoresunsubscribesuppressions and is blocked only bybounce,complaintandmanualones — a contact who unsubscribed from marketing still receives receipts and login codes. See Suppressions.
When suppression is checked
POST /transactional/sendchecks the recipient when you call it and refuses any suppressed address, whatever the reason, with403anddetails.code: recipient_suppressed; no message is created.POST /transactional/send-batchreports the same code in that item’sresultsentry.- Every other path, including OTP, creates the message and checks the suppression list again just before hand-off. A blocked message ends in
suppressed, and the request that created it does not fail.
Statuses
status has ten values.
| Status | Meaning |
|---|---|
queued |
Accepted and waiting to be sent. A scheduled transactional send (send_at in the future) stays here until its time arrives. A message also returns to queued, with the error in error_message, after a temporary provider error; it is retried automatically with increasing delays. |
sending |
Being handed to the mail provider right now. |
sent |
The mail provider accepted the message for delivery. This is not yet a delivery confirmation. |
delivered |
The provider reported that the recipient’s mail server accepted the message. A later hard bounce or a complaint can still replace this status. |
bounced |
The provider reported a bounce. Both hard (permanent) and soft (temporary, such as a full mailbox) bounces set this status. Only a hard bounce, or repeated soft bounces for the same address, adds the address to the suppression list — see Suppressions. |
complained |
The recipient reported the message as spam through their mailbox provider. The address is added to the suppression list with reason complaint. |
failed |
The message could not be sent and will not be retried. error_message names the cause, for example defer_cap_exceeded (held back too many times, see deferred), domain_not_verified, domain_not_found or provider_not_allowed (the sending domain stopped being usable after the message was queued), render_failed, kumo_outcome_unknown (see below), a provider rejection, or a scheduled send that was not picked up in time. |
suppressed |
The recipient was on the suppression list when the message was about to be handed off, so it was never sent. |
deferred |
The platform held the message back because a sending limit was reached — the domain’s warmup allowance for the day or an hourly volume limit. It is retried automatically: about once a minute for the warmup allowance, and when the hour rolls over for an hourly limit. After 10 such deferrals the message becomes failed with defer_cap_exceeded and its credit is refunded; a message held back by the daily warmup allowance is not kept waiting for the next day. |
canceled |
A scheduled transactional send was canceled with DELETE /transactional/{message_id} before it was picked up, or the message’s campaign was canceled before the message was handed off (error_message: campaign_canceled). |
Final statuses are delivered, bounced, complained, failed, suppressed and canceled; delivered and failed can still be replaced by a later bounce or complaint, as described in How delivery events change the status.
A delay that the mail provider reports after the message is sent does not change the status. It adds a deferral event and, if you subscribe to it, an email.deferred webhook; the provider keeps retrying on its own.
kumo_outcome_unknown means the platform could not tell whether the provider accepted the message (for example, the connection dropped mid-request). To avoid sending a duplicate, the message is not retried and is marked failed, but it may still be delivered. A later delivery report does not change the status; a later bounce or complaint does.
Lifecycle
queued ──► sending ──► sent ──► delivered
▲ │ │ │
│ │ │ └──► bounced (hard) / complained
│ │ └──► bounced / complained / failed (provider rejection)
│ ├──► suppressed
│ ├──► failed (permanent error before hand-off)
│ ├──► deferred ──► retried ──► ... ──► failed (defer_cap_exceeded)
└───────────┘ (temporary provider error: back to queued, retried)
queued ──► canceled (scheduled send deleted, or campaign canceled)
queued ──► failed (scheduled send not picked up in time)
- A send request, a campaign launch, or an automation step creates the message in
queued. A transactional send with a futuresend_at(at most 30 days ahead) waits inqueueduntil that time;DELETE /transactional/{message_id}cancels it while it is still waiting. - When the message is picked up it moves to
sending. The platform checks that the sending domain is still verified and allowed, checks the suppression list, and claims capacity: the domain’s warmup allowance and the hourly volume limits. A suppressed recipient ends insuppressed; exhausted capacity moves the message todeferred; an unusable domain or a rendering error ends it infailed. - A successful hand-off to the mail provider sets
sent. A temporary provider error returns the message toqueuedand retries it with increasing delays; after 10 such retries it becomesfailedwithdefer_cap_exceeded. - Delivery reports from the provider then set
delivered,bounced,complainedorfailed. Opens and clicks never change the status.
How delivery events change the status
Reports can arrive late and out of order, so the status only ever moves to a stronger outcome:
- A delivery sets
delivered. - A hard bounce sets
bounced, even on a message that is alreadydeliveredorfailed. - A soft bounce sets
bouncedunless the message is alreadydelivered. - A complaint sets
complained, even on adeliveredorbouncedmessage. - A provider rejection sets
failedunless the message is alreadydelivered. - Opens, clicks and provider delays record an event but never change the status.
- Nothing replaces
canceledorcomplained.
Credits and refunds
Every message costs one credit, reserved when the message is created. The credit is refunded only when the message fails before it is handed to the provider: suppressed, canceled, and failed for reasons such as defer_cap_exceeded, an unusable domain, a rendering error, or a scheduled send that expired. Once the provider has accepted the message, the credit is spent whatever happens next — bounced, complained and a provider rejection are not refunded. A kumo_outcome_unknown failure is not refunded at once, because the message may have been accepted. Requests refused before a message exists (for example 402, 403 or 422) cost nothing. See Billing & Credits.
Message events
GET /messages/{id} returns an events array. Each entry has a type — delivery, open, click, bounce, complaint, reject or deferral — and a timestamp at. Event types are spelled differently from statuses: branch on delivery, not delivered.
Each event type appears at most once per message: a repeated open or click, or a second delay report, updates the existing entry instead of adding a new one. The response includes up to the 50 most recent entries. The same facts drive the message status and the webhook events you can subscribe to.
Finding messages
GET /messages is cursor-paginated and accepts these filters:
campaign_id— messages of one campaign or automation.status— one status value.from/to— a window oncreated_at, both inclusive.email— a case-insensitive match on any part of the recipient address.tag— one tag, normalised the same way as on send.transactional=true— only messages without acampaign_id(transactional, OTP and template test sends), whencampaign_idis not given.
Messages are kept for your account’s message-log retention period; older messages are no longer returned.
Related API
- Email tag —
POST /transactional/send,POST /transactional/send-batch,DELETE /transactional/{message_id}. - OTP tag —
POST /otp/request,POST /otp/verify. - Messages tag —
GET /messages,GET /messages/{id}.
See the API Reference for full schemas, and Send a transactional email and Send OTP codes for worked examples.