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.

Critical Requirement: Your webhook server MUST return an HTTP 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.

Development Testing

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:

CategoryWhen It Fires
Incoming MessagesA user sends a message to your business number (text, media, location, contacts, sticker)
Delivery NotificationsAn outgoing message changes status: sent, delivered, read, failed, deleted
Template UpdatesA template submission is approved or rejected by Meta
User Actions & ReactionsA 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"
}
FieldTypeDescription
messagesArrayArray of message objects (typically one per webhook post)
messages[].idStringUnique message ID — use for mark-as-read, tracking reactions
messages[].fromStringSender phone number with country code
messages[].timestampStringUnix timestamp in UTC
messages[].typeStringContent type: text, image, video, audio, document, location, contacts, sticker
contactsArraySender profile information
contacts[].profile.nameStringSender's WhatsApp display name
contacts[].wa_idStringSender's WhatsApp ID (phone number with country code)
business_phoneStringYour business phone number that received the message
Important: Your server MUST return HTTP 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"
}
FieldTypeDescription
image.mime_typeStringMIME type (e.g., image/jpeg, image/png)
image.sha256StringSHA-256 hash of the image file
image.idStringMedia ID — use with Get Media API to download the image
image.captionStringOptional 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"
}
FieldTypeDescription
video.mime_typeStringMIME type (e.g., video/mp4)
video.sha256StringSHA-256 hash
video.idStringMedia ID for download
video.captionStringOptional caption
video.filenameStringOriginal 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"
}
FieldTypeDescription
audio.mime_typeStringMIME type (e.g., audio/ogg, audio/mp3)
audio.sha256StringSHA-256 hash
audio.idStringMedia ID for download
audio.voiceBooleantrue 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"
}
FieldTypeDescription
location.latitudeStringLatitude
location.longitudeStringLongitude
location.nameStringLocation name
location.addressStringLocation 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"
}
FieldTypeDescription
sticker.mime_typeStringAlways image/webp
sticker.sha256StringSHA-256 hash
sticker.idStringMedia ID for download
sticker.animatedBooleantrue 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:

StatusMeaning
sentMessage is in transit to the recipient
deliveredMessage reached the recipient's device
readRecipient opened and read the message (only if read receipts are enabled)
failedMessage failed to process or deliver — error details included
warningMessage contains a catalog item that doesn't exist
deletedRecipient 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"
}
FieldTypeDescription
idStringThe message ID returned when you sent the message via API
recipient_idStringRecipient phone number with country code
statusStringsent — message is in transit
timestampStringUnix timestamp (UTC)
typeStringmessage for delivery notifications
conversation.idStringConversation ID for reporting
conversation.expiration_timestampStringWhen the 24-hour conversation window expires (UTC)
conversation.origin.typeStringbusiness_initiated or user_initiated
extraStringCustom data you passed when sending the message
Important: Your server MUST return HTTP 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 FieldTypeDescription
errors[].codeNumberWhatsApp error code
errors[].titleStringHuman-readable error description
errors[].detailsStringAdditional 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"
}
FieldTypeDescription
template_nameStringTemplate name as defined during creation
template_idNumberTemplate ID returned at submission
template_languageStringLanguage code
reasonStringNONE for approved; rejection reason for rejected (e.g., INVALID_FORMAT)
statusStringAPPROVED or REJECTED
timestampNumberUnix timestamp (UTC)
typeStringAlways message_template_status_update
informationStringnull for approved; additional context for rejected
Important: Your server MUST return HTTP 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"
}
FieldTypeDescription
reaction.message_idStringID of the message being reacted to
reaction.emojiStringThe 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"
}
FieldTypeDescription
context.fromStringPhone number of the sender of the original message
context.idStringMessage ID of the original message being replied to
Note: The reply can contain any content type (text, image, video, etc.). The 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"
}
FieldTypeDescription
button.payloadStringLabel of the clicked button
button.textStringLabel of the clicked button
context.idStringMessage 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"
}
FieldTypeDescription
interactive.typeStringbutton_reply
interactive.button_reply.idStringThe button ID you defined when sending the interactive message
interactive.button_reply.titleStringThe 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"
}
FieldTypeDescription
interactive.typeStringlist_reply
interactive.list_reply.idStringThe item ID you defined in the list
interactive.list_reply.titleStringThe item title
interactive.list_reply.descriptionStringThe item description
Important: Your server MUST return HTTP 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.