Skip to main content
Every webhook event has a consistent envelope:
All timestamps (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): id equals message_id — the message is the event identity, since a message emits at most one event of each type. The same id therefore recurs across one message’s lifecycle (message.sent then message.delivered), distinguished by event_type.
  • message.replied: id equals message_id and identifies the inbound message (the reply itself), not the outbound message it responds to. Correlate via conversation_id.
  • link.clicked: id is a unique per-click identifier — every unique click delivers its own event. tracking_link_id identifies the link and recurs across clicks of that link.

Test events

Message events include is_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.
Triggered when a recipient clicks a tracking link in your message.