Webhook Events
Surge sends 13 event types. All events arrive at your configured webhook endpoint as HTTP POST requests with a JSON body. Voice events (call.ended, recording.completed, voicemail.received) are flag-gated and only sent to accounts with voice enabled.
See Receiving Messages & Webhooks for endpoint configuration and signature verification.
Event structure
Every event has the same top-level shape:
{
"type": "message.received",
"data": { ... }
}The type field tells you what happened. The data field contains the resource that changed.
Message events
message.sent
Fires when an outbound message is accepted by the carrier network. Includes sent_at (ISO 8601 timestamp), the full conversation thread, and any attachments.
{
"type": "message.sent",
"data": {
"id": "msg_01jjnn7s0zfx5tdcsxjfy93et2",
"blast_id": null,
"body": "Your order has shipped.",
"metadata": {},
"attachments": [],
"sent_at": "2024-10-21T23:29:41Z",
"conversation": {
"id": "cnv_01jav8xy7fe4nsay3c9deqxge9",
"phone_number": {
"id": "pn_01jsjwe4d9fx3tpymgtg958d9w",
"number": "+18015556789",
"type": "local"
},
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
}
}
}
}message.delivered
Fires when the carrier confirms delivery to the handset. Not all carriers send delivery receipts — if you don't receive this event, it doesn't necessarily mean delivery failed.
{
"type": "message.delivered",
"data": {
"id": "msg_01jjnn7s0zfx5tdcsxjfy93et2",
"blast_id": null,
"body": "Your order has shipped.",
"metadata": {},
"attachments": [],
"delivered_at": "2024-10-21T23:29:42Z",
"conversation": {
"id": "cnv_01jav8xy7fe4nsay3c9deqxge9",
"phone_number": {
"id": "pn_01jsjwe4d9fx3tpymgtg958d9w",
"number": "+18015556789",
"type": "local"
},
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
}
}
}
}message.failed
Fires when a message cannot be delivered. The failure_reason field explains why.
{
"type": "message.failed",
"data": {
"id": "msg_01jjnn7s0zfx5tdcsxjfy93et2",
"blast_id": null,
"body": "Your order has shipped.",
"metadata": {},
"attachments": [],
"failed_at": "2024-10-21T23:29:42Z",
"failure_reason": "carrier_error",
"conversation": {
"id": "cnv_01jav8xy7fe4nsay3c9deqxge9",
"phone_number": {
"id": "pn_01jsjwe4d9fx3tpymgtg958d9w",
"number": "+18015556789",
"type": "local"
},
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
}
}
}
}message.received
Fires when an inbound message arrives on one of your numbers. This is how you implement two-way messaging — your handler receives the conversation.id and the body, and you can reply by sending a message to the same conversation.
{
"type": "message.received",
"data": {
"id": "msg_01jav96823f9x9054d6gyzpp16",
"blast_id": null,
"body": "What are your hours?",
"metadata": {},
"attachments": [],
"received_at": "2024-10-21T23:29:43Z",
"conversation": {
"id": "cnv_01jav8xy7fe4nsay3c9deqxge9",
"phone_number": {
"id": "pn_01jsjwe4d9fx3tpymgtg958d9w",
"number": "+18015556789",
"type": "local"
},
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
}
}
}
}Contact events
contact.opted_in
Fires when a contact sends a recognized opt-in keyword. Surge recognises these exact words (case-insensitive):
| Keyword | Notes |
|---|---|
START | Standard re-opt-in keyword |
YES | Common confirmation keyword |
UNSTOP | Used to re-subscribe after a STOP |
{
"type": "contact.opted_in",
"data": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk"
}
}When this event fires, the conversation status returns to active and you can send to the contact again.
contact.opted_out
Fires when a contact sends a recognized opt-out keyword. Surge recognises these exact words (case-insensitive):
| Keyword | Notes |
|---|---|
STOP | Standard carrier opt-out keyword |
STOPALL | Variant of STOP |
UNSUBSCRIBE | Common unsubscribe keyword |
CANCEL | Alternative opt-out keyword |
END | Alternative opt-out keyword |
QUIT | Alternative opt-out keyword |
OPTOUT | Alternative opt-out keyword |
REVOKE | Alternative opt-out keyword |
Once opted out, sending to this contact returns an opted_out error. Do not attempt to re-subscribe contacts without their explicit new consent — wait for them to send an opt-in keyword or otherwise affirmatively re-consent.
{
"type": "contact.opted_out",
"data": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk"
}
}Opt-out state is per-conversation, not per-contact. A contact can opt out of messages from one of your numbers while remaining active on another. This matters for multi-number accounts: a contact who texts STOP to
+18015556789can still receive messages from+18025557890if they haven't opted out of that thread.
Conversation events
conversation.created
Fires when a new conversation thread starts — either when a contact sends you their first message, or when you initiate a conversation with a contact for the first time. The event includes the phone number and contact on the new thread.
{
"type": "conversation.created",
"data": {
"id": "cnv_01jav8xy7fe4nsay3c9deqxge9",
"phone_number": {
"id": "pn_01jsjwe4d9fx3tpymgtg958d9w",
"number": "+18015556789",
"type": "local",
"campaign_id": null
},
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
}
}
}Campaign events
campaign.approved
Fires when a campaign's status changes to active — meaning it's been approved by the US carriers and is cleared for production sending. Surge automatically attaches any existing phone numbers on the account to the newly active campaign.
{
"type": "campaign.approved",
"data": {
"id": "cpn_01jrzhe8d9enptypyx360pcmxl",
"status": "active"
}
}Phone number events
phone_number.attached_to_campaign
Fires when a phone number is successfully attached to a campaign. Once you receive this event, the number is ready for production traffic.
{
"type": "phone_number.attached_to_campaign",
"data": {
"id": "pn_01jrzhe8d9enptypyx360pcmxm",
"number": "+18015559876",
"campaign_id": "cpn_01jrzhe8d9enptypyx360pcmxl"
}
}Link events
link.followed
Fires when a recipient clicks a tracked link in a message. Only fires on the first click — subsequent clicks on the same link by the same contact don't trigger additional events.
Link tracking requires link shortening to be enabled on your account. When enabled, Surge replaces the original URLs in your messages with short links at Surge's link-shortener domain. When a recipient clicks one of these links, Surge fires the link.followed event and then redirects them to the original URL. Contact support to enable link shortening.
Note: If link shortening is not enabled on your account, no
link.followedevents will fire, even if your messages contain URLs.
{
"type": "link.followed",
"data": {
"id": "lnk_01kedctzhxexdbr5xf2bht5q84",
"message_id": "msg_01jjnn7s0zfx5tdcsxjfy93et2",
"message": {
"id": "msg_01jjnn7s0zfx5tdcsxjfy93et2",
"blast_id": null,
"body": "Track your order: https://acme.com/track/12345",
"metadata": {},
"attachments": [],
"conversation": {
"id": "cnv_01jav8xy7fe4nsay3c9deqxge9",
"phone_number": {
"id": "pn_01jsjwe4d9fx3tpymgtg958d9w",
"number": "+18015556789",
"type": "local"
},
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
}
}
},
"url": "https://acme.com/track/12345"
}
}The message_id top-level field is deprecated. Use message.id instead. See Deprecation Notices.
Voice events (flag-gated)
These events are only sent to accounts with voice enabled.
call.ended
Fires when a voice call completes.
{
"type": "call.ended",
"data": {
"id": "call_01jjnn7s0zfx5tdcsxjfy93et2",
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
},
"duration": 142,
"initiated_at": "2025-03-31T21:01:37Z",
"status": "completed"
}
}recording.completed
Fires when a call recording is processed and ready for download.
{
"type": "recording.completed",
"data": {
"id": "rec_01kfyc9dgdec1avkgs7tng8htg",
"duration": 124,
"call": {
"id": "call_01jjnn7s0zfx5tdcsxjfy93et2",
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
},
"duration": 184,
"initiated_at": "2025-03-31T21:01:37Z",
"status": "completed"
}
}
}voicemail.received
Fires when a voicemail is left on one of your numbers and the recording is ready.
{
"type": "voicemail.received",
"data": {
"id": "vml_01kfycac7rery81axwmpzcgpp8",
"recording_id": "rec_01kfyc9dgdec1avkgs7tng8htg",
"duration": 124,
"call": {
"id": "call_01jjnn7s0zfx5tdcsxjfy93et2",
"contact": {
"id": "ctc_01ja88cboqffhswjx8zbak3ykk",
"first_name": "Dominic",
"last_name": "Toretto",
"phone_number": "+18015551234"
},
"duration": 184,
"initiated_at": "2025-03-31T21:01:37Z",
"status": "completed"
}
}
}