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. The test event’s sample payload carries "carrier": "Verizon".
Carrier values
carrier (string or null) sits in payload next to to and from on message.sent, message.delivered, message.failed, and message.replied. It is null when the carrier is not known.
The three national networks always arrive as exactly AT&T, Verizon, or T-Mobile. Brands and subsidiaries are reported under the network they run on:
Any other non-null value is a regional carrier’s own registered name (for example
CELLULAR SOUTH, INC.); treat it as an opaque string.
message.sent
Triggered when a message is successfully accepted by the carrier. carrier is the recipient’s mobile carrier; see Carrier values.
message.delivered
Triggered when the carrier confirms the message was delivered to the recipient’s device. carrier is the recipient’s mobile carrier; see Carrier values.
sent_at is always present on message.delivered. When the carrier’s delivery receipt arrives before the send is recorded, it is the carrier-reported send time, or the delivery time if none was reported.
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. carrier is the recipient’s mobile carrier, or null when the message was rejected before it reached a carrier; see Carrier values.
message.replied
Triggered when an inbound SMS/MMS is received from a contact. carrier is the sender’s mobile carrier; see Carrier values.
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
These five event types can be subscribed on any endpoint alongside the message
events. They fire for organizations with email enabled.
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.
bounce_class is address when the mailbox does not exist or refuses all mail,
and policy when the receiving mail system refused this message for policy or
reputation reasons. Only address bounces are suppressed.
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.
Contact list events
contact_list.analyzed
Fires once when a line-type analysis run on a contact list finishes. It is opt-in: subscribe to it on the endpoint alongside the other events. Use it instead of polling GET /v1/contact-lists/{id} after POST /v1/contact-lists/{id}/analyze.
analyzed_url is an API URL, so download it with your X-API-Key (see GET /v1/contact-lists/{id}/download). It does not expire.