Event Types
Complete reference for all webhook event types and their payloads
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 |
| Contacts | contact.identity_linked | Know when a WhatsApp contact's BSUID and phone number were tied together |
| Outbound | message.queued, message.sent, message.delivered, message.failed | Track your outbound message delivery |
| Templates | template.status_changed | Track WhatsApp template approval status |
| Broadcasts | broadcast.status_changed | Track broadcast delivery lifecycle |
| Invitations | invitation.status_changed | Track partner invitation status changes |
Event Structure
All events follow this structure:
JSON{ "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.
JSON{ "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. |
message.inbound
Triggered when a customer sends you a message on any channel: SMS, WhatsApp, Telegram, Instagram, Messenger or email. This includes text, images, videos, audio, documents, stickers, locations, and contacts.
Text Message Example
JSON{ "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", "providerTimestamp": 1705312199000 } }
Username Contact (BSUID) Example
WhatsApp users can adopt a username and hide their phone number. Their messages arrive through the same event, but from carries a business-scoped user ID (BSUID) instead of an E.164 phone number:
JSON{ "id": "evt_1705312200000_bsu123", "type": "message.inbound", "timestamp": 1705312200000, "senderId": "snd_abc123", "projectId": "prj_xyz789", "data": { "messageId": "msg_bsu789", "conversationId": "js723987cyghwqxxaxcf590qd18axd95", "contactId": "jd7x2k3m4n5p6q7r8s9t0abc", "whatsapp": { "bsuid": "US.13491208655302741918", "username": "sheena" }, "from": "US.13491208655302741918", "to": "+13125559876", "channel": "whatsapp", "messageType": "text", "text": "Hi, I saw your listing", "profileName": "Sheena", "providerTimestamp": 1705312199000 } }
Image Message Example
JSON{ "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" } }
Location Message Example
JSON{ "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" } }
Contact Message Example
JSON{ "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:
JSON{ "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" } }
Reply / Quote Context Example
When a customer replies to (quotes) an earlier message, the content object carries the reply context so you can thread the conversation. When the quoted message exists in Zavu, you get its Zavu replyToMessageId, a text snippet, and its type:
JSON{ "id": "evt_1705312300000_rep123", "type": "message.inbound", "timestamp": 1705312300000, "senderId": "snd_abc123", "projectId": "prj_xyz789", "data": { "messageId": "msg_reply999", "from": "+14155551234", "to": "+13125559876", "channel": "whatsapp", "messageType": "text", "text": "Yes, that order is correct", "content": { "replyToMessageId": "msg_original123", "replyToProviderMessageId": "wamid.HBgL...ORIGINAL123", "replyToFrom": "+13125559876", "replyToText": "Your order #ORD-12345 has shipped.", "replyToMessageType": "text" }, "profileName": "John Doe" } }
If the quoted message is not stored in Zavu (e.g. an old or unknown message), the context degrades gracefully — you still get the provider ID and sender, but replyToMessageId, replyToText, and replyToMessageType are omitted:
JSON{ "id": "evt_1705312400000_rep456", "type": "message.inbound", "timestamp": 1705312400000, "senderId": "snd_abc123", "projectId": "prj_xyz789", "data": { "messageId": "msg_reply888", "from": "+14155551234", "to": "+13125559876", "channel": "whatsapp", "messageType": "text", "text": "Replying to an old message", "content": { "replyToProviderMessageId": "wamid.HBgL...OLD_UNKNOWN", "replyToFrom": "+13125559876" }, "profileName": "John Doe" } }
Data Fields
| Field | Type | Description |
|---|---|---|
messageId | string | Unique message identifier |
conversationId | string | The inbox thread this message was filed in. Always set: the webhook is sent once the message is in its thread, so the first message of a new thread carries the same id as conversation.new. Absent on reactions. |
contactId | string | null | The Zavu contact. Stays the same when a WhatsApp contact's from switches between BSUID and phone number, unless that switch reveals two contacts for one person: they are merged, and contact.identity_linked names the one that went away in mergedContactId. Absent on reactions. |
whatsapp | object | WhatsApp only, when the BSUID is known: { bsuid, username? }. |
from | string | Customer's phone number (E.164 format), or their BSUID (e.g. US.13491208655302741918) for WhatsApp contacts who adopted a username and hid their number |
to | string | Your Sender's phone number |
channel | string | sms, whatsapp, telegram, instagram, email, etc. |
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: { name, phones: string[] }[] |
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 |
content.replyToMessageId | string | Zavu ID of the quoted message. Only present when the quoted message exists in Zavu. |
content.replyToProviderMessageId | string | Provider message ID (WhatsApp WAMID) of the quoted message. Always present on a reply. |
content.replyToFrom | string | Sender of the quoted message (E.164 format). |
content.replyToText | string | Truncated snippet of the quoted message text (≤200 chars). Empty when the quoted message has no text (e.g. media). |
content.replyToMessageType | string | Type of the quoted message (text, image, video, etc.). |
profileName | string | null | Sender's WhatsApp profile name. null for SMS. |
providerTimestamp | number | null | Provider's original receive time (Unix ms) — when the channel received the message from the contact. Set for WhatsApp, Telegram, Instagram, and Messenger; null for SMS and email. Compare against the top-level timestamp (dispatch time) to detect delayed deliveries. |
heldAt | number | Present only on a message that arrived while the monthly allowance was spent and was delivered later, once the team paid: when it was held (Unix ms). See sending limits. Absent on every other message. |
Example Handler
TypeScriptasync 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 cannot be fully represented on the channel. On WhatsApp Cloud API, this includes polls, deleted messages, ephemeral messages, and some third-party stickers.
JSON{ "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 |
conversationId | string | The inbox thread this message was filed in. Always set: the webhook is sent once the message is in its thread, so the first message of a new thread carries the same id as conversation.new. Absent on reactions. |
contactId | string | null | The Zavu contact. Stays the same when a WhatsApp contact's from switches between BSUID and phone number, unless that switch reveals two contacts for one person: they are merged, and contact.identity_linked names the one that went away in mergedContactId. Absent on reactions. |
whatsapp | object | WhatsApp only, when the BSUID is known: { bsuid, username? }. |
from | string | Customer's phone number (E.164 format), or their BSUID (e.g. US.13491208655302741918) for WhatsApp contacts who adopted a username and hid their number |
to | string | Your Sender's phone number |
channel | string | Always whatsapp for unsupported messages |
timestamp | number | Unix timestamp when message was received |
Example Handler
TypeScriptasync 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.
JSON{ "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 |
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,
});
}
}
contact.identity_linked
Fired when a WhatsApp contact known by its business-scoped user ID (BSUID) is tied to a phone number, a contact known by phone receives a BSUID, or a contact changes their number. It fires once per link, not on every message. Subscribe to it on the sender's webhook.
JSON{ "id": "evt_1705312300000_def456", "type": "contact.identity_linked", "timestamp": 1705312300000, "senderId": "snd_abc123", "projectId": "prj_xyz789", "data": { "contactId": "jd7x2k3m4n5p6q7r8s9t0abc", "bsuid": "US.13491208655302741918", "phoneNumber": "+13125551234", "reason": "provider_reported" } }
Data Fields
| Field | Type | Description |
|---|---|---|
contactId | string | The Zavu contact. If two contacts were merged, the one that remains. |
mergedContactId | string | Only when two contacts were merged: the contact that was folded into contactId. Move anything you stored under it to contactId. |
bsuid | string | null | The contact's business-scoped user ID. null only on phone_changed, for a contact WhatsApp never reported a BSUID for |
phoneNumber | string | The contact's phone number (E.164 format) |
reason | string | provider_reported (WhatsApp sent phone and BSUID together on an inbound message), contact_shared (the contact answered a request_contact_info message), phone_changed (the contact changed their number), or bsuid_changed (WhatsApp reported a new BSUID for the same number) |
previousPhoneNumber | string | Only with phone_changed: the number the contact had before |
previousBsuid | string | A BSUID you may have stored that no longer identifies this contact: the one it had before, or the merged contact's when the remaining contact keeps its own |
After this event, from on message.inbound is still the BSUID whenever WhatsApp leaves the phone number out of a message. Use contactId to recognise the person. See Usernames & BSUIDs.
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.
JSON{ "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 accepted by the carrier or Meta. This confirms the message has left Zavu's systems and is being processed by the provider.
JSON{ "id": "evt_1705312200000_def456", "type": "message.sent", "timestamp": 1705312200000, "senderId": "snd_abc123", "projectId": "prj_xyz789", "data": { "messageId": "msg_abc123", "to": "+14155551234", "channel": "sms", "status": "sent" } }
Data Fields
| Field | Type | Description |
|---|---|---|
messageId | string | Your message identifier |
to | string | Recipient's phone number |
channel | string | sms or whatsapp |
status | string | Always sent for this event |
WhatsApp Business App Echo (Coexistence)
When using WhatsApp in coexistence mode, messages sent directly from the WhatsApp Business App also trigger message.sent events. These events include the full message content so you can sync outbound messages that were not sent through the Zavu API.
You can distinguish these from regular status updates by checking the source field.
JSON{ "id": "evt_1705312200000_echo456", "type": "message.sent", "timestamp": 1705312200000, "senderId": "snd_abc123", "projectId": "prj_xyz789", "data": { "messageId": "msg_echo789", "from": "+13125559876", "to": "+14155551234", "channel": "whatsapp", "messageType": "text", "status": "delivered", "text": "Hi! Thanks for reaching out. We'll get back to you shortly.", "source": "whatsapp_business_app", "direction": "outbound" } }
Additional Fields (Coexistence Only)
| Field | Type | Description |
|---|---|---|
from | string | Your WhatsApp Business phone number |
messageType | string | Message type: text, image, video, etc. |
text | string | Message content or caption |
source | string | Always whatsapp_business_app for echo messages |
direction | string | Always outbound for echo messages |
message.delivered
Triggered when your message is confirmed delivered to the recipient's device.
JSON{ "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 |
message.failed
Triggered when message delivery fails.
JSON{ "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.
JSON{ "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. Use it to follow a client through onboarding without polling.
It is delivered to the project webhook of the project that created the invitation. If you create invitations with a sub-account's API key, configure the webhook on that sub-account: a parent project does not receive its sub-accounts' events. If that project's default sender has a webhook subscribed to this event, it receives a copy too, with a top-level senderId.
JSON{ "id": "evt_1705312200000_inv123", "type": "invitation.status_changed", "timestamp": 1705312200000, "projectId": "prj_xyz789", "data": { "invitationId": "inv_xyz789", "clientName": "Acme Corp", "clientEmail": "contact@acme.com", "connectionType": "whatsapp_waba", "previousStatus": "in_progress", "currentStatus": "completed", "senderId": "snd_newclient123", "wabaAccountId": "waba_abc456", "connectedAccount": { "channel": "whatsapp", "id": "109876543210", "name": "Acme Corp" } } }
Data Fields
| Field | Type | Description |
|---|---|---|
invitationId | string | Invitation identifier |
clientName | string | null | Client's business name |
clientEmail | string | null | Client's email address |
connectionType | string | whatsapp_waba or messenger |
previousStatus | string | Status before the change |
currentStatus | string | New status: in_progress, completed, failed, cancelled, or pending |
failureReason | string | Only on failed. Stable code such as fb_cancelled, fb_not_authorized or signup_abandoned |
senderId | string | Only on completed. The sender created for this client |
wabaAccountId | string | Only on completed WhatsApp invitations. The WhatsApp Business Account ID |
connectedAccount | object | null | Only on completed. The account that was linked: channel, id, name |
When It Fires
| Transition | Cause |
|---|---|
pending or failed to in_progress | The client opened the link and started the connection |
in_progress to completed | The connection finished and the sender was created |
in_progress to failed | The client closed Meta's dialog, denied a permission, or did not finish within an hour |
any open status to cancelled | You cancelled the invitation |
failed or cancelled to pending | You resent the invitation from the dashboard, which issues a new link |
A change to the same status sends nothing, so retrying a step never produces a duplicate event.
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`);
} else if (currentStatus === "failed") {
// The link still works: the client can retry it
console.log(`${clientName} did not finish: ${data.failureReason}`);
}
}
Broadcast Events
broadcast.status_changed
Triggered when a broadcast changes status. This is a project-level event — configure it in your project webhook settings.
JSON{ "id": "evt_1705312200000_brd123", "type": "broadcast.status_changed", "timestamp": 1705312200000, "projectId": "prj_xyz789", "data": { "broadcastId": "brd_abc123", "name": "Black Friday Campaign", "status": "approved", "previousStatus": "pending_review", "channel": "sms", "totalContacts": 5000 } }
Data Fields
| Field | Type | Description |
|---|---|---|
broadcastId | string | Broadcast identifier |
name | string | Broadcast campaign name |
status | string | New status after the change |
previousStatus | string | Status before the change |
channel | string | Broadcast channel (sms, whatsapp, email, etc.) |
totalContacts | number | Total contacts in the broadcast |
Status Flow
Codedraft → pending_review → approved → sending → completed ↓ rejected → escalated → rejected_final ↑ (edit + retry)
| Status | Description |
|---|---|
draft | Initial state, adding contacts |
pending_review | AI content review in progress |
approved | Review passed, ready to send |
rejected | Content rejected — edit and retry |
escalated | Sent to human review |
rejected_final | Rejected by human review (cannot appeal) |
sending | Messages being delivered |
completed | All messages processed |
cancelled | Broadcast stopped |
Example Handler
async function handleBroadcastStatusChanged(data) {
const { broadcastId, name, status, previousStatus } = data;
switch (status) {
case "approved":
console.log(`Broadcast "${name}" approved — will begin sending`);
break;
case "rejected":
console.log(`Broadcast "${name}" rejected — edit content and retry`);
await notifyTeam({ message: `Broadcast rejected: ${name}` });
break;
case "completed":
console.log(`Broadcast "${name}" completed!`);
await generateReport(broadcastId);
break;
case "cancelled":
console.log(`Broadcast "${name}" was cancelled`);
break;
}
}
Best Practices
Idempotency
Webhook deliveries may be retried, so your handler should be idempotent:
JavaScriptasync 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:
JavaScriptapp.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'); } });