Skip to main content
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
Outboundmessage.queued, message.sent, message.delivered, message.failedTrack your outbound message delivery
Templatestemplate.status_changedTrack WhatsApp template approval status
Invitationsinvitation.status_changedTrack 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

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.
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"
  }
}

Contact Message Example

{
  "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

FieldTypeDescription
messageIdstringUnique message identifier
fromstringCustomer’s phone number (E.164 format)
tostringYour Sender’s phone number
channelstringsms or whatsapp
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 (contact messages)
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
profileNamestring | nullSender’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

FieldTypeDescription
messageIdstringUnique message identifier
fromstringCustomer’s phone number (E.164 format)
tostringYour Sender’s phone number
channelstringAlways whatsapp for unsupported messages
timestampnumberUnix 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

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

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

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

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

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.
{
  "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. 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

FieldTypeDescription
invitationIdstringInvitation identifier
clientNamestring | nullClient’s business name
clientEmailstring | nullClient’s email address
previousStatusstringStatus before the change
currentStatusstringNew status: in_progress, completed, or cancelled
senderIdstring(Only on completed) The new sender created for this client
wabaAccountIdstring(Only on completed) The WhatsApp Business Account ID

Status Flow

StatusDescription
pendingInvitation created, waiting for client
in_progressClient started the WhatsApp signup flow
completedClient successfully connected their WhatsApp Business Account
cancelledInvitation 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