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;
}
}
- Go to Webhooks in the sidebar
- Click Create Webhook
- Enter your endpoint URL
- Select events to receive
- Save and test
All webhook events follow this structure:
{
"id": "evt_1705312200000_abc123",
"type": "message.delivered",
"timestamp": 1705312200000,
"projectId": "k57abc123def456",
"data": {
// Event-specific data
}
}
| Field | Type | Description |
|---|
id | string | Unique event identifier |
type | string | Event type (e.g., message.delivered) |
timestamp | number | Unix timestamp in milliseconds |
projectId | string | Your project ID |
data | object | Event-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:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 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:
- Go to Webhooks
- Click on your webhook
- Click Send Test Event
- Verify your endpoint receives the event
Troubleshooting
| Issue | Cause | Solution |
|---|
| Not receiving webhooks | Wrong URL | Verify endpoint URL in settings |
| 401 errors | Invalid signature | Check webhook secret |
| Timeouts | Slow processing | Process asynchronously |
| Duplicate events | Normal retries | Implement idempotency |
| Missing inbound events | Wrong event selected | Ensure message.inbound is enabled |