occurred_at, sent_at, delivered_at, failed_at, received_at, clicked_at) are ISO-8601 UTC with millisecond precision and a Z suffix. Phone numbers (to, from) are E.164 strings. The envelope owns the event identity (id, occurred_at); the event-specific payload carries only event fields and resource references (message_id, tracking_link_id).
Identifiers
data.id uniquely identifies the event, and together with event_type it is the delivery deduplication key — if you receive the same (event_type, id) pair twice, treat the second as a redelivery. It also arrives in the X-Event-ID header.
- Message events (
message.sent,message.delivered,message.failed):idequalsmessage_id— the message is the event identity, since a message emits at most one event of each type. The sameidtherefore recurs across one message’s lifecycle (message.sentthenmessage.delivered), distinguished byevent_type. message.replied:idequalsmessage_idand identifies the inbound message (the reply itself), not the outbound message it responds to. Correlate viaconversation_id.link.clicked:idis a unique per-click identifier — every unique click delivers its own event.tracking_link_ididentifies the link and recurs across clicks of that link.
Test events
Message events includeis_test (boolean). It is true for test messages — sent from the app’s test flow or via POST /v1/projects/{id}/test — and for the synthetic event produced by the dashboard’s Send test webhook button (which additionally corresponds to no real message: its ids are random). Production traffic always carries is_test: false. link.clicked has no is_test field.
message.sent
Triggered when a message is successfully accepted by the carrier.
message.delivered
Triggered when the carrier confirms the message was delivered to the recipient’s device.
message.failed
Triggered when the carrier rejects the message or delivery fails. error_code is a short delivery-failure code (e.g. "300" opted-out, "012" invalid destination) paired with a human-readable error_message. The full list of codes, messages, and how to react to each lives in Delivery error codes. Fields describing a message that was accepted before failing (sent_at, message_text) may be absent when the message was rejected at send time.
message.replied
Triggered when an inbound SMS/MMS is received from a contact.
message.replied event with your own text, call POST /v1/conversations/{conversation_id}/messages; the reply goes out from the same number inside the same thread and its outcome arrives on message.sent / message.delivered / message.failed. See Two-Way Conversations.
link.clicked
Triggered when a recipient clicks a tracking link in your message.
What “unique” means
is_unique_click is true the first time a given device fingerprint clicks a
particular tracking link, and false for that fingerprint’s later clicks on the
same link.
The fingerprint is derived from the clicker’s IP address and User-Agent — it
identifies a device on a network, not a person. Two consequences worth planning
around if you aggregate these events yourself:
- One person who taps a link on cell data and again on Wi-Fi produces two unique clicks, because the IP changed.
- Two people behind one shared connection (a household or office NAT) using the same device model and browser can collapse into one unique click.
is_bot: true and are excluded from the
click totals shown in campaign reports.
Email events (early access)
The email product is in early access. These five event types are already in the
registry and can be subscribed on any endpoint, but no email traffic is sent
until the product reaches general availability. The
/v1/email/* API answers
403 EMAIL_EARLY_ACCESS in the meantime.data.event_type,
data.id, data.occurred_at, and data.payload, with meta.attempt. There
are only five, and each one is deliberate:
There is no
email.opened: open tracking is a pixel, which makes it both
approximate and, at campaign volume, the highest-rate event we could send. There
is no inbound email event either, because the product has no inbox.
Test sends and seed addresses never emit email webhooks.
Deduplication
Every email payload carries a stableid that is also the dedup key, so a
redelivery of the same event reuses the same value:
The HTML body of a campaign is never included in a webhook payload.
email.delivered
email.bounced
Only permanent bounces fire. bounce_type and bounce_subtype come from the
mailbox provider; reason is the SMTP diagnostic code when one is supplied.
email.complained
reason is the provider’s feedback type (for example abuse), and is null
when the provider does not supply one.
email.unsubscribed
scope says what the recipient opted out of, and reason is one_click for an
RFC 8058 one-click unsubscribe or the reason chosen on the hosted preferences
page.
email.clicked
Fires once per recipient per link. Repeat clicks on the same link by the same
recipient do not fire again, and clicks we identify as automated (link scanners,
security crawlers, preview fetchers) never fire at all.
occurred_at is the moment of the click
rather than the moment of delivery. device_type, browser, and os are
best-effort and may be null.