Skip to main content
Every send creates a message — the record of truth for what SenderKit did with it. A message captures the recipient, the channel, the variables, which provider handled it, and a timeline of everything that happened. Because sending is asynchronous, the message is how you find out whether delivery actually succeeded.

The message lifecycle

StatusWhat it meansWhat triggers it
scheduledAccepted but holding until scheduledAtA send() with a future scheduledAt
queuedAccepted and ready to dispatchThe send() call, or a scheduled message reaching its fire time
renderedTemplate + variables resolvedBackground dispatcher
sentA provider accepted the messageSuccessful provider dispatch
deliveredThe provider confirmed deliveryProvider webhook
failedDelivery failedProvider webhook, a config error, or retries exhausted
opted_outRecipient is unsubscribed/suppressedProvider webhook, one-click unsubscribe link, or send skipped because the recipient previously opted out
blockedDelivery halted by outbound abuse detectionPhishing scan flagged the content with high confidence; the message is never handed to a provider. The message timeline records a generic notice; detection details are operator-only and not exposed via the customer API.
canceledDelivery intentionally stopped before dispatchmessages.cancel() or the dashboard cancel action
The status enum also includes dispatched. In the current pipeline a message moves queued → rendered → sent, and the moment a provider accepts it is recorded as a dispatched event on the message’s timeline while the status itself becomes sent. Read sent as “handed to the provider.”
In test mode, SenderKit synthesizes this lifecycle (rendered → sent → delivered) without calling a provider, so your logs look the same as live without anything leaving the building.

Querying messages

List messages newest-first with cursor pagination, and filter by status, channel, template, or your own metadata:
const { data, nextCursor } = await senderkit.messages.list({
  status: "failed",
  channel: "email",
});
The API also supports a live tail (a Server-Sent Events stream) for watching sends in real time. See the API Reference, the TypeScript SDK, and the CLI for the full surface.
Attach metadata (e.g. { userId: "usr_123", orderId: "ord_9" }) when you send. It’s indexed, so you can later filter messages down to a single user or order.

Engagement (opens & clicks)

For email, SenderKit also records provider-reported opens and link clicks as openedAt / clickedAt on the message — each set once, on the first occurrence. These are engagement signals, not lifecycle states: they never change a message’s status, and delivered remains the terminal happy-path status regardless of whether the recipient later opens it or clicks a link. Subscribe to the message.opened / message.clicked webhook events to be notified as they happen, or read the fields directly via messages.get/messages.list.

Retention

Messages are retained for a limited window and then deleted:
PlanRetention
Free3 days
Starter30 days
Pro90 days
Don’t treat SenderKit as your long-term audit log. If you need history beyond your plan’s retention window, export or persist messages in your own system, and use metadata to correlate them with your records.

Sending

How a message gets created and dispatched.

Channels & Providers

Who delivered the message and how failures surface.