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
- Configure your webhook URL in the Dashboard Settings
- Verify your webhook using the Facebook-style challenge verification
- Zapy sends HTTP POST requests to your URL when events occur
- Your server processes the event and responds with a 2xx status code
Webhook Configuration
Setting Up Your Webhook
- Go to Dashboard Settings
- Enter your HTTPS webhook URL
- (Optional) Set a webhook secret for signature verification
- Click Verify to complete the challenge-response verification
- Enable your webhook
Webhook Verification
Before receiving events, you must verify ownership of your webhook URL. Zapy uses a Facebook-style challenge-response verification:
-
When you click "Verify", Zapy sends a GET request to your URL with:
hub.mode=subscribehub.verify_token=<random_token>hub.challenge=<random_challenge>
-
Your endpoint must respond with exactly the
hub.challengevalue
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
}
}
| Field | Type | Description |
|---|---|---|
event | string | The event type identifier |
instanceId | string | The WhatsApp instance ID that triggered the event |
data | object | Event-specific payload data |
Available Events
| Event | Description |
|---|---|
message | New message received (text, media, polls, calls, etc.) |
message-status | Message delivery status update |
qr-code | QR code generated for authentication |
instance-status | Instance connection status changed |
contact-created | New contact discovered |
contact-updated | Contact information updated |
contact-deduplicated | Duplicate 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:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | ZapyAPI-Webhook/1.0 |
X-Webhook-Signature | sha256=<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:
- Initial delivery attempt - Immediate
- Retry attempts - Up to 5 retries with exponential backoff
- Pause on repeated failures - If too many failures occur, your webhook is paused
- 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
- Respond quickly - Return a 2xx status code within 10 seconds
- Process asynchronously - Queue events for background processing
- Verify signatures - Always validate the webhook signature in production
- Handle duplicates - Use the
messageIdto deduplicate events - Use HTTPS - Webhook URLs must use HTTPS
- Keep secrets secure - Store your webhook secret securely
- Route by instanceId - Use the
instanceIdfield to handle events from different instances