Skip to content

Event Types

Complete reference for all webhook event types and their payloads

View .md

Zavu sends webhook events for message lifecycle stages. There are several categories:

CategoryEventsPurpose
Inboundconversation.new, message.inbound, message.unsupported, message.reactionReceive messages, reactions, and new contact notifications
Contactscontact.identity_linkedKnow when a WhatsApp contact's BSUID and phone number were tied together
Outboundmessage.queued, message.sent, message.delivered, message.failedTrack your outbound message delivery
Templatestemplate.status_changedTrack WhatsApp template approval status
Broadcastsbroadcast.status_changedTrack broadcast delivery lifecycle
Invitationsinvitation.status_changedTrack 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

FieldTypeDescription
phoneNumberstringNew contact's phone number (E.164 format)
channelstringChannel used: sms or whatsapp
firstMessageIdstringID of the first message
firstMessageTextstringContent of the first message
profileNamestring | nullContact'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

FieldTypeDescription
messageIdstringUnique message identifier
conversationIdstringThe 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.
contactIdstring | nullThe 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.
whatsappobjectWhatsApp only, when the BSUID is known: { bsuid, username? }.
fromstringCustomer's phone number (E.164 format), or their BSUID (e.g. US.13491208655302741918) for WhatsApp contacts who adopted a username and hid their number
tostringYour Sender's phone number
channelstringsms, whatsapp, telegram, instagram, email, etc.
messageTypestringMessage type: text, image, video, audio, document, sticker, location, contact
textstringMessage text or media caption (also button/list reply title for interactive messages)
contentobject | nullAdditional content (for non-text messages)
content.mediaIdstringMedia ID from WhatsApp (media messages)
content.mimeTypestringMIME type of the media
content.filenamestringFilename (for documents)
content.latitudenumberLatitude coordinate (location messages)
content.longitudenumberLongitude coordinate (location messages)
content.namestringLocation name (location messages)
content.addressstringLocation address (location messages)
content.contactsarrayContact cards: { name, phones: string[] }[]
content.interactiveReplyobjectInteractive reply details (when user clicks a button or list item)
content.interactiveReply.typestringbutton_reply or list_reply
content.interactiveReply.idstringID of the button/list item clicked
content.interactiveReply.titlestringTitle of the button/list item clicked
content.replyToMessageIdstringZavu ID of the quoted message. Only present when the quoted message exists in Zavu.
content.replyToProviderMessageIdstringProvider message ID (WhatsApp WAMID) of the quoted message. Always present on a reply.
content.replyToFromstringSender of the quoted message (E.164 format).
content.replyToTextstringTruncated snippet of the quoted message text (≤200 chars). Empty when the quoted message has no text (e.g. media).
content.replyToMessageTypestringType of the quoted message (text, image, video, etc.).
profileNamestring | nullSender's WhatsApp profile name. null for SMS.
providerTimestampnumber | nullProvider'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.
heldAtnumberPresent 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

TypeScript
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 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

FieldTypeDescription
messageIdstringUnique message identifier
conversationIdstringThe 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.
contactIdstring | nullThe 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.
whatsappobjectWhatsApp only, when the BSUID is known: { bsuid, username? }.
fromstringCustomer's phone number (E.164 format), or their BSUID (e.g. US.13491208655302741918) for WhatsApp contacts who adopted a username and hid their number
tostringYour Sender's phone number
channelstringAlways whatsapp for unsupported messages
timestampnumberUnix timestamp when message was received

Example Handler

TypeScript
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.

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

FieldTypeDescription
messageIdstringID of the message that was reacted to
providerMessageIdstringWhatsApp message ID
reaction.emojistringThe emoji used for the reaction
reaction.fromstringPhone number of the person who reacted
reaction.timestampnumberUnix timestamp of the reaction
reaction.actionstringadded 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

FieldTypeDescription
contactIdstringThe Zavu contact. If two contacts were merged, the one that remains.
mergedContactIdstringOnly when two contacts were merged: the contact that was folded into contactId. Move anything you stored under it to contactId.
bsuidstring | nullThe contact's business-scoped user ID. null only on phone_changed, for a contact WhatsApp never reported a BSUID for
phoneNumberstringThe contact's phone number (E.164 format)
reasonstringprovider_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)
previousPhoneNumberstringOnly with phone_changed: the number the contact had before
previousBsuidstringA 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

FieldTypeDescription
messageIdstringYour message identifier
tostringRecipient's phone number
channelstringsms or whatsapp
statusstringAlways 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

FieldTypeDescription
messageIdstringYour message identifier
tostringRecipient's phone number
channelstringsms or whatsapp
statusstringAlways 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)

FieldTypeDescription
fromstringYour WhatsApp Business phone number
messageTypestringMessage type: text, image, video, etc.
textstringMessage content or caption
sourcestringAlways whatsapp_business_app for echo messages
directionstringAlways 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

FieldTypeDescription
messageIdstringYour message identifier
tostringRecipient's phone number
channelstringsms or whatsapp
statusstringAlways 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

FieldTypeDescription
messageIdstringYour message identifier
tostringRecipient's phone number
channelstringsms or whatsapp
statusstringAlways failed for this event
errorCodestringError code from carrier/provider
errorMessagestringHuman-readable error description

Common Error Codes

CodeDescriptionSuggested Action
30003Unreachable destinationNumber may be invalid or disconnected
30005Unknown destinationVerify the phone number format
30006Landline not supportedSMS cannot be sent to landlines
30007Carrier rejectedMessage blocked by carrier
21610Unsubscribed recipientUser 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

FieldTypeDescription
templateIdstringTemplate identifier
templateNamestringTemplate name
languagestringTemplate language code
previousStatusstringStatus before the change
currentStatusstringNew status: approved, rejected, pending, disabled
rejectionReasonstring | nullReason for rejection (only present when currentStatus is rejected)

Status Values

StatusDescription
draftTemplate created locally, not yet submitted
pendingTemplate submitted to Meta, awaiting review
approvedTemplate approved and ready to use
rejectedTemplate rejected by Meta
disabledTemplate 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

FieldTypeDescription
invitationIdstringInvitation identifier
clientNamestring | nullClient's business name
clientEmailstring | nullClient's email address
connectionTypestringwhatsapp_waba or messenger
previousStatusstringStatus before the change
currentStatusstringNew status: in_progress, completed, failed, cancelled, or pending
failureReasonstringOnly on failed. Stable code such as fb_cancelled, fb_not_authorized or signup_abandoned
senderIdstringOnly on completed. The sender created for this client
wabaAccountIdstringOnly on completed WhatsApp invitations. The WhatsApp Business Account ID
connectedAccountobject | nullOnly on completed. The account that was linked: channel, id, name

When It Fires

TransitionCause
pending or failed to in_progressThe client opened the link and started the connection
in_progress to completedThe connection finished and the sender was created
in_progress to failedThe client closed Meta's dialog, denied a permission, or did not finish within an hour
any open status to cancelledYou cancelled the invitation
failed or cancelled to pendingYou 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

FieldTypeDescription
broadcastIdstringBroadcast identifier
namestringBroadcast campaign name
statusstringNew status after the change
previousStatusstringStatus before the change
channelstringBroadcast channel (sms, whatsapp, email, etc.)
totalContactsnumberTotal contacts in the broadcast

Status Flow

Code
draft → pending_review → approved → sending → completed ↓ rejected → escalated → rejected_final ↑ (edit + retry)
StatusDescription
draftInitial state, adding contacts
pending_reviewAI content review in progress
approvedReview passed, ready to send
rejectedContent rejected — edit and retry
escalatedSent to human review
rejected_finalRejected by human review (cannot appeal)
sendingMessages being delivered
completedAll messages processed
cancelledBroadcast 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:

JavaScript
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:

JavaScript
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