Skip to main content
Webhooks notify your server when events occur, like message delivery, failures, or inbound messages. This is more efficient than polling the API.

How Webhooks Work

1. An event occurs (message sent, delivered, received, etc.)
2. Zavu sends a POST request to your endpoint
3. Your server processes the event and returns 200 OK

Setting Up Webhooks

1. Create a Webhook Endpoint

Your endpoint must:
  • Accept POST requests
  • Return 200 status quickly (within 5 seconds)
  • Handle retries gracefully
// Express.js example
app.post('/webhooks/zavu', (req, res) => {
  const event = req.body;

  // Process asynchronously
  processWebhook(event).catch(console.error);

  // Respond immediately
  res.status(200).send('OK');
});

async function processWebhook(event) {
  switch (event.type) {
    case 'message.inbound':
      await handleInboundMessage(event.data);
      break;
    case 'message.delivered':
      await handleDelivered(event.data);
      break;
    case 'message.failed':
      await handleFailed(event.data);
      break;
  }
}

2. Configure in Dashboard

  1. Go to Webhooks in the sidebar
  2. Click Create Webhook
  3. Enter your endpoint URL
  4. Select events to receive
  5. Save and test

Event Payload Format

All webhook events follow this structure:
{
  "id": "evt_1705312200000_abc123",
  "type": "message.delivered",
  "timestamp": 1705312200000,
  "projectId": "k57abc123def456",
  "data": {
    // Event-specific data
  }
}
FieldTypeDescription
idstringUnique event identifier
typestringEvent type (e.g., message.delivered)
timestampnumberUnix timestamp in milliseconds
projectIdstringYour project ID
dataobjectEvent-specific payload

Message Events

message.queued

Fired when a message is accepted and queued for delivery.
{
  "id": "evt_1705312200000_abc123",
  "type": "message.queued",
  "timestamp": 1705312200000,
  "projectId": "k57abc123def456",
  "data": {
    "messageId": "k57msg789xyz",
    "to": "+56912345678",
    "channel": "whatsapp",
    "status": "queued",
    "templateId": null
  }
}

message.sent

Fired when a message is successfully sent to the carrier/provider.
{
  "id": "evt_1705312201000_def456",
  "type": "message.sent",
  "timestamp": 1705312201000,
  "projectId": "k57abc123def456",
  "data": {
    "messageId": "k57msg789xyz",
    "to": "+56912345678",
    "channel": "whatsapp",
    "status": "sending",
    "errorCode": null,
    "errorMessage": null
  }
}

message.delivered

Fired when a message is successfully delivered to the recipient.
{
  "id": "evt_1705312205000_ghi789",
  "type": "message.delivered",
  "timestamp": 1705312205000,
  "projectId": "k57abc123def456",
  "data": {
    "messageId": "k57msg789xyz",
    "to": "+56912345678",
    "channel": "sms",
    "status": "delivered",
    "errorCode": null,
    "errorMessage": null
  }
}

message.failed

Fired when a message fails to deliver.
{
  "id": "evt_1705312210000_jkl012",
  "type": "message.failed",
  "timestamp": 1705312210000,
  "projectId": "k57abc123def456",
  "data": {
    "messageId": "k57msg789xyz",
    "to": "+56912345678",
    "channel": "sms",
    "status": "failed",
    "errorCode": "30003",
    "errorMessage": "Unreachable destination handset"
  }
}

message.inbound

Fired when you receive a message from a user. This is essential for building conversational applications.
{
  "id": "evt_1705312215000_mno345",
  "type": "message.inbound",
  "timestamp": 1705312215000,
  "projectId": "k57abc123def456",
  "data": {
    "messageId": "k57msg456abc",
    "from": "+56912345678",
    "to": "+14155551234",
    "channel": "whatsapp",
    "text": "Hello, I need help with my order"
  }
}
Use message.inbound to build chatbots, customer support systems, or any two-way messaging application.

Template Events

template.approved

Fired when a WhatsApp template is approved by Meta.
{
  "id": "evt_1705312220000_pqr678",
  "type": "template.approved",
  "timestamp": 1705312220000,
  "projectId": "k57abc123def456",
  "data": {
    "templateId": "k57tpl123abc",
    "name": "order_confirmation",
    "status": "approved"
  }
}

template.rejected

Fired when a WhatsApp template is rejected by Meta.
{
  "id": "evt_1705312225000_stu901",
  "type": "template.rejected",
  "timestamp": 1705312225000,
  "projectId": "k57abc123def456",
  "data": {
    "templateId": "k57tpl456def",
    "name": "promotional_offer",
    "status": "rejected",
    "reason": "Template contains prohibited content"
  }
}

Webhook Security

Signature Verification

Every webhook includes a signature header for verification:
X-Zavu-Signature: sha256=abc123...
Verify the signature:
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return `sha256=${expected}` === signature;
}

app.post('/webhooks/zavu', (req, res) => {
  const signature = req.headers['x-zavu-signature'];
  const webhookSecret = process.env.ZAVU_WEBHOOK_SECRET;

  if (!verifyWebhook(req.body, signature, webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook...
  res.status(200).send('OK');
});
Always verify webhook signatures in production to prevent spoofed requests.

Retry Policy

Failed webhook deliveries are retried with exponential backoff:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
After 5 failed attempts, the webhook is marked as failed and logged.

Best Practices

1. Respond Quickly

Return 200 status immediately, process asynchronously:
// Good: Respond immediately
app.post('/webhooks/zavu', (req, res) => {
  queue.add('process-webhook', req.body);
  res.status(200).send('OK');
});

// Bad: Process synchronously
app.post('/webhooks/zavu', async (req, res) => {
  await database.update(...);  // Slow!
  await sendEmail(...);        // Even slower!
  res.status(200).send('OK');
});

2. Handle Duplicates

Webhooks may be delivered multiple times. Use the event ID for idempotency:
async function processWebhook(event) {
  const processed = await redis.get(`webhook:${event.id}`);
  if (processed) {
    return; // Already processed
  }

  await handleEvent(event);
  await redis.set(`webhook:${event.id}`, '1', 'EX', 86400);
}

3. Log Everything

Log webhook events for debugging:
app.post('/webhooks/zavu', (req, res) => {
  console.log('Webhook received:', JSON.stringify(req.body));
  // ...
});

4. Monitor Failures

Set up alerts for webhook processing failures:
async function processWebhook(event) {
  try {
    await handleEvent(event);
  } catch (error) {
    console.error('Webhook processing failed:', error);
    await alerting.notify('Webhook failure', error);
    throw error;
  }
}

Testing Webhooks

Local Development

Use a tunnel service like ngrok:
ngrok http 3000
# Use the ngrok URL in your webhook settings

Webhook Tester

Use our built-in tester in the dashboard:
  1. Go to Webhooks
  2. Click on your webhook
  3. Click Send Test Event
  4. Verify your endpoint receives the event

Troubleshooting

IssueCauseSolution
Not receiving webhooksWrong URLVerify endpoint URL in settings
401 errorsInvalid signatureCheck webhook secret
TimeoutsSlow processingProcess asynchronously
Duplicate eventsNormal retriesImplement idempotency
Missing inbound eventsWrong event selectedEnsure message.inbound is enabled