How to Receive Webhooks

Configure your application to receive real-time webhook notifications for SMS, SMS Flash, Voice, and RCS: delivery status updates, replies to your SMS, RCS reply-button postbacks, and the review decisions on your RCS sender brand.

Overview

LigueLead automatically sends webhooks to notify your application when something happens on your account. Most of it is campaign events, across all channels: SMS, SMS Flash, Voice, and RCS. One event is not about a campaign at all — rcs.agent.status reports the review of your RCS sender brand, which exists before any send. Webhooks enable you to:

  • Monitor in real-time the status of your deliveries and calls
  • Implement retry logic for delivery or call failures
  • Keep synchronization between your system and LigueLead
  • Collect metrics on delivery, call performance, and credits consumed
  • React to user interactions with RCS reply buttons (postback events) and replies to your SMS
  • Know when your RCS brand is approved without polling for it

Webhook URL Configuration

Client Area Access

  1. Access https://areadocliente.liguelead.app.br/
  2. Log in with your credentials
  3. Navigate to Integrations > API Token
  4. Locate the "Webhook URL" section
  5. Enter the complete URL of your webhook endpoint
  6. Click "Save"
📌

The app's webhook URL receives notifications for all channels (SMS, SMS Flash, Voice, and RCS) and all campaign event types (delivery status, SMS replies and RCS postbacks). Route incoming requests by inspecting the event field and, for delivery events, the campaign.type field. To send the status of a specific send somewhere else, pass webhook_url on that send — see below.

Per-send webhook URL

Every send endpoint (POST /v1/sms, POST /v1/rcs, POST /v1/voice and POST /v1/voice-agent/call) accepts an optional webhook_url. When it is present, the status events of that send (campaign.status) go to it instead of the app's webhook URL — the same model as the Twilio StatusCallback.

The URL is called exactly as you wrote it, query string included, so use it to carry your own identifiers and match each status to your records:

{
  "message": "Your order has shipped",
  "phones": ["11999999999"],
  "webhook_url": "https://api.mycompany.com/webhooks/liguelead?order=123"
}
  • Only campaign.status follows it. SMS replies (sms.inbound), RCS button postbacks and rcs.agent.status keep their usual destination.
  • A send without webhook_url behaves as before.
  • It must use http or https, have a public hostname without underscores and be at most 512 characters. Private and reserved addresses (localhost, 10.x, 192.168.x, 169.254.x and the like) are refused with 422, like every other validation error of the API.
  • No authentication header is sent to it; if you need to verify the caller, put a token of your own in the query string.
  • Redirects are not followed: the URL itself has to answer with a 2xx status. A 3xx answer is not retried, since the endpoint may already have processed the request before redirecting.
  • The URL is kept for 7 days after the send, which covers every status that send can produce.

URL Requirements

Your webhook URL must meet these requirements:

RequirementDetails
ProtocolHTTPS (required for production)
Response timeAnswer within a few seconds: acknowledge first, then process. The request is abandoned after 60 seconds
Status codeReturn a 2xx status (e.g. 200) to confirm receipt
AvailabilityMust be always available to receive notifications

A 5xx, 408, 425 or 429 answer, a timeout or an unreachable endpoint is retried for about 45 minutes; any other answer outside 2xx drops that notification (see Retries).

Valid URL example:

https://api.mycompany.com/webhooks/liguelead

Webhook Payload Structure

All LigueLead webhooks share the same three top-level fields. Campaign webhooks — delivery updates and RCS reply-button clicks — carry a campaign object on top of them, with the common fields below.

Two events differ from that shape, and switching on event is what tells them apart:

  • rcs.agent.status, about the sender brand rather than a message, has no campaign at all and goes to a different address. It is described right after this table.
  • sms.inbound, a reply to an SMS you sent, carries a lean campaign object with only id, phone and message. It is described in the SMS / SMS Flash tab below.

Common Fields

event, app_id and occurred_at are present in every webhook payload. The campaign.* fields are present in every campaign.status and rcs.inbound.postback payload — not in rcs.agent.status, and only id and phone of them in sms.inbound:

FieldTypeDescription
eventstringEvent type. "campaign.status" for delivery updates (all channels), "sms.inbound" for a reply to an SMS, "rcs.inbound.postback" for an RCS reply-button click, or "rcs.agent.status" when an RCS brand moves through review.
app_idstringYour application ID in LigueLead
occurred_atstringWhen LigueLead processed the event and issued this notification, in UTC, ISO 8601 with milliseconds (e.g. "2026-02-02T12:00:15.400Z"). Each notification gets its own value, and a retry of the same notification carries the same one
campaign.idstringUnique campaign ID (UUID v4) — the campaign_id returned by the send
campaign.typestringCampaign type: "sms", "sms_flash", "voice", "voice_ai", "rcs_single", or "rcs_basic". "voice" is a regular voice call and "voice_ai" is an AI agent call
campaign.sourcestringSource of the campaign (e.g., "api", "n8n", "make"). An SMS sent by an AI agent during a call carries "voice_ai"
campaign.phonestringRecipient phone number in international format
campaign.credits_requirednumberNumber of credits consumed by this message or call
campaign.sent_atstringWhen the send was dispatched, in ISO 8601. SMS, SMS Flash and RCS carry the São Paulo offset (e.g. "2026-02-02T09:00:00-03:00"), and may carry only the date ("2026-02-02") for older sends. Voice and Voice AI carry UTC (e.g. "2026-02-02T12:00:00Z"). Parse it with an ISO 8601 parser instead of comparing strings
campaign.statusstringCurrent campaign status. Present on campaign.status events only — see channel-specific statuses below.
⚠️

rcs.agent.status is the one event without a campaign. It is not about a
message — it is about the sender brand, which exists before any send and
outlives every one of them. That event carries an agent object instead, and
only event, app_id and occurred_at are shared with the rest. Switch on
event before reading campaign, or a brand approval will look like a
malformed delivery.

It also does not arrive at the same address: it goes to the webhook_url
declared on the agent, while everything else on this page goes to the app's
webhook URL.

Channel-Specific Payload and Statuses

Each channel includes additional fields and its own set of statuses. Select the tab below for your channel.

SMS Payload Example

{
  "event": "campaign.status",
  "app_id": "your-app-id-here",
  "occurred_at": "2026-02-02T12:00:15.400Z",
  "campaign": {
    "id": "campaign-uuid-v4",
    "type": "sms",
    "source": "api",
    "phone": "+5513991884678",
    "message": "Check out our latest offers! Visit our website now.",
    "credits_required": 1,
    "sent_at": "2026-02-02T09:00:00-03:00",
    "status": "delivered"
  }
}

SMS Flash Payload Example

SMS Flash uses the same envelope — only the campaign.type discriminator changes:

{
  "event": "campaign.status",
  "app_id": "your-app-id-here",
  "occurred_at": "2026-02-02T12:00:15.400Z",
  "campaign": {
    "id": "campaign-uuid-v4",
    "type": "sms_flash",
    "source": "api",
    "phone": "+5513991884678",
    "message": "Your verification code is 123456",
    "credits_required": 1,
    "sent_at": "2026-02-02T09:00:00-03:00",
    "status": "delivered"
  }
}

SMS-Specific Fields

These fields appear only in SMS and SMS Flash webhooks, in addition to the common fields:

FieldTypeDescription
campaign.messagestringThe SMS message content sent to the recipient
💡

Standard SMS and SMS Flash share an identical payload structure. Differentiate them via the campaign.type field: "sms" for standard SMS and "sms_flash" for Flash SMS.

SMS Campaign Statuses

Currently, SMS and SMS Flash campaign.status events report a single status:

StatusDescription
deliveredMessage was delivered to the recipient

A message that is not delivered currently produces no campaign.status event, and SMS does not report sent or any intermediate status by webhook.

🔎

To learn the outcome of every recipient — including the ones that were not delivered — use GET /v1/campaigns/{campaign_id}/recipients. That endpoint reports each recipient individually, with a finer set of statuses than the webhook, and keeps them for 72 hours after the send. See How to Check Campaign Status.

⚠️

Write your handler to accept any status value instead of rejecting the ones it does not know. failed can appear in exceptional cases, and the statuses reported by webhook for SMS may be extended in the future.

graph TD
    A[SMS accepted] --> B{Delivered?}
    B -->|Yes| C["campaign.status: delivered"]
    B -->|No| D["No webhook — check<br/>GET /v1/campaigns/{campaign_id}/recipients"]

SMS Reply Payload Example (sms.inbound)

Fired when a recipient replies to an SMS you sent. It goes to the app's webhook URL — unless LigueLead has set up a dedicated forwarding address for replies on your account, in which case replies go there instead. It never goes to the webhook_url of the send.

{
  "event": "sms.inbound",
  "app_id": "your-app-id-here",
  "occurred_at": "2026-02-02T12:07:41.902Z",
  "campaign": {
    "id": "campaign-uuid-v4",
    "phone": "+5513991884678",
    "message": "Yes, I want to know more"
  }
}
FieldTypeDescription
campaign.idstringThe campaign_id of the send this reply answers, or an empty string when the reply could not be matched to a send.
campaign.phonestringPhone number of the person who replied, in international format (+55…).
campaign.messagestringText of the reply.
ℹ️

Matching a reply to the send it answers is best effort, so treat campaign.id as optional. A reply that cannot be attributed to a recent send of your app may not be delivered at all — except at a dedicated forwarding address, which can also receive unmatched replies, with campaign.id and app_id empty. Not every send can receive replies: it depends on the route the message went out on.

sms.inbound has no campaign.type, campaign.status, campaign.source, campaign.credits_required or campaign.sent_at — switch on event before reading them.

Endpoint Implementation

The webhook endpoint structure is the same for all channels. The difference is in the fields you validate and the statuses you handle. Select the channel tab, then the language tab for an example.

⚠️

These examples are illustrations, not production code. Once your endpoint answers 2xx, LigueLead never sends that notification again — retries only happen while it does not —, so a production receiver must store the payload durably — in a message queue or a database — before answering 2xx, and process it from there. Work kept only in memory (setImmediate, in-process background tasks, or processing inline before answering) is lost if the process restarts, and processing inline also risks the 60-second limit. The SMS / SMS Flash tab shows that shape; in the other tabs, the routing and status handling is what to take from them.

const express = require('express');
const app = express();

app.use(express.json());

// `durableQueue` stands for your own durable storage (a message queue or a
// database table). It is not part of any LigueLead SDK.
app.post('/webhooks/liguelead', async (req, res) => {
  try {
    // Persist BEFORE acknowledging: a notification answered with 2xx is not
    // retried, so whatever is only in memory when you answer is lost if this
    // process restarts. Keep the delivery id with it, to drop a retry you
    // already have.
    await durableQueue.enqueue({
      deliveryId: req.get('X-LigueLead-Delivery-Id'),
      payload: req.body,
    });
    res.status(200).json({ received: true });
  } catch (error) {
    // Not stored. A 500 makes LigueLead send this notification again later,
    // for about 45 minutes; log enough to reconcile if it never gets through.
    console.error('Could not store webhook:', error, req.body);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// A separate worker consumes `durableQueue`, skips a deliveryId it already
// processed, and calls routeEvent() with the stored payload.
function routeEvent(payload) {
  switch (payload.event) {
    case 'campaign.status':
      // The same URL receives every channel; keep only SMS and SMS Flash here.
      if (['sms', 'sms_flash'].includes(payload.campaign?.type)) {
        processStatus(payload);
      }
      break;
    case 'sms.inbound':
      processReply(payload);
      break;
    default:
      // Other events and future ones: acknowledged above, ignored here.
      break;
  }
}

function processStatus(payload) {
  const { campaign, occurred_at } = payload;
  const { id: campaign_id, type: campaign_type, phone, status } = campaign;

  console.log(`Campaign ${campaign_id} (${campaign_type}) ${phone}: ${status} at ${occurred_at}`);

  switch (status) {
    case 'delivered':
      handleDelivered(payload);
      break;
    case 'failed':
      handleFailed(payload);
      break;
    default:
      // Accept statuses you do not handle yet instead of rejecting them.
      console.info(`Unhandled SMS status: ${status}`);
  }
}

function processReply(payload) {
  const { id: campaign_id, phone, message } = payload.campaign;

  // campaign_id is the send this reply answers, or '' when it could not be matched.
  console.log(`Reply from ${phone} to campaign ${campaign_id || '(unknown)'}: ${message}`);
}

function handleDelivered(payload) {
  // Update status in database
  // Send notification to user
  // Trigger next workflow action
}

function handleFailed(payload) {
  // Exceptional — look the recipient up with
  // GET /v1/campaigns/{campaign_id}/recipients?phone=...
}

app.listen(3000, () => console.log('Webhook server running on port 3000'));

Error Handling

Retries

LigueLead sends a notification again when your endpoint could not take it, and only then. This applies to every event type, on the app's webhook URL and on a per-send webhook_url alike.

Your endpoint…What happens
answers 2xxDelivered. Not retried — though a notification can still reach you twice, so deduplicate (see below)
answers 5xx, 408, 425 or 429Retried
has not answered after 60 seconds, refuses the connection, or its hostname does not resolveRetried
answers any other 4xxNot retried — the notification is dropped
answers a 3xxOn a per-send webhook_url: the redirect is not followed and the notification is not retried. On the app's webhook URL the redirect is followed, as it always was, and the final answer decides

Schedule. Up to 8 attempts over about 45 minutes. The wait after a failed attempt starts at 30 seconds and doubles each time, up to 15 minutes — 30 s, 1 min, 2 min, 4 min, 8 min, 15 min, then 15 min — and each wait varies by up to 20% (never above 15 minutes), so that a backlog does not come back all at once. When a 429 or 503 carries a Retry-After header (in seconds or as an HTTP date), the wait is at least that long, up to 15 minutes. After the eighth failed attempt the notification is no longer sent.

Headers. Every attempt carries two headers that tell a retry apart from a new notification:

HeaderValue
X-LigueLead-Delivery-IdIdentifies the notification. The same on every attempt of it
X-LigueLead-Delivery-Attempt1 on the first attempt, counting up on each retry. A number can repeat when LigueLead had to send the same attempt again

What this means for your endpoint:

  • Answer 2xx fast — store the payload and answer before processing it. A request still open after 60 seconds is a failed attempt, and the notification is sent again even if you went on to process it
  • Deduplicate by X-LigueLead-Delivery-Id — a retry is the same notification sent again, and you can receive one you already have: when your answer did not reach us in time, for example. If you already stored that id, answer 2xx and drop the request
  • Expect events out of order — a retried notification can arrive after a later one for the same recipient, such as an RCS delivered retried after the read that followed it got through. Order by occurred_at, which is set when LigueLead issues the notification and does not change between attempts, and do not let an older status overwrite a newer one
  • Answer 4xx only for what you never want to receive — a 400 or 404 drops the notification for good, while a 503 gets it sent again
⚠️

A notification your endpoint keeps failing for about 45 minutes — or refuses with a 4xx other than 408, 425 and 429 — is no longer sent. To keep it from being lost:

  • Persist, then respond — store the payload durably (message queue or database) and return a 2xx status before doing any other processing
  • Use asynchronous processing — process from that durable queue, not from memory: an in-process background task is lost if the process restarts after you answered
  • Keep detailed logs — log every incoming webhook, with its X-LigueLead-Delivery-Id, for debugging and reconciliation
  • Monitor endpoint availability — an outage longer than about 45 minutes loses the notifications issued during it
  • Reconcile SMS on demand — GET /v1/campaigns/{campaign_id}/recipients lists the status of every recipient of an SMS campaign for 72 hours, so a lost notification can be recovered there

Security and Best Practices

1. Origin Validation

LigueLead does not currently implement HMAC signatures. Validate the request origin and payload structure:

// Validate origin IP (if LigueLead provides a static IP range)
const allowedIPs = ['LIGUELEAD_IP'];
if (!allowedIPs.includes(req.ip)) {
  return res.status(403).json({ error: 'Forbidden' });
}

// Check the event type, but acknowledge one you do not know instead of rejecting it:
// a 400 is not retried, so it only loses the notification.
const knownEvents = ['campaign.status', 'sms.inbound', 'rcs.inbound.postback', 'rcs.agent.status'];
if (!knownEvents.includes(payload.event)) {
  console.warn(`Ignoring unknown event: ${payload.event}`);
  return res.status(200).json({ received: true });
}

2. Idempotency

Two kinds of duplicate can reach your endpoint, and each has its own key:

  • A retry of a notification you already received carries the same X-LigueLead-Delivery-Id header (see Retries). Drop a request whose delivery id you already stored.
  • A second notification of the same status — the provider reported it twice, for example — is a new notification, with its own delivery id and its own occurred_at. Recognise it by its content instead, leaving occurred_at out of the key:
const processedWebhooks = new Set();

function processWebhook(payload) {
  // Unique key for a status event: campaign ID + recipient + status
  const webhookId = `${payload.campaign.id}_${payload.campaign.phone}_${payload.campaign.status}`;

  if (processedWebhooks.has(webhookId)) {
    console.log('Webhook already processed:', webhookId);
    return;
  }

  processedWebhooks.add(webhookId);
  // Process webhook...
}
💡

In production, replace the in-memory Set with a persistent store (e.g., Redis or a database table) to survive server restarts.

3. Rate Limiting

Size your endpoint for the notification volume instead of throttling it. A campaign produces status notifications for each of its recipients, and they can arrive in bursts. A rate limiter that answers 429 only delays the notification it refuses — it is retried, after at least the Retry-After you send, up to 15 minutes — but only for about 45 minutes, and every refused notification comes back on top of the new ones.

If you must put a limiter in front of the endpoint, set it above your peak campaign volume, answer 429 (never another 4xx, which drops the notification), and keep the processing behind a queue so the endpoint itself only acknowledges.

Monitoring and Debug

Webhook Headers

LigueLead sends the following headers with every webhook request:

Content-Type: application/json
User-Agent: LigueLead-WebhookDispatcher/1.0
X-LigueLead-Delivery-Id: 6f1d2c3b-8a4e-4f5d-9b7c-0e1f2a3b4c5d
X-LigueLead-Delivery-Attempt: 1

X-LigueLead-Delivery-Id is the same on every attempt of a notification, and X-LigueLead-Delivery-Attempt counts them — see Retries.

Recommended Logging

Log incoming webhooks with channel-relevant fields:

console.log('Webhook received:', {
  campaign_id: payload.campaign.id,
  campaign_type: payload.campaign.type,           // 'sms' or 'sms_flash'
  status: payload.campaign.status,
  message: payload.campaign.message,
  timestamp: new Date().toISOString(),
  processing_time_ms: processingTime
});

Important Metrics

Monitor these metrics across all channels:

  • Success rate of received webhooks (2xx responses)
  • Response time of your endpoint (target < 1 second)
  • Status distribution across campaigns (delivered vs. failed, read for RCS, answer vs. no_answer for voice)
  • Webhook frequency per campaign and channel

Testing Webhooks

1. Development Environment

Use ngrok to expose your local server to the internet:

npm install -g ngrok
ngrok http 3000

Copy the generated HTTPS URL (e.g., https://abc123.ngrok.io) and paste it into the Webhook URL field in the LigueLead client area.

2. Payload Simulation

Send a test webhook to your local endpoint to verify your implementation:

// Standard SMS
const smsPayload = {
  "event": "campaign.status",
  "app_id": "test-app-id",
  "occurred_at": new Date().toISOString(),
  "campaign": {
    "id": "test-campaign-123",
    "type": "sms",
    "source": "api",
    "phone": "+5511999999999",
    "message": "Check out our latest offers!",
    "credits_required": 1,
    "sent_at": "2026-02-02T09:00:00-03:00",
    "status": "delivered"
  }
};

// SMS Flash — same envelope, only campaign.type changes
const smsFlashPayload = {
  ...smsPayload,
  campaign: {
    ...smsPayload.campaign,
    type: "sms_flash",
    message: "Your verification code is 123456"
  }
};

// Reply to an SMS — lean campaign object, sent to the app's webhook URL (never to a send's webhook_url)
const smsReplyPayload = {
  "event": "sms.inbound",
  "app_id": "test-app-id",
  "occurred_at": new Date().toISOString(),
  "campaign": {
    "id": "test-campaign-123",
    "phone": "+5511999999999",
    "message": "Yes, I want to know more"
  }
};

for (const payload of [smsPayload, smsFlashPayload, smsReplyPayload]) {
  fetch('http://localhost:3000/webhooks/liguelead', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
}

Frequently Asked Questions

clockHow often are webhooks sent?

Webhooks are sent as soon as LigueLead processes the event, for all channels (SMS, SMS Flash, RCS, and Voice). For SMS and SMS Flash, the status events currently report only deliveries (delivered). An sms.inbound event is sent when a recipient replies to an SMS, and for RCS an additional rcs.inbound.postback event is sent every time the recipient taps an interactive reply button.

triangle-exclamationWhat if my endpoint is down?

LigueLead retries the notification for about 45 minutes — up to 8 attempts, with waits from 30 seconds to 15 minutes — while your endpoint is unreachable, does not answer within 60 seconds, or answers 5xx, 408, 425 or 429; see Retries. After that, or at once for any other 4xx or a 3xx, the notification is no longer sent. An outage longer than about 45 minutes therefore loses the notifications issued during it, so high availability is still strongly recommended. For SMS, GET /v1/campaigns/{campaign_id}/recipients keeps the status of every recipient for 72 hours, so you can reconcile what you missed.

linkCan I configure different URLs for different campaign types?

Not per campaign type: the app's webhook URL receives notifications for all channels and events. Use the event field to differentiate campaign.status from sms.inbound and rcs.inbound.postback, and campaign.type to route status events by channel. What you can do is pass webhook_url on a send, and the status events of that send go there instead — see Per-send webhook URL.

copyHow do I identify duplicate webhooks?

A retry of the same notification carries the same X-LigueLead-Delivery-Id header: drop a request whose delivery id you already stored. A second notification of the same status is a new notification with its own delivery id, so for status events also use the combination of campaign.id + campaign.phone + campaign.status: a campaign has one campaign.id for all of its recipients, so the phone is what tells them apart. Do not include occurred_at — it is set each time LigueLead issues a notification, so a second notification of the same status carries a different value (a retry keeps the same one). For rcs.inbound.postback events, campaign.id + campaign.phone + campaign.inbound.postback_data identifies the same answer from the same recipient.

gauge-highIs there a payload or frequency limit?

There is no specific payload size limit. Webhook frequency depends on your campaign volume and, for RCS, on user engagement with interactive buttons.

boltHow do I distinguish SMS from SMS Flash in the payload?

Check the campaign.type field: "sms" for standard SMS and "sms_flash" for Flash SMS. The rest of the payload is identical between the two.

code-branchHow do I distinguish webhooks across channels?

First check the event field: "rcs.inbound.postback" always refers to an RCS button tap; "sms.inbound" is a reply to an SMS and carries no campaign.type; "campaign.status" is a delivery status update. For status events, use campaign.type to route by channel: "sms" (SMS) and "sms_flash" (SMS Flash), "rcs_single" or "rcs_basic" (RCS), and "voice" (regular voice) or "voice_ai" (AI agent call). Match every value you expect — routing Voice on "voice" alone silently drops AI agent calls.

hand-pointerWhat is an `rcs.inbound.postback` event?

When a recipient taps a reply button on an RCS card or carousel, the messaging app sends back the button's postback_data value. LigueLead forwards this to your webhook as an rcs.inbound.postback event so you can react in real time (e.g., capture interest, trigger a follow-up flow). Buttons of type open_url and dial_call do not generate postback events — they are handled directly by the user's device.

replyHow do I receive replies to my SMS?

Replies arrive at the app's webhook URL as sms.inbound events — or at the dedicated forwarding address for replies, when LigueLead has set one up for your account — with the reply text in campaign.message and the sender in campaign.phone. campaign.id is the campaign_id of the send being answered when the reply could be matched to it, and an empty string otherwise — matching is best effort. The payload is described in the SMS / SMS Flash tab of Channel-Specific Payload and Statuses.

link-simpleHow do I correlate a postback with the original RCS campaign?

The campaign.id returned in the rcs.inbound.postback event is the same identifier returned when the RCS message was sent. Store the mapping campaign.id → user/context at send time and look it up when the postback arrives. The campaign.template_id is also included for template-level correlation.

rotateWhat happens if RCS delivery fails — do I get an SMS fallback notification?

When an RCS message cannot be delivered (recipient not RCS-capable, device offline beyond TTL, etc.), LigueLead automatically falls back to SMS — with the template's fallback_message for template sends, or with the message itself for freeform sends. In that case you receive an RCS campaign.status with status: "failed" or "undelivered" for the RCS attempt, and, once the fallback SMS is delivered, a separate campaign.status with campaign.type: "sms" and status: "delivered". Both carry the same campaign.id. Like any SMS, the fallback currently reports only delivered: a fallback SMS that is not delivered produces no SMS event.

Support

For questions or issues with webhooks:


This documentation was updated in October 2026. For the latest version, always consult the API portal.


Did this page help you?