Every message keeps an ordered event timeline: an append-only log of what happened to it, from the moment Sendara accepted it through delivery, opens, and any bounces or complaints. You can read the timeline on demand from the message resource, or subscribe to webhooksto receive each event as a POST the instant it's recorded.
The message lifecycle
A message moves through a small, predictable sequence of states. Each transition records an event, and the message's status field always reflects its most recent one. The happy path for a transactional email is:
queued → sent → delivered → opened
Any of three terminal failure signals can interrupt it: failed if the provider rejects the handoff, bounced if the receiving server refuses the message, and complained if the recipient later reports it as spam. Not every message emits every event — opened only fires when open tracking is enabled, and the failure signals may arrive seconds or hours after delivered, depending on the receiving server. The timeline is the source of truth; status is just a convenience snapshot of the latest entry.
Event types
Sendara normalizes every provider signal into one stable set of canonical types, so your code doesn't depend on the quirks of any upstream provider.
| Type | Terminal? | Meaning |
|---|---|---|
| queued | No | Sendara accepted the message and queued it for delivery. |
| sent | No | The message was handed off to the upstream mail provider. |
| delivered | No | The receiving mail server accepted the message. |
| opened | No | The recipient opened the email (requires open tracking; may fire more than once). |
| bounced | Yes | Delivery failed. A hard (Permanent) bounce suppresses the recipient automatically. |
| complained | Yes | The recipient marked the message as spam. The recipient is suppressed automatically. |
| failed | Yes | The provider rejected the message, or a delivery attempt failed irrecoverably. |
Transient) bounces do not suppress.Reading the timeline
Fetch a message with GET /v1/messages/{id} to get its current status plus the full ordered events array. Events are returned oldest-first (by occurred_at).
The response embeds the timeline directly on the message:
{
"id": "msg_a1b2c3",
"channel": "email",
"status": "delivered",
"message_type": "transactional",
"idempotency_key": "rcpt_9f21",
"provider_message_id": "0100018f-...-000000",
"created_at": "2026-06-14T10:00:00.000Z",
"updated_at": "2026-06-14T10:00:12.642Z",
"events": [
{
"id": "evt_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"type": "queued",
"message_id": "msg_a1b2c3",
"source": "worker",
"provider_event_id": "evt_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"payload": {},
"occurred_at": "2026-06-14T10:00:00.000Z",
"created_at": "2026-06-14T10:00:00.001Z"
},
{
"id": "evt_2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e",
"type": "sent",
"message_id": "msg_a1b2c3",
"source": "worker",
"provider_event_id": "evt_2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e",
"payload": { "provider_message_id": "0100018f-...-000000" },
"occurred_at": "2026-06-14T10:00:00.412Z",
"created_at": "2026-06-14T10:00:00.420Z"
},
{
"id": "evt_3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f",
"type": "delivered",
"message_id": "msg_a1b2c3",
"source": "ses",
"provider_event_id": "ses-delivery-77f2c1",
"payload": { "recipient": "user@acme.com" },
"occurred_at": "2026-06-14T10:00:12.481Z",
"created_at": "2026-06-14T10:00:12.642Z"
}
]
}The event object
Every entry in the timeline — and, after stripping the webhook envelope, every webhook delivery — has the same top-level shape:
id on a timeline event is the same value Sendara sends as the Sendara-Event-Id header and the event_id field on the matching webhook. Use it as the idempotency key in your webhook handler — an event can be delivered more than once, but its id never changes across retries.Per-event payload schemas
The payload object carries the detail that is specific to each event type. Fields also vary by source: events from the upstream provider (source: "ses") carry the provider's diagnostics, while synthetic sandbox events (source: "worker") carry a human-readable detail instead. The sections below list every field you may see.
queued & opened
These carry no type-specific detail; payload is an empty object {}. The event's occurred_at is the meaningful signal — for opened, the timestamp of the open.
sent
Emitted when the upstream provider accepts the handoff. The payload surfaces the provider's own message id, which is also written to the message's provider_message_id.
{
"type": "sent",
"source": "worker",
"payload": { "provider_message_id": "0100018f-...-000000" }
}delivered
Emitted when the receiving mail server accepts the message. For email this arrives as a provider callback (source: "ses") and carries the recipient. For synchronous channels such as outbound webhooks, delivery is recorded at send time (source: "worker") with a detail string.
{
"type": "delivered",
"source": "ses",
"payload": { "recipient": "user@acme.com" }
}bounced
Emitted when delivery fails. Read bounce_type to tell a hard bounce (Permanent — the address is dead and is suppressed) from a soft one (Transient — a temporary failure like a full mailbox, which does not suppress).
{
"type": "bounced",
"source": "ses",
"payload": {
"recipient": "user@acme.com",
"bounce_type": "Permanent",
"diagnostic": "smtp; 550 5.1.1 user unknown"
}
}complained
Emitted when a recipient reports the message as spam through their mail client. Complaints always suppress the recipient — stop sending marketing mail to anyone who complains.
{
"type": "complained",
"source": "ses",
"payload": { "recipient": "user@acme.com" }
}failed
Emitted when the provider rejects the message outright (for example an SES Reject) or a delivery attempt fails irrecoverably. Unlike a bounce, a failure usually means the message never left the pipeline.
{
"type": "failed",
"source": "worker",
"payload": { "error": "provider rejected: message content flagged" }
}Ordering & delivery guarantees
The timeline is built to be safe to consume from either side — polling the message resource or receiving webhooks — without losing or double-counting events.
- Append-only, ordered by
occurred_at. Events are never mutated or removed. The array fromGET /v1/messages/{id}is sorted ascending byoccurred_at. - At-least-once for webhooks. A provider can replay a callback, and a webhook delivery is retried on any non-
2xxresponse, so the same event may reach your endpoint more than once. Deduplicate onevent_id/idand make handlers idempotent. - Exactly-once in the timeline. Each underlying provider signal is recorded once — duplicate provider callbacks are collapsed by
provider_event_id— so reading the message resource always returns a clean, deduplicated history. - No global timestamp ordering across events. Because opens and bounces depend on remote servers, a later
occurred_atcan be recorded before an earlier one. Don't assume webhooks arrive in lifecycle order; reconcile against the timeline if exact order matters.
Receiving events as webhooks
To get events pushed to you in real time, register a webhook subscription and Sendara will POST each matching event to your URL, signed so you can verify it came from us. The webhook body wraps the event in a small envelope (event_id, event_type, message_id, the same payload shown above, and timestamps).
The full receiving guide lives on the Webhooks page:
- Create a subscription and choose which event types to receive.
- The request envelope & headers — the four
Sendara-*headers on every request. - Verifying signatures — the HMAC-SHA256 scheme with copy-paste Node and Python snippets.
- Retries & delivery guarantees — exponential backoff over a 24-hour window.
Driving events in the sandbox
Use a test key (sk_test_…) to exercise the whole send-to-webhook path without sending real email or being billed. The recipient address drives the simulated outcome, and your webhooks still fire with the same payload shape as production — so you can build and test your handler end to end before going live:
delivered@…→ aqueued→sent→deliveredsequencebounced@…→ abouncedevent (synthetic,source: "worker", with adetailstring)complained@…→ acomplainedevent
curl https://api.sendara.dev/v1/send \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"idempotency_key": "test_bounce_1",
"destination": { "email": "bounced@example.com" },
"payload": { "subject": "Test", "body_html": "<p>Test</p>" },
"metadata": { "from_email": "hello@yourdomain.com" }
}'Then poll GET /v1/messages/{id} to watch the timeline populate, or point a subscription at a tunnel (such as a local webhook inspector) to see the live deliveries. See the Sandbox & test sends page for the full list of simulated outcomes and verified test recipients.