Zavu sends webhook events for message lifecycle stages. There are several categories:
| Category | Events | Purpose |
|---|
| Inbound | conversation.new, message.inbound, message.unsupported, message.reaction | Receive messages, reactions, and new contact notifications |
| Outbound | message.queued, message.sent, message.delivered, message.failed | Track your outbound message delivery |
| Templates | template.status_changed | Track WhatsApp template approval status |
| Invitations | invitation.status_changed | Track partner invitation status changes |
Event Structure
All events follow this structure:
{
"id": "evt_1705312200000_abc123",
"type": "message.inbound",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
// Event-specific payload
}
}
Inbound Events
conversation.new
Triggered when a new contact sends you their first message. This is useful for tracking new leads or customers.
{
"id": "evt_1705312200000_new123",
"type": "conversation.new",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"phoneNumber": "+14155551234",
"channel": "whatsapp",
"firstMessageId": "msg_xyz789",
"firstMessageText": "Hi, I'm interested in your product",
"profileName": "John Doe"
}
}
Data Fields
| Field | Type | Description |
|---|
phoneNumber | string | New contact’s phone number (E.164 format) |
channel | string | Channel used: sms or whatsapp |
firstMessageId | string | ID of the first message |
firstMessageText | string | Content of the first message |
profileName | string | null | Contact’s WhatsApp profile name. null for SMS. |
You’ll also receive a message.inbound event for the same message. Use conversation.new specifically for new lead notifications or CRM integrations.
message.inbound
Triggered when a customer sends you a message via SMS or WhatsApp. This includes text messages, images, videos, audio, documents, and stickers.
Text Message Example
{
"id": "evt_1705312200000_abc123",
"type": "message.inbound",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_xyz789",
"from": "+14155551234",
"to": "+13125559876",
"channel": "whatsapp",
"messageType": "text",
"text": "Hi, I have a question about my order",
"profileName": "John Doe"
}
}
Image Message Example
{
"id": "evt_1705312200000_img456",
"type": "message.inbound",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_img789",
"from": "+14155551234",
"to": "+13125559876",
"channel": "whatsapp",
"messageType": "image",
"text": "Check out this product",
"content": {
"mediaId": "1234567890",
"mimeType": "image/jpeg"
},
"profileName": "John Doe"
}
}
For media messages (image, video, audio, document, sticker), the content.mediaId is provided initially. The media is then downloaded and stored, and subsequent reads of the message will include a content.mediaUrl with the permanent URL.
Location Message Example
{
"id": "evt_1705312200000_loc789",
"type": "message.inbound",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_loc123",
"from": "+14155551234",
"to": "+13125559876",
"channel": "whatsapp",
"messageType": "location",
"text": "",
"content": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "San Francisco",
"address": "San Francisco, CA, USA"
},
"profileName": "John Doe"
}
}
{
"id": "evt_1705312200000_cnt456",
"type": "message.inbound",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_cnt789",
"from": "+14155551234",
"to": "+13125559876",
"channel": "whatsapp",
"messageType": "contact",
"text": "",
"content": {
"contacts": [
{
"name": "Jane Smith",
"phones": [
{ "phone": "+14155559999", "type": "MOBILE" }
],
"emails": [
{ "email": "jane@example.com", "type": "WORK" }
]
}
]
},
"profileName": "John Doe"
}
}
Interactive Reply Example
When a user clicks a button or selects a list item that you sent, you’ll receive the reply with the button/item ID:
{
"id": "evt_1705312200000_int123",
"type": "message.inbound",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_int456",
"from": "+14155551234",
"to": "+13125559876",
"channel": "whatsapp",
"messageType": "text",
"text": "Yes, I'm interested",
"content": {
"interactiveReply": {
"type": "button_reply",
"id": "btn_interested",
"title": "Yes, I'm interested"
}
},
"profileName": "John Doe"
}
}
The interactiveReply.id is the ID you specified when sending the button or list message. Use this to identify which option the user selected.
Data Fields
| Field | Type | Description |
|---|
messageId | string | Unique message identifier |
from | string | Customer’s phone number (E.164 format) |
to | string | Your Sender’s phone number |
channel | string | sms or whatsapp |
messageType | string | Message type: text, image, video, audio, document, sticker, location, contact |
text | string | Message text or media caption (also button/list reply title for interactive messages) |
content | object | null | Additional content (for non-text messages) |
content.mediaId | string | Media ID from WhatsApp (media messages) |
content.mimeType | string | MIME type of the media |
content.filename | string | Filename (for documents) |
content.latitude | number | Latitude coordinate (location messages) |
content.longitude | number | Longitude coordinate (location messages) |
content.name | string | Location name (location messages) |
content.address | string | Location address (location messages) |
content.contacts | array | Contact cards (contact messages) |
content.interactiveReply | object | Interactive reply details (when user clicks a button or list item) |
content.interactiveReply.type | string | button_reply or list_reply |
content.interactiveReply.id | string | ID of the button/list item clicked |
content.interactiveReply.title | string | Title of the button/list item clicked |
profileName | string | null | Sender’s WhatsApp profile name. null for SMS. |
Example Handler
async function handleInboundMessage(data) {
const { from, text, channel } = data;
// Store the message
await db.messages.create({
from,
text,
channel,
receivedAt: new Date()
});
// Send auto-reply if needed
if (text.toLowerCase().includes("help")) {
await zavu.messages.send({
to: from,
text: "Thanks for reaching out! A support agent will contact you shortly."
});
}
}
message.unsupported
Triggered when a customer sends a message type that is not supported by WhatsApp Cloud API. This includes polls, reactions, deleted messages, ephemeral messages, and third-party stickers.
{
"id": "evt_1705312200000_unsup123",
"type": "message.unsupported",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_xyz789",
"from": "+14155551234",
"to": "+13125559876",
"channel": "whatsapp",
"timestamp": 1705312200000
}
}
Data Fields
| Field | Type | Description |
|---|
messageId | string | Unique message identifier |
from | string | Customer’s phone number (E.164 format) |
to | string | Your Sender’s phone number |
channel | string | Always whatsapp for unsupported messages |
timestamp | number | Unix timestamp when message was received |
Unsupported message types include:
- Polls - Survey/voting messages
- Deleted messages - Messages deleted by the sender
- Ephemeral messages - Disappearing messages
- Third-party stickers - Stickers not meeting WhatsApp specifications
These limitations are from WhatsApp Cloud API, not Zavu.
Example Handler
async function handleUnsupportedMessage(data) {
const { from, messageId } = data;
// Log unsupported message for analytics
await db.unsupportedMessages.create({
messageId,
from,
receivedAt: new Date()
});
// Optionally notify the customer
await zavu.messages.send({
to: from,
text: "We received your message, but this message type is not supported. Please send a text message instead."
});
}
message.reaction
Triggered when a customer reacts to a message with an emoji (WhatsApp only). This event is sent when a reaction is added or removed from a message.
{
"id": "evt_1705312200000_react123",
"type": "message.reaction",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_xyz789",
"providerMessageId": "wamid.HBgLMTU1NT...",
"reaction": {
"emoji": "👍",
"from": "+14155551234",
"timestamp": 1705312200000,
"action": "added"
}
}
}
Data Fields
| Field | Type | Description |
|---|
messageId | string | ID of the message that was reacted to |
providerMessageId | string | WhatsApp message ID |
reaction.emoji | string | The emoji used for the reaction |
reaction.from | string | Phone number of the person who reacted |
reaction.timestamp | number | Unix timestamp of the reaction |
reaction.action | string | added when reaction is added, removed when reaction is removed |
When a user removes a reaction, action will be removed and emoji will be an empty string.
Example Handler
async function handleReaction(data) {
const { messageId, reaction } = data;
const { emoji, from, action } = reaction;
if (action === "added") {
// Store the reaction
await db.reactions.create({
messageId,
emoji,
from,
createdAt: new Date()
});
// Track engagement metrics
await analytics.track("message_reaction", {
messageId,
emoji,
});
} else if (action === "removed") {
// Remove the reaction
await db.reactions.delete({
messageId,
from,
});
}
}
Outbound Events
These events help you track the delivery status of messages you send. Subscribe to these if you need delivery confirmations or failure notifications.
message.queued
Triggered when your message is accepted and queued for delivery.
{
"id": "evt_1705312200000_abc456",
"type": "message.queued",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_abc123",
"to": "+14155551234",
"channel": "sms",
"status": "queued"
}
}
Data Fields
| Field | Type | Description |
|---|
messageId | string | Your message identifier |
to | string | Recipient’s phone number |
channel | string | sms or whatsapp |
status | string | Always queued for this event |
message.sent
Triggered when your outbound message is successfully sent to the carrier or Meta.
{
"id": "evt_1705312200000_def456",
"type": "message.sent",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_abc123",
"to": "+14155551234",
"channel": "sms",
"status": "sending"
}
}
Data Fields
| Field | Type | Description |
|---|
messageId | string | Your message identifier |
to | string | Recipient’s phone number |
channel | string | sms or whatsapp |
status | string | Always sending for this event |
message.delivered
Triggered when your message is confirmed delivered to the recipient’s device.
{
"id": "evt_1705312200000_ghi789",
"type": "message.delivered",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_abc123",
"to": "+14155551234",
"channel": "whatsapp",
"status": "delivered"
}
}
Data Fields
| Field | Type | Description |
|---|
messageId | string | Your message identifier |
to | string | Recipient’s phone number |
channel | string | sms or whatsapp |
status | string | Always delivered for this event |
Delivery confirmation availability depends on the carrier and channel. WhatsApp provides reliable delivery receipts, while SMS delivery confirmations vary by carrier.
message.failed
Triggered when message delivery fails.
{
"id": "evt_1705312200000_jkl012",
"type": "message.failed",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"messageId": "msg_abc123",
"to": "+14155551234",
"channel": "sms",
"status": "failed",
"errorCode": "30003",
"errorMessage": "Unreachable destination handset"
}
}
Data Fields
| Field | Type | Description |
|---|
messageId | string | Your message identifier |
to | string | Recipient’s phone number |
channel | string | sms or whatsapp |
status | string | Always failed for this event |
errorCode | string | Error code from carrier/provider |
errorMessage | string | Human-readable error description |
Common Error Codes
| Code | Description | Suggested Action |
|---|
30003 | Unreachable destination | Number may be invalid or disconnected |
30005 | Unknown destination | Verify the phone number format |
30006 | Landline not supported | SMS cannot be sent to landlines |
30007 | Carrier rejected | Message blocked by carrier |
21610 | Unsubscribed recipient | User opted out of messages |
Example Handler
async function handleDeliveryFailure(data) {
const { messageId, errorCode, errorMessage } = data;
// Log the failure
await db.messages.update(messageId, {
status: "failed",
errorCode,
errorMessage,
failedAt: new Date()
});
// Alert your team for critical failures
if (["30007", "21610"].includes(errorCode)) {
await alertTeam({
type: "message_blocked",
messageId,
reason: errorMessage
});
}
}
Template Events
template.status_changed
Triggered when a WhatsApp template’s approval status changes. This is useful for tracking when templates are approved, rejected, or disabled by Meta.
{
"id": "evt_1705312200000_tmpl123",
"type": "template.status_changed",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"templateId": "tmpl_xyz789",
"templateName": "order_confirmation",
"language": "en",
"previousStatus": "pending",
"currentStatus": "approved",
"rejectionReason": null
}
}
Data Fields
| Field | Type | Description |
|---|
templateId | string | Template identifier |
templateName | string | Template name |
language | string | Template language code |
previousStatus | string | Status before the change |
currentStatus | string | New status: approved, rejected, pending, disabled |
rejectionReason | string | null | Reason for rejection (only present when currentStatus is rejected) |
Status Values
| Status | Description |
|---|
draft | Template created locally, not yet submitted |
pending | Template submitted to Meta, awaiting review |
approved | Template approved and ready to use |
rejected | Template rejected by Meta |
disabled | Template was approved but later disabled by Meta |
Example Handler
async function handleTemplateStatusChange(data) {
const { templateId, templateName, currentStatus, rejectionReason } = data;
// Update template status in your database
await db.templates.update(templateId, {
status: currentStatus,
rejectionReason,
updatedAt: new Date()
});
if (currentStatus === "approved") {
await notifyTeam({
message: `Template "${templateName}" has been approved!`,
type: "success"
});
} else if (currentStatus === "rejected") {
await notifyTeam({
message: `Template "${templateName}" was rejected: ${rejectionReason}`,
type: "error"
});
}
}
Partner Invitation Events
invitation.status_changed
Triggered when a partner invitation changes status. This is useful for tracking when your clients complete the WhatsApp onboarding process.
{
"id": "evt_1705312200000_inv123",
"type": "invitation.status_changed",
"timestamp": 1705312200000,
"senderId": "snd_abc123",
"projectId": "prj_xyz789",
"data": {
"invitationId": "inv_xyz789",
"clientName": "Acme Corp",
"clientEmail": "contact@acme.com",
"previousStatus": "pending",
"currentStatus": "completed",
"senderId": "snd_newclient123",
"wabaAccountId": "waba_abc456"
}
}
Data Fields
| Field | Type | Description |
|---|
invitationId | string | Invitation identifier |
clientName | string | null | Client’s business name |
clientEmail | string | null | Client’s email address |
previousStatus | string | Status before the change |
currentStatus | string | New status: in_progress, completed, or cancelled |
senderId | string | (Only on completed) The new sender created for this client |
wabaAccountId | string | (Only on completed) The WhatsApp Business Account ID |
Status Flow
| Status | Description |
|---|
pending | Invitation created, waiting for client |
in_progress | Client started the WhatsApp signup flow |
completed | Client successfully connected their WhatsApp Business Account |
cancelled | Invitation was cancelled by the partner |
Example Handler
async function handleInvitationStatusChanged(data) {
const { invitationId, clientName, currentStatus, senderId } = data;
if (currentStatus === "completed") {
// Client successfully onboarded
await notifyTeam({
message: `${clientName} has connected their WhatsApp!`,
senderId,
});
// Create templates for the new client
await createDefaultTemplates(senderId);
} else if (currentStatus === "in_progress") {
// Client started the process
console.log(`${clientName} is completing WhatsApp setup`);
}
}
Best Practices
Idempotency
Webhook deliveries may be retried, so your handler should be idempotent:
async function handleEvent(event) {
// Check if we've already processed this event
const existing = await db.processedEvents.find(event.id);
if (existing) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
// Process the event
await processEventLogic(event);
// Mark as processed
await db.processedEvents.create({ eventId: event.id });
}
Error Handling
Always wrap your handlers in try-catch to prevent unhandled exceptions:
app.post('/webhooks/zavu', async (req, res) => {
try {
await handleEvent(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook handling error:', error);
// Return 200 to prevent retries if the error is not transient
res.status(200).send('OK');
}
});
Next Steps