---
updatedAt: 2026-05-28T12:46:30.000Z
---

Fetch the complete documentation index at: https://docs.liguelead.com.br/llms.txt. Use this file to discover all available pages before exploring further.

# 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

1. Access [https://areadocliente.liguelead.app.br/](https://areadocliente.liguelead.com.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"**

<Callout icon="📌" theme="info">
  A single webhook URL receives notifications for **all channels** (SMS, SMS Flash, and Voice). You cannot configure different URLs per channel.
</Callout>

### 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:**

```text
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:

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

<Tabs>
  <Tab title="SMS / SMS Flash">
    #### SMS / SMS Flash Payload Example

    ```json
    {
      "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 |

    <Callout icon="💡" theme="info">
      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.
    </Callout>

    #### 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                                       |

    ```mermaid
    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]
    ```
  </Tab>

  <Tab title="Voice">
    #### Voice Payload Example

    ```json
    {
      "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) |

    ```mermaid
    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]
    ```
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="SMS / SMS Flash">
    <Tabs>
      <Tab title="Node.js (Express)">
        ```javascript
        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'));
        ```
      </Tab>

      <Tab title="Python (FastAPI)">
        ```python
        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
            pass
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Voice">
    <Tabs>
      <Tab title="Node.js (Express)">
        ```javascript
        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',
            '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'));
        ```
      </Tab>

      <Tab title="Python (FastAPI)">
        ```python
        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
            pass
        ```
      </Tab>
    </Tabs>
  </Tab>
</Tabs>

## Error Handling

### Retry Behavior

<Callout icon="🚨" theme="danger">
  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**.
</Callout>

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:

```json
{
  "message": "Webhook notification dispatched successfully",
  "url": "https://your-url.com/webhook",
  "method": "POST",
  "payload": { /* webhook payload */ }
}
```

On failed dispatch:

```json
{
  "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:

```javascript
// 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:

```javascript
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...
}
```

<Callout icon="💡" theme="info">
  In production, replace the in-memory `Set` with a persistent store (e.g., Redis or a database table) to survive server restarts.
</Callout>

### 3. Rate Limiting

Protect your endpoint from excessive requests:

```javascript
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:

```text
Content-Type: application/json
User-Agent: LigueLead-WebhookDispatcher/1.0
```

### Recommended Logging

Log incoming webhooks with channel-relevant fields:

<Tabs>
  <Tab title="SMS / SMS Flash">
    ```javascript
    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
    });
    ```
  </Tab>

  <Tab title="Voice">
    ```javascript
    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
    });
    ```
  </Tab>
</Tabs>

### 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](https://ngrok.com/) to expose your local server to the internet:

```bash
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](https://areadocliente.liguelead.com.br/).

### 2. Payload Simulation

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

<Tabs>
  <Tab title="SMS / SMS Flash">
    ```javascript
    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)
    });
    ```
  </Tab>

  <Tab title="Voice">
    ```javascript
    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)
    });
    ```
  </Tab>
</Tabs>

## Frequently Asked Questions

<Accordion title="How often are webhooks sent?" icon="clock">
  Webhooks are sent immediately when a campaign status changes, for all channels (SMS, SMS Flash, and Voice).
</Accordion>

<Accordion title="What if my endpoint is down?" icon="triangle-exclamation">
  LigueLead does **not** store or resend webhooks. If your endpoint is unavailable, the notification is permanently lost. High availability is strongly recommended.
</Accordion>

<Accordion title="Can I configure different URLs for different campaign types?" icon="link">
  No. A single webhook URL receives notifications for all channels (SMS, SMS Flash, and Voice). Use the `campaign.type` field to route processing logic.
</Accordion>

<Accordion title="How do I identify duplicate webhooks?" icon="copy">
  Use the combination of `campaign.id` + `campaign.status` + `occurred_at` as a unique identifier to detect and skip duplicates.
</Accordion>

<Accordion title="Is there a payload or frequency limit?" icon="gauge-high">
  There is no specific payload size limit. Webhook frequency depends on your campaign volume.
</Accordion>

<Accordion title="How do I distinguish SMS from SMS Flash in the payload?" icon="bolt">
  Both use `campaign.type: "sms"`. Check the `campaign.is_flash` field: `true` for Flash SMS, `false` for standard SMS.
</Accordion>

<Accordion title="How do I distinguish SMS from Voice webhooks?" icon="code-branch">
  Check the `campaign.type` field: `"sms"` for SMS/SMS Flash campaigns, `"voice"` for Voice campaigns. Each type includes different channel-specific fields.
</Accordion>

## Support

For questions or issues with webhooks:

* 🌐 **Portal**: [https://areadocliente.liguelead.app.br/](https://areadocliente.liguelead.com.br/)

***

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