Skip to main content

Webhooks

Zapy API sends real-time notifications to your server when events occur on your WhatsApp instances. Configure a single webhook URL to receive events from all your instances.

User-Level Webhooks

Webhooks are configured at the user/account level, not per-instance. This means:

  • One webhook URL receives events from all your instances
  • Simplified management - configure once, receive all events
  • Built-in retry queue with automatic retries and pause/resume
  • Webhook verification (Facebook-style challenge-response)
  • HMAC-SHA256 signature for security

Each webhook payload includes instanceId to identify which instance generated the event.

How Webhooks Work

  1. Configure your webhook URL in the Dashboard Settings
  2. Verify your webhook using the Facebook-style challenge verification
  3. Zapy sends HTTP POST requests to your URL when events occur
  4. Your server processes the event and responds with a 2xx status code

Webhook Configuration

Setting Up Your Webhook

  1. Go to Dashboard Settings
  2. Enter your HTTPS webhook URL
  3. (Optional) Set a webhook secret for signature verification
  4. Click Verify to complete the challenge-response verification
  5. Enable your webhook

Webhook Verification

Before receiving events, you must verify ownership of your webhook URL. Zapy uses a Facebook-style challenge-response verification:

  1. When you click "Verify", Zapy sends a GET request to your URL with:

    • hub.mode=subscribe
    • hub.verify_token=<random_token>
    • hub.challenge=<random_challenge>
  2. Your endpoint must respond with exactly the hub.challenge value

Example verification handler (Node.js/Express):

app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const challenge = req.query['hub.challenge'];

if (mode === 'subscribe') {
// Return the challenge to verify ownership
res.status(200).send(challenge);
} else {
res.status(400).send('Invalid request');
}
});

Webhook Payload Structure

All webhook payloads follow this structure:

{
"event": "message",
"instanceId": "your-instance-id",
"data": {
// Event-specific data
}
}
FieldTypeDescription
eventstringThe event type identifier
instanceIdstringThe WhatsApp instance ID that triggered the event
dataobjectEvent-specific payload data

Available Events

EventDescription
messageNew message received (text, media, polls, calls, etc.)
message-statusMessage delivery status update
qr-codeQR code generated for authentication
instance-statusInstance connection status changed
contact-createdNew contact discovered
contact-updatedContact information updated
contact-deduplicatedDuplicate contacts merged (LID-to-phone mapping)

Webhook Security

Signature Verification

If you configure a webhook secret, Zapy will sign all webhook payloads using HMAC-SHA256. The signature is included in the X-Webhook-Signature header.

Header format:

X-Webhook-Signature: sha256=<signature>

Verification example (Node.js):

const crypto = require('crypto');

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

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

// In your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(req.body, signature, YOUR_SECRET);

if (!isValid) {
return res.status(401).send('Invalid signature');
}

// Process the webhook...
res.status(200).send('OK');
});

Using the SDK helper:

import { verifyWebhookSignature } from '@zapyapi/sdk';

app.post('/webhook', express.json(), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);

if (!isValid) {
return res.status(401).send('Invalid signature');
}

// Process the webhook...
res.status(200).send('OK');
});

Request Headers

All webhook requests include these headers:

HeaderValue
Content-Typeapplication/json
User-AgentZapyAPI-Webhook/1.0
X-Webhook-Signaturesha256=<signature> (if secret configured)

Type-Safe Webhook Handling

Use the SDK's enums and type guards for type-safe webhook handling:

import {
ZapyWebhookPayload,
ZapyEventTypes,
ZapyMessageStatusEnum,
InstanceStatus,
isTextMessage,
isImageMessage
} from '@zapyapi/sdk';

function handleWebhook(payload: ZapyWebhookPayload) {
switch (payload.event) {
case ZapyEventTypes.MESSAGE:
// Handle incoming message
const message = payload.data;
console.log('Message from:', message.sender.name);

if (isTextMessage(message)) {
console.log('Text:', message.text);
} else if (isImageMessage(message)) {
console.log('Image caption:', message.caption);
}
break;

case ZapyEventTypes.MESSAGE_STATUS:
// Handle message status update
if (payload.data.status === ZapyMessageStatusEnum.READ) {
console.log('Message was read!');
}
break;

case ZapyEventTypes.INSTANCE_STATUS:
// Handle instance status change
console.log(`Instance ${payload.data.instanceId} status: ${payload.data.status}`);
if (payload.data.status === InstanceStatus.CONNECTED) {
console.log('Instance is now connected!');
}
break;

case ZapyEventTypes.QR_CODE:
// Handle QR code
console.log('QR Code:', payload.data.qr);
break;

case ZapyEventTypes.REACTION:
// Handle message reaction
console.log('Reaction:', payload.data.reaction);
break;

case ZapyEventTypes.PRESENCE:
// Handle presence update
console.log('Presence:', payload.data.presence);
break;

case ZapyEventTypes.CONTACT_DEDUPLICATED:
// Handle contact deduplication (important for syncing contacts locally)
console.log('Kept contact:', payload.data.keptContactId);
console.log('Deleted contact:', payload.data.deletedContactId);
// Update your database to merge these contacts
break;
}
}

Retry Queue & Failure Handling

Zapy includes a built-in retry queue for failed webhook deliveries:

  1. Initial delivery attempt - Immediate
  2. Retry attempts - Up to 5 retries with exponential backoff
  3. Pause on repeated failures - If too many failures occur, your webhook is paused
  4. Resume - You can resume paused webhooks from the dashboard

Queue Status

Monitor your webhook queue status in the dashboard:

  • Pending - Webhooks waiting to be delivered
  • Failed - Webhooks that failed after all retries
  • Paused - Webhooks paused due to repeated failures
  • Delivered - Successfully delivered webhooks

Best Practices

  1. Respond quickly - Return a 2xx status code within 10 seconds
  2. Process asynchronously - Queue events for background processing
  3. Verify signatures - Always validate the webhook signature in production
  4. Handle duplicates - Use the messageId to deduplicate events
  5. Use HTTPS - Webhook URLs must use HTTPS
  6. Keep secrets secure - Store your webhook secret securely
  7. Route by instanceId - Use the instanceId field to handle events from different instances