WhatsApp Webhooks
Webhooks are how WhatsApp communicates with your server in real time. Every incoming message from a user, every delivery status change, every template approval, and every user interaction (button click, emoji reaction, message deletion) is delivered to your webhook endpoint as an HTTP POST with a JSON payload.
200 response to acknowledge every webhook delivery. If your server fails to return HTTP 200, WhatsApp will consider the delivery unacknowledged and may retry or drop future notifications.
Webhook Setup Requirements
Before receiving webhook notifications, configure your webhook URL through the EnableX Portal for each WhatsApp Business phone number. Your server must meet the following requirements:
HTTPS Host — Your webhook URL must use HTTPS. Plain HTTP endpoints are not supported.
Valid TLS/SSL Certificate — A valid certificate issued by a trusted Certificate Authority is required. Self-signed certificates are not accepted.
Raw Body Parsing — JSON payloads are delivered as raw HTTP body content. Your server must parse the raw body directly — not as form-encoded data.
Securing Your Webhook — HTTP Basic Authentication
EnableX supports HTTP Basic Authentication for WhatsApp webhook delivery. When enabled, EnableX includes your credentials in every webhook POST so your server can verify the request is genuinely from EnableX before processing the payload.
To enable this, check the HTTP Authentication checkbox in the webhook configuration screen in the EnableX Portal and enter a username and password. EnableX will include these credentials in the Authorization header of every WhatsApp notification sent to your endpoint.
Your webhook URL must be publicly reachable over HTTPS. During local development, use a tunnelling tool such as ngrok http 3000 to expose your local server, then paste the generated HTTPS URL into the portal.
Acknowledging Webhook Notifications
Your webhook endpoint MUST return an HTTP 200 status code to confirm successful receipt of every notification. This acknowledgement must be sent in the same HTTP connection.
If your server does not return HTTP 200, WhatsApp treats the delivery as unacknowledged. Unacknowledged events may be retried, and persistent failures may cause WhatsApp to stop delivering notifications to your endpoint entirely.
Below is a minimal webhook handler in PHP, Node.js, and Python demonstrating the mandatory HTTP 200 acknowledgement:
<?php
// Read the incoming JSON payload
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
// Process the webhook data (your business logic here)
// ...
// MANDATORY: Return HTTP 200 to acknowledge receipt
header("HTTP/1.1 200 OK");
?>
// Node.js / Express
app.post('/webhook/whatsapp', (req, res) => {
const data = req.body;
// Process the webhook data (your business logic here)
// ...
// MANDATORY: Return HTTP 200 to acknowledge receipt
res.status(200).send('OK');
});
# Python / Flask
@app.route('/webhook/whatsapp', methods=['POST'])
def whatsapp_webhook():
data = request.get_json()
# Process the webhook data (your business logic here)
# ...
# MANDATORY: Return HTTP 200 to acknowledge receipt
return 'OK', 200
Types of Webhook Notifications
Your webhook receives four categories of notifications:
| Category | When It Fires |
|---|---|
| Incoming Messages | A user sends a message to your business number (text, media, location, contacts, sticker) |
| Delivery Notifications | An outgoing message changes status: sent, delivered, read, failed, deleted |
| Template Updates | A template submission is approved or rejected by Meta |
| User Actions & Reactions | A user reacts with emoji, replies, deletes a message, clicks a button, or selects a list item |
Incoming Messages
When a user sends a message to your business phone number — whether initiating a conversation or replying to one — WhatsApp POSTs the message to your webhook. The payload format is consistent across all content types, with a type-specific object containing the message data.
Standard Incoming Message Format
Every incoming message follows this structure. The type field tells you the content type, and the corresponding object (e.g., text, image, audio) contains type-specific data:
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "text",
"text": {
"body": "Hello, I need help with my order"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
messages | Array | Array of message objects (typically one per webhook post) |
messages[].id | String | Unique message ID — use for mark-as-read, tracking reactions |
messages[].from | String | Sender phone number with country code |
messages[].timestamp | String | Unix timestamp in UTC |
messages[].type | String | Content type: text, image, video, audio, document, location, contacts, sticker |
contacts | Array | Sender profile information |
contacts[].profile.name | String | Sender's WhatsApp display name |
contacts[].wa_id | String | Sender's WhatsApp ID (phone number with country code) |
business_phone | String | Your business phone number that received the message |
200 to acknowledge this webhook. Failure to acknowledge may result in missed future notifications.
Incoming Image Message
When a user sends an image, the image object contains a media ID (not a direct URL). Use the media ID with the Get Media API to retrieve the image content.
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "image",
"image": {
"mime_type": "image/jpeg",
"sha256": "a1b2c3d4e5f6...",
"id": "media_id_12345",
"caption": "Photo of the damaged item"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
image.mime_type | String | MIME type (e.g., image/jpeg, image/png) |
image.sha256 | String | SHA-256 hash of the image file |
image.id | String | Media ID — use with Get Media API to download the image |
image.caption | String | Optional caption sent with the image |
Incoming Video Message
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "video",
"video": {
"mime_type": "video/mp4",
"sha256": "f6e5d4c3b2a1...",
"id": "media_id_67890",
"caption": "Video of the issue",
"filename": "issue_recording.mp4"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
video.mime_type | String | MIME type (e.g., video/mp4) |
video.sha256 | String | SHA-256 hash |
video.id | String | Media ID for download |
video.caption | String | Optional caption |
video.filename | String | Original file name from sender's device |
Incoming Audio / Voice Message
Both audio files (selected from file manager) and voice recordings (recorded in WhatsApp) arrive as audio type. The voice field distinguishes them: true = voice recording, false = audio file.
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "audio",
"audio": {
"mime_type": "audio/ogg",
"sha256": "b2c3d4e5f6a1...",
"id": "media_id_11111",
"voice": true
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
audio.mime_type | String | MIME type (e.g., audio/ogg, audio/mp3) |
audio.sha256 | String | SHA-256 hash |
audio.id | String | Media ID for download |
audio.voice | Boolean | true if voice recording; false if audio file |
Incoming Document Message
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "document",
"document": {
"filename": "contract_v2.pdf",
"mime_type": "application/pdf",
"sha256": "c3d4e5f6a1b2...",
"id": "media_id_22222"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
Incoming Location Message
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "location",
"location": {
"latitude": "1.3521",
"longitude": "103.8198",
"name": "Raffles Place",
"address": "1 Raffles Place, Singapore 048616"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
location.latitude | String | Latitude |
location.longitude | String | Longitude |
location.name | String | Location name |
location.address | String | Location address |
Incoming Contact Message
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "contacts",
"contacts": [
{
"name": {
"formatted_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe"
},
"phones": [
{
"phone": "+6598765432",
"wa_id": "6598765432",
"type": "work"
}
],
"emails": [
{
"email": "[email protected]",
"type": "work"
}
],
"org": {
"company": "Acme Corp",
"department": "Sales",
"title": "Manager"
}
}
]
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
Incoming Sticker Message
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "sticker",
"sticker": {
"mime_type": "image/webp",
"sha256": "d4e5f6a1b2c3...",
"id": "media_id_33333",
"animated": false
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
sticker.mime_type | String | Always image/webp |
sticker.sha256 | String | SHA-256 hash |
sticker.id | String | Media ID for download |
sticker.animated | Boolean | true if animated sticker; false if static |
Delivery Notifications
When you send a message via API, WhatsApp tracks its delivery lifecycle and notifies your webhook at each stage. This is how you know whether your message was sent, delivered, read, or failed.
Message Delivery Lifecycle
A message progresses through these statuses:
| Status | Meaning |
|---|---|
sent | Message is in transit to the recipient |
delivered | Message reached the recipient's device |
read | Recipient opened and read the message (only if read receipts are enabled) |
failed | Message failed to process or deliver — error details included |
warning | Message contains a catalog item that doesn't exist |
deleted | Recipient deleted the message |
Sent Notification
Received when WhatsApp accepts your message and begins delivery. For the first message in a conversation, includes conversation tracking information with the 24-hour window expiry:
{
"statuses": [
{
"id": "wamid.HBgMOTE5...",
"recipient_id": "6599999999",
"status": "sent",
"timestamp": "1680000000",
"type": "message",
"conversation": {
"id": "conv_abc123",
"expiration_timestamp": "1680086400",
"origin": {
"type": "business_initiated"
}
},
"extra": "your_custom_tracking_data"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
id | String | The message ID returned when you sent the message via API |
recipient_id | String | Recipient phone number with country code |
status | String | sent — message is in transit |
timestamp | String | Unix timestamp (UTC) |
type | String | message for delivery notifications |
conversation.id | String | Conversation ID for reporting |
conversation.expiration_timestamp | String | When the 24-hour conversation window expires (UTC) |
conversation.origin.type | String | business_initiated or user_initiated |
extra | String | Custom data you passed when sending the message |
200 to acknowledge this webhook.
Delivered Notification
{
"statuses": [
{
"id": "wamid.HBgMOTE5...",
"recipient_id": "6599999999",
"status": "delivered",
"timestamp": "1680000060",
"type": "message",
"extra": "your_custom_tracking_data"
}
],
"business_phone": "918800899287"
}
Read Notification
Only received if the user has read receipts enabled on their device.
{
"statuses": [
{
"id": "wamid.HBgMOTE5...",
"recipient_id": "6599999999",
"status": "read",
"timestamp": "1680000120",
"type": "message",
"extra": "your_custom_tracking_data"
}
],
"business_phone": "918800899287"
}
Failed Notification
Received when a message fails to process or deliver. The errors array contains one or more error objects with codes and descriptions:
{
"statuses": [
{
"id": "wamid.HBgMOTE5...",
"recipient_id": "6599999999",
"status": "failed",
"timestamp": "1680000000",
"type": "message",
"errors": [
{
"code": 131047,
"title": "Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
"details": "Re-engagement requires a pre-approved template."
}
],
"extra": "your_custom_tracking_data"
}
],
"business_phone": "918800899287"
}
| Error Field | Type | Description |
|---|---|---|
errors[].code | Number | WhatsApp error code |
errors[].title | String | Human-readable error description |
errors[].details | String | Additional details (when available) |
Template Status Updates
When you submit a template for approval via the Templates API, Meta reviews it (typically within minutes). The approval or rejection is delivered to your webhook. You also receive notifications if an approved template is later paused or disabled due to spam complaints.
Template Approved
{
"statuses": [
{
"template_name": "order_shipped",
"template_id": 755147549669633,
"template_language": "en",
"reason": "NONE",
"status": "APPROVED",
"timestamp": 1680000000,
"type": "message_template_status_update",
"information": null
}
],
"business_phone": "918800899287"
}
Template Rejected
{
"statuses": [
{
"template_name": "promo_offer",
"template_id": 1179591852722215,
"template_language": "en",
"reason": "INVALID_FORMAT",
"status": "REJECTED",
"timestamp": 1680000000,
"type": "message_template_status_update",
"information": "Template body text exceeds character limit"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
template_name | String | Template name as defined during creation |
template_id | Number | Template ID returned at submission |
template_language | String | Language code |
reason | String | NONE for approved; rejection reason for rejected (e.g., INVALID_FORMAT) |
status | String | APPROVED or REJECTED |
timestamp | Number | Unix timestamp (UTC) |
type | String | Always message_template_status_update |
information | String | null for approved; additional context for rejected |
200 to acknowledge this webhook.
User Actions & Reactions
When users interact with messages — reacting with emoji, replying, deleting, clicking buttons, or selecting list items — these events are delivered to your webhook. The payload format follows the same structure as incoming messages.
Emoji Reaction
Triggered when a user reacts to a message with an emoji (thumbs up, heart, etc.):
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "reaction",
"reaction": {
"message_id": "wamid.ORIGINAL_MSG_ID",
"emoji": "\ud83d\udc4d"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
reaction.message_id | String | ID of the message being reacted to |
reaction.emoji | String | The emoji used for the reaction |
Message Deleted
Triggered when a user deletes a message they previously sent. The type is unknown with an error code:
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "unknown",
"errors": [
{
"code": 131051,
"title": "Message type unknown"
}
]
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
Reply to a Message
When a user replies to a specific message (long-press → reply), the incoming message includes a context object referencing the original message:
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "text",
"text": {
"body": "Yes, I agree with this"
},
"context": {
"from": "918800899287",
"id": "wamid.ORIGINAL_MSG_ID"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
context.from | String | Phone number of the sender of the original message |
context.id | String | Message ID of the original message being replied to |
context object is appended alongside the content-type object.
Quick Reply Button Click (Template)
When a user taps a quick reply button from a template message, the webhook delivers the button label and a context reference to the original message:
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "button",
"button": {
"payload": "Yes, Renew",
"text": "Yes, Renew"
},
"context": {
"from": "918800899287",
"id": "wamid.ORIGINAL_MSG_ID"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
button.payload | String | Label of the clicked button |
button.text | String | Label of the clicked button |
context.id | String | Message ID of the template message containing the button |
Interactive Button Click (Session Message)
When a user taps a button from an interactive session message, the webhook returns the button's id and title:
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "interactive",
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "btn_yes",
"title": "Yes, Please"
}
},
"context": {
"from": "918800899287",
"id": "wamid.ORIGINAL_MSG_ID"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
interactive.type | String | button_reply |
interactive.button_reply.id | String | The button ID you defined when sending the interactive message |
interactive.button_reply.title | String | The button label |
List Item Selection
When a user selects an item from an interactive list message:
{
"messages": [
{
"id": "wamid.HBgMOTE5...",
"from": "6591234567",
"timestamp": "1680000000",
"type": "interactive",
"interactive": {
"type": "list_reply",
"list_reply": {
"id": "plan_pro",
"title": "Pro",
"description": "$29/month — 10,000 messages"
}
},
"context": {
"from": "918800899287",
"id": "wamid.ORIGINAL_MSG_ID"
}
}
],
"contacts": [
{
"profile": {
"name": "John Smith"
},
"wa_id": "6591234567"
}
],
"business_phone": "918800899287"
}
| Field | Type | Description |
|---|---|---|
interactive.type | String | list_reply |
interactive.list_reply.id | String | The item ID you defined in the list |
interactive.list_reply.title | String | The item title |
interactive.list_reply.description | String | The item description |
200 to acknowledge ALL webhook notifications — incoming messages, delivery status updates, template updates, and user actions. If your webhook consistently fails to acknowledge, WhatsApp may stop sending notifications to your endpoint.