How to Receive Webhooks

Webhook guide for SMS, SMS Flash, and Voice campaigns

How to Receive LigueLead 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

  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"
📌

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:

RequirementDetails
ProtocolHTTPS (required for production)
Response timeMust respond within 5 seconds
Status codeReturn HTTP 200 to confirm receipt
AvailabilityMust be always available to receive notifications

Valid URL example:

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

Webhook 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:

FieldTypeDescription
eventstringEvent type (always "campaign.status")
app_idstringYour application ID in LigueLead
occurred_atstringEvent date/time in ISO 8601 format
campaign.idstringUnique campaign ID (UUID v4)
campaign.typestringCampaign type: "sms" or "voice"
campaign.sourcestringSource of the campaign (e.g., "api", "n8n", "make")
campaign.phonestringRecipient phone number in international format
campaign.credits_requirednumberNumber of credits consumed by this message or call
campaign.sent_atstringDate when the campaign was initiated (ISO 8601 format)
campaign.statusstringCurrent 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:

FieldTypeDescription
campaign.messagestringThe SMS message content sent to the recipient
campaign.is_flashbooleantrue 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_flash field: true for Flash SMS, false for standard SMS.

SMS Campaign Statuses

StatusDescription
sentSMS was queued and sent to the carrier
deliveredSMS was delivered to the recipient
undeliveredSMS was not delivered (includes timeout and carrier rejection)
failedSMS failed (error, congestion, no route, or unknown)
graph TD
    A[SMS Initiated] --> B[sent]
    B --> C{Delivery Result}
    C --> D[delivered]
    C --> E[undelivered]
    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 'undelivered':
      handleUndelivered(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 handleDelivered(payload) {
  // Update status in database
  // Send notification to user
  // Trigger next workflow action
}

function handleUndelivered(payload) {
  // Implement retry logic
  // Notify about failure
}

function handleInvalidNumber(payload) {
  // Remove or flag the number in your contact list
}

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'));

Error 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 Set with 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.0

Recommended 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
});

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. 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 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:

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)
});

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:


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