How to Receive Webhooks
Configure your application to receive real-time webhook notifications for SMS, SMS Flash, and Voice campaign status changes.
Overview
LigueLead automatically sends webhooks to notify your application when campaign statuses change. This applies to all channels: SMS, SMS Flash, and Voice. 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
Webhook URL Configuration
Client Area Access
- Access https://areadocliente.liguelead.app.br/
- Log in with your credentials
- Navigate to Integrations > API Token
- Locate the "Webhook URL" section
- Enter the complete URL of your webhook endpoint
- Click "Save"
A single webhook URL receives notifications for all channels (SMS, SMS Flash, and Voice). You cannot configure different URLs per channel.
URL Requirements
Your webhook URL must meet these requirements:
| Requirement | Details |
|---|---|
| Protocol | HTTPS (required for production) |
| Response time | Must respond within 5 seconds |
| Status code | Return HTTP 200 to confirm receipt |
| Availability | Must be always available to receive notifications |
Valid URL example:
https://api.mycompany.com/webhooks/ligueleadWebhook Payload Structure
All LigueLead webhooks — regardless of channel — share the same top-level JSON structure and a set of common fields inside the campaign object.
Common Fields
These fields are present in every webhook payload:
| Field | Type | Description |
|---|---|---|
event | string | Event type (always "campaign.status") |
app_id | string | Your application ID in LigueLead |
occurred_at | string | Event date/time in ISO 8601 format |
campaign.id | string | Unique campaign ID (UUID v4) |
campaign.type | string | Campaign type: "sms" or "voice" |
campaign.source | string | Source of the campaign (e.g., "api", "n8n", "make") |
campaign.phone | string | Recipient phone number in international format |
campaign.credits_required | number | Number of credits consumed by this message or call |
campaign.sent_at | string | Date when the campaign was initiated (ISO 8601 format) |
campaign.status | string | Current campaign status (see channel-specific statuses below) |
Channel-Specific Payload and Statuses
Each channel includes additional fields and its own set of statuses. Select the tab below for your channel.
SMS / SMS Flash 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.",
"is_flash": false,
"credits_required": 1,
"sent_at": "2026-02-02",
"status": "delivered"
}
}SMS-Specific Fields
These fields appear only in SMS and SMS Flash webhooks, in addition to the common fields:
| Field | Type | Description |
|---|---|---|
campaign.message | string | The SMS message content sent to the recipient |
campaign.is_flash | boolean | true if the message was sent as Flash SMS, false for standard SMS |
SMS and SMS Flash share the same payload structure. The only difference is the
is_flashfield:truefor Flash SMS,falsefor standard SMS.
SMS Campaign Statuses
| Status | Description |
|---|---|
sent | Message was sent to the gateway |
delivered | Message was delivered to the recipient |
failed | Generic failure, such as configuration, unknown, request, or carrier errors |
policy_block | Message was blocked by a spam or fraud filter |
unreachable | Number is unreachable or the region is denied |
invalid_number | Number is invalid, landline, or does not support SMS |
opt_out | Recipient has unsubscribed from SMS messages |
graph TD
A[SMS Initiated] --> B[sent]
B --> C{Delivery Result}
C --> D[delivered]
C --> E[failed]
C --> F[policy_block]
C --> G[unreachable]
C --> H[invalid_number]
C --> I[opt_out]Voice 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": "voice",
"source": "api",
"phone": "+5513991884678",
"audio_title": "Promo Fevereiro",
"audio_time": "00:00:30",
"audio_id": 1234,
"credits_required": 1,
"sent_at": "2026-02-02",
"duration_sec": 23,
"status": "answer"
}
}Voice-Specific Fields
These fields appear only in Voice webhooks, in addition to the common fields:
| Field | Type | Description |
|---|---|---|
campaign.audio_title | string | Title of the audio file used in the call |
campaign.audio_time | string | Duration of the audio message (e.g., "00:00:30") |
campaign.audio_id | number | Unique identifier of the audio file |
campaign.duration_sec | number | Duration of the answered call, in seconds. Example: 23 |
Voice Campaign Statuses
| Status | Description |
|---|---|
sent | Call was queued and sent to the carrier |
answer | Call was answered by the recipient |
no_answer | Call was not answered (includes busy and timeout) |
invalid_number | The provided phone number is invalid |
failed | Call failed (hangup, error, congestion, no route, or unknown) |
graph TD
A[Call Initiated] --> B[sent]
B --> C{Call Result}
C --> D[answer]
C --> E[no_answer]
C --> F[invalid_number]
C --> G[failed]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 a complete example.
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/liguelead', (req, res) => {
try {
const payload = req.body;
// Validate payload structure
if (!isValidPayload(payload)) {
return res.status(400).json({ error: 'Invalid payload' });
}
// Process webhook based on status
processWebhook(payload);
// Respond immediately to avoid 5-second timeout
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
function isValidPayload(payload) {
const requiredFields = ['event', 'app_id', 'occurred_at'];
const campaignFields = [
'id', 'type', 'status', 'source', 'phone',
'message', 'is_flash', 'credits_required', 'sent_at'
];
return requiredFields.every(field => payload.hasOwnProperty(field))
&& payload.campaign
&& campaignFields.every(field => payload.campaign.hasOwnProperty(field));
}
function processWebhook(payload) {
const { campaign, occurred_at } = payload;
const { id: campaign_id, type: campaign_type, status } = campaign;
console.log(`Campaign ${campaign_id} (${campaign_type}): ${status} at ${occurred_at}`);
switch (status) {
case 'delivered':
handleDelivered(payload);
break;
case 'policy_block':
handlePolicyBlock(payload);
break;
case 'unreachable':
handleUnreachable(payload);
break;
case 'invalid_number':
handleInvalidNumber(payload);
break;
case 'opt_out':
handleOptOut(payload);
break;
case 'failed':
handleFailed(payload);
break;
case 'sent':
handleSent(payload);
break;
default:
console.warn(`Unknown status: ${status}`);
}
}
function handleDelivered(payload) {
// Update status in database
// Send notification to user
// Trigger next workflow action
}
function handlePolicyBlock(payload) {
// Message blocked by spam or fraud policy — review message content and campaign rules
}
function handleUnreachable(payload) {
// Number or region cannot be reached — flag the contact or region for review
}
function handleInvalidNumber(payload) {
// Remove or flag the number in your contact list
}
function handleOptOut(payload) {
// Recipient unsubscribed — suppress this number from future SMS campaigns
}
function handleFailed(payload) {
// Implement retry logic
// Notify about failure
// Update error metrics
}
function handleSent(payload) {
// Update campaign status in your system
}
app.listen(3000, () => console.log('Webhook server running on port 3000'));from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import logging
app = FastAPI()
class Campaign(BaseModel):
id: str
type: str
source: str
phone: str
message: str
is_flash: bool
credits_required: int
sent_at: str
status: str
class WebhookPayload(BaseModel):
event: str
app_id: str
campaign: Campaign
occurred_at: str
@app.post("/webhooks/liguelead")
async def receive_webhook(payload: WebhookPayload):
try:
# Validate event type
if payload.event != "campaign.status":
raise HTTPException(status_code=400, detail="Invalid event type")
# Process webhook asynchronously
await process_webhook(payload)
return {"received": True}
except Exception as e:
logging.error(f"Webhook processing error: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
async def process_webhook(payload: WebhookPayload):
logging.info(f"Campaign {payload.campaign.id} ({payload.campaign.type}): {payload.campaign.status}")
if payload.campaign.status == "delivered":
await handle_delivered(payload)
elif payload.campaign.status == "policy_block":
await handle_policy_block(payload)
elif payload.campaign.status == "unreachable":
await handle_unreachable(payload)
elif payload.campaign.status == "invalid_number":
await handle_invalid_number(payload)
elif payload.campaign.status == "opt_out":
await handle_opt_out(payload)
elif payload.campaign.status == "failed":
await handle_failed(payload)
elif payload.campaign.status == "sent":
await handle_sent(payload)
async def handle_delivered(payload: WebhookPayload):
# Implement successful delivery logic
pass
async def handle_policy_block(payload: WebhookPayload):
# Message blocked by spam or fraud policy — review message content and campaign rules
pass
async def handle_unreachable(payload: WebhookPayload):
# Number or region cannot be reached — flag the contact or region for review
pass
async def handle_invalid_number(payload: WebhookPayload):
# Remove or flag the number in your contact list
pass
async def handle_opt_out(payload: WebhookPayload):
# Recipient unsubscribed — suppress this number from future SMS campaigns
pass
async def handle_failed(payload: WebhookPayload):
# Implement failure logic
pass
async def handle_sent(payload: WebhookPayload):
# Update campaign status in your system
passconst express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/liguelead', (req, res) => {
try {
const payload = req.body;
// Validate payload structure
if (!isValidPayload(payload)) {
return res.status(400).json({ error: 'Invalid payload' });
}
// Process webhook based on status
processWebhook(payload);
// Respond immediately to avoid 5-second timeout
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
function isValidPayload(payload) {
const requiredFields = ['event', 'app_id', 'occurred_at'];
const campaignFields = [
'id', 'type', 'status', 'source', 'phone',
'audio_title', 'audio_time', 'audio_id',
'duration_sec', 'credits_required', 'sent_at'
];
return requiredFields.every(field => payload.hasOwnProperty(field))
&& payload.campaign
&& campaignFields.every(field => payload.campaign.hasOwnProperty(field));
}
function processWebhook(payload) {
const { campaign, occurred_at } = payload;
const { id: campaign_id, type: campaign_type, status } = campaign;
console.log(`Campaign ${campaign_id} (${campaign_type}): ${status} at ${occurred_at}`);
switch (status) {
case 'answer':
handleAnswered(payload);
break;
case 'no_answer':
handleNoAnswer(payload);
break;
case 'invalid_number':
handleInvalidNumber(payload);
break;
case 'failed':
handleFailed(payload);
break;
case 'sent':
handleSent(payload);
break;
default:
console.warn(`Unknown status: ${status}`);
}
}
function handleAnswered(payload) {
// Call was answered — update status, trigger follow-up actions
console.log(`Call answered: ${payload.campaign.id}`);
}
function handleNoAnswer(payload) {
// Call not answered — schedule retry or notify
console.log(`Call not answered: ${payload.campaign.id}`);
}
function handleInvalidNumber(payload) {
// Invalid number — remove or flag in your contact list
console.log(`Invalid number: ${payload.campaign.phone}`);
}
function handleFailed(payload) {
// Call failed — log error, update metrics, notify
console.log(`Call failed: ${payload.campaign.id}`);
}
function handleSent(payload) {
// Call queued — update campaign status in your system
console.log(`Call queued: ${payload.campaign.id}`);
}
app.listen(3000, () => console.log('Webhook server running on port 3000'));from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import logging
app = FastAPI()
class Campaign(BaseModel):
id: str
type: str
source: str
phone: str
audio_title: str
audio_time: str
audio_id: int
duration_sec: int
credits_required: int
sent_at: str
status: str
class WebhookPayload(BaseModel):
event: str
app_id: str
campaign: Campaign
occurred_at: str
@app.post("/webhooks/liguelead")
async def receive_webhook(payload: WebhookPayload):
try:
# Validate event type
if payload.event != "campaign.status":
raise HTTPException(status_code=400, detail="Invalid event type")
# Process webhook asynchronously
await process_webhook(payload)
return {"received": True}
except Exception as e:
logging.error(f"Webhook processing error: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
async def process_webhook(payload: WebhookPayload):
logging.info(f"Campaign {payload.campaign.id} ({payload.campaign.type}): {payload.campaign.status}")
if payload.campaign.status == "answer":
await handle_answered(payload)
elif payload.campaign.status == "no_answer":
await handle_no_answer(payload)
elif payload.campaign.status == "invalid_number":
await handle_invalid_number(payload)
elif payload.campaign.status == "failed":
await handle_failed(payload)
elif payload.campaign.status == "sent":
await handle_sent(payload)
async def handle_answered(payload: WebhookPayload):
# Call was answered — update status, trigger follow-up actions
pass
async def handle_no_answer(payload: WebhookPayload):
# Call not answered — schedule retry or notify
pass
async def handle_invalid_number(payload: WebhookPayload):
# Invalid number — remove or flag in your contact list
pass
async def handle_failed(payload: WebhookPayload):
# Call failed — log error, update metrics
pass
async def handle_sent(payload: WebhookPayload):
# Call queued — update campaign status in your system
passError Handling
Retry Behavior
LigueLead does NOT implement automatic retry for webhooks. If your endpoint does not respond within 5 seconds, returns a non-success status, or is unavailable, the notification will be permanently lost.
To prevent data loss:
- Respond immediately — return HTTP 200 before doing heavy processing
- Use asynchronous processing — offload database writes, API calls, and notifications to a background queue
- Keep detailed logs — log every incoming webhook for debugging and reconciliation
- Monitor endpoint availability — set up uptime monitoring and alerts for your webhook URL
LigueLead Log Examples
On successful dispatch:
{
"message": "Webhook notification dispatched successfully",
"url": "https://your-url.com/webhook",
"method": "POST",
"payload": { /* webhook payload */ }
}On failed dispatch:
{
"message": "Failed to dispatch webhook notification",
"url": "https://your-url.com/webhook",
"error": "timeout of 5000ms exceeded",
"stack": "..."
}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' });
}
// Validate expected event type
if (payload.event !== 'campaign.status') {
return res.status(400).json({ error: 'Invalid event' });
}2. Idempotency
Prevent duplicate processing by tracking a unique identifier per webhook:
const processedWebhooks = new Set();
function processWebhook(payload) {
// Unique key: campaign ID + status + timestamp
const webhookId = `${payload.campaign.id}_${payload.campaign.status}_${payload.occurred_at}`;
if (processedWebhooks.has(webhookId)) {
console.log('Webhook already processed:', webhookId);
return;
}
processedWebhooks.add(webhookId);
// Process webhook...
}In production, replace the in-memory
Setwith a persistent store (e.g., Redis or a database table) to survive server restarts.
3. Rate Limiting
Protect your endpoint from excessive requests:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1-minute window
max: 100, // Maximum 100 requests per minute
message: 'Too many webhooks'
});
app.use('/webhooks/liguelead', webhookLimiter);Monitoring and Debug
Webhook Headers
LigueLead sends the following headers with every webhook request:
Content-Type: application/json
User-Agent: LigueLead-WebhookDispatcher/1.0Recommended Logging
Log incoming webhooks with channel-relevant fields:
console.log('Webhook received:', {
campaign_id: payload.campaign.id,
campaign_type: payload.campaign.type,
status: payload.campaign.status,
message: payload.campaign.message,
is_flash: payload.campaign.is_flash,
timestamp: new Date().toISOString(),
processing_time_ms: processingTime
});console.log('Webhook received:', {
campaign_id: payload.campaign.id,
campaign_type: payload.campaign.type,
status: payload.campaign.status,
audio_title: payload.campaign.audio_title,
duration_sec: payload.campaign.duration_sec,
timestamp: new Date().toISOString(),
processing_time_ms: processingTime
});Important Metrics
Monitor these metrics across all channels:
- Success rate of received webhooks (HTTP 200 responses)
- Response time of your endpoint (target < 1 second)
- Status distribution across campaigns (delivered vs. policy_block vs. failed, answered vs. no_answer)
- 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 3000Copy 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:
const testPayload = {
"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!",
"is_flash": false,
"credits_required": 1,
"sent_at": new Date().toISOString().split('T')[0],
"status": "delivered"
}
};
fetch('http://localhost:3000/webhooks/liguelead', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testPayload)
});const testPayload = {
"event": "campaign.status",
"app_id": "test-app-id",
"occurred_at": new Date().toISOString(),
"campaign": {
"id": "test-campaign-123",
"type": "voice",
"source": "api",
"phone": "+5511999999999",
"audio_title": "Test Audio",
"audio_time": "00:00:30",
"audio_id": 1234,
"credits_required": 1,
"sent_at": new Date().toISOString().split('T')[0],
"duration_sec": 23,
"status": "answer"
}
};
fetch('http://localhost:3000/webhooks/liguelead', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testPayload)
});Frequently Asked Questions
How often are webhooks sent?
Webhooks are sent immediately when a campaign status changes, for all channels (SMS, SMS Flash, and Voice).
What if my endpoint is down?
LigueLead does not store or resend webhooks. If your endpoint is unavailable, the notification is permanently lost. High availability is strongly recommended.
Can I configure different URLs for different campaign types?
No. A single webhook URL receives notifications for all channels (SMS, SMS Flash, and Voice). Use the campaign.type field to route processing logic.
How do I identify duplicate webhooks?
Use the combination of campaign.id + campaign.status + occurred_at as a unique identifier to detect and skip duplicates.
Is there a payload or frequency limit?
There is no specific payload size limit. Webhook frequency depends on your campaign volume.
How do I distinguish SMS from SMS Flash in the payload?
Both use campaign.type: "sms". Check the campaign.is_flash field: true for Flash SMS, false for standard SMS.
How do I distinguish SMS from Voice webhooks?
Check the campaign.type field: "sms" for SMS/SMS Flash campaigns, "voice" for Voice campaigns. Each type includes different channel-specific fields.
Support
For questions or issues with webhooks:
- 🌐 Portal: https://areadocliente.liguelead.app.br/
This documentation was updated in March 2026. For the latest version, always consult the API portal.
Updated 3 months ago
