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, critical mail goes first, then transactional, then marketing.
  • Tracking and unsubscribe. Only marketing mail gets open and click tracking and a signed unsubscribe link ({{unsubscribe_url}}). Transactional and OTP mail is not tracked, so it never produces open or click events.
  • Suppression. marketing mail is blocked by every suppression reason. transactional and critical mail ignores unsubscribe suppressions and is blocked only by bounce, complaint and manual ones — a contact who unsubscribed from marketing still receives receipts and login codes. See Suppressions.

When suppression is checked

  • POST /transactional/send checks the recipient when you call it and refuses any suppressed address, whatever the reason, with 403 and details.code: recipient_suppressed; no message is created. POST /transactional/send-batch reports the same code in that item’s results entry.
  • 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)
  1. A send request, a campaign launch, or an automation step creates the message in queued. A transactional send with a future send_at (at most 30 days ahead) waits in queued until that time; DELETE /transactional/{message_id} cancels it while it is still waiting.
  2. 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 in suppressed; exhausted capacity moves the message to deferred; an unusable domain or a rendering error ends it in failed.
  3. A successful hand-off to the mail provider sets sent. A temporary provider error returns the message to queued and retries it with increasing delays; after 10 such retries it becomes failed with defer_cap_exceeded.
  4. Delivery reports from the provider then set delivered, bounced, complained or failed. 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 already delivered or failed.
  • A soft bounce sets bounced unless the message is already delivered.
  • A complaint sets complained, even on a delivered or bounced message.
  • A provider rejection sets failed unless the message is already delivered.
  • Opens, clicks and provider delays record an event but never change the status.
  • Nothing replaces canceled or complained.

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 on created_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 a campaign_id (transactional, OTP and template test sends), when campaign_id is not given.

Messages are kept for your account’s message-log retention period; older messages are no longer returned.

  • 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.