send(), you get back a
message id and status: queued immediately — but the outcomes that matter
(delivery confirmation, bounces, opt-outs) happen seconds or minutes later inside
the provider. Webhooks let SenderKit push those outcomes to your backend the moment
they arrive, rather than making you poll. The same channel also delivers mail
sent to your inbound addresses.
Events
Webhooks fire only for asynchronous outcomes you can’t predict from the API response. Internal pipeline states (queued, rendered) are not emitted.
message.opened and message.clicked are engagement signals, not lifecycle
states — they never change a message’s status. delivered remains the
terminal happy-path status; an open or click can arrive seconds or minutes
after it. Both fire only once, on the message’s first reported open/click.message.opted_out and message.complained are consent/reputation signals,
not lifecycle states — like opens and clicks, they don’t retroactively change
a message’s status. A message that was already delivered (or failed)
when the recipient unsubscribes or complains keeps that status; only a send
skipped before dispatch because the recipient had already opted out gets
the opted_out status itself (see Messages).
A spam complaint fires both message.complained and message.opted_out; a
plain unsubscribe fires message.opted_out alone.message.suppressed fires on managed (AWS SES) sending only, when the
recipient address fails SES validation or is already on the account’s
suppression list — the send is skipped before it ever reaches a provider,
and the message lands in the terminal suppressed status. It’s distinct
from message.failed, which reports a real bounce from the receiving mail
server after an actual delivery attempt. Bring-your-own-provider connections
never emit message.suppressed.Setting up an endpoint
- Open Webhooks from the sidebar in your dashboard (
/app/webhooks). - Click Add endpoint and paste your HTTPS URL.
- Copy the signing secret shown after creation — it is displayed only once and cannot be retrieved later.
- Choose which events to subscribe to (or leave all selected to receive everything).
- Click Send test event to confirm your endpoint receives and verifies the payload correctly before going live.
Webhooks deliver to live mode endpoints only. In test mode, delivery is
simulated in-process — no real HTTP requests are made to your endpoint.
Payload
Every event is aPOST with Content-Type: application/json. The body follows a
consistent envelope:
data.message is a public projection of the message — it omits rendered HTML,
template variables, and internal provider message IDs. openedAt / clickedAt
are set once, on the message’s first reported open/click; a message.clicked
event additionally carries the clicked URL as data.link:
message.received payloads
message.received carries a different data.message shape — a received
message, not a send. It’s only emitted for mail that matched an active
inbound address; unmatched or quota-exceeded mail is
recorded but never delivered as a webhook.
attachments[].url and rawUrl are authenticated API links, not signed
public URLs — fetch them with an Authorization: Bearer header carrying an
API key with the inbound scope.
Verifying signatures
Every webhook request carries three headers:
The signature format is:
HMAC-SHA256(key=<signing-secret>, data="<timestamp>.<raw-body>")
and compare with the v1 value. Reject the event if the signature doesn’t match or
if the timestamp is more than 5 minutes old.
Verification example (Node.js)
Express example
Retries and delivery logs
SenderKit retries failed deliveries automatically on any non-2xx response or
network error. Each endpoint retries independently — a slow or unavailable endpoint
does not block delivery to your other endpoints.
You can inspect delivery history in the Webhooks dashboard. Each endpoint shows
recent attempts, HTTP status codes, response times, and whether retries are pending.
Messages
The message lifecycle and the statuses that trigger webhook events.
Inbound Email
Provision addresses and receive mail as a
message.received webhook.