Receiving WhatsApp Events via Webhook
Superfone can forward the WhatsApp events that arrive on your business number — incoming customer messages, delivery/read receipts, and other account events — to an HTTPS endpoint you control. Use this to feed your own systems: an AI agent, a CRM, a ticketing tool, or custom automation.
This is an outbound webhook. Superfone receives events from Meta on your behalf and relays them to your endpoint. It runs alongside the Superfone dashboard — it does not replace it.
How it works
- A customer messages your WhatsApp business number.
- Meta delivers the event to Superfone.
- Superfone forwards that event to each enabled webhook endpoint configured on your account.
The payload Superfone forwards is Meta's original WhatsApp Cloud API webhook payload,
relayed as-is. You therefore code against Meta's standard webhook schema, documented
below. The one addition is on inbound media messages, where Superfone adds a downloadable
media_url and a media_status — see Inbound media message.
For the authoritative field-level reference, see Meta's
Webhooks Components reference.
Registering your endpoint
Webhook forwarding is configured per WhatsApp account. Each entry has:
| Field | Type | Description |
|---|---|---|
url | string | Your HTTPS endpoint that will receive the POST requests. |
enabled | boolean | Whether forwarding to this endpoint is active. |
events | string[] | Event categories (reserved for future use; all enabled events are currently forwarded). |
The configuration field that holds your HTTPS endpoint is named url in the system.
Just provide your HTTPS endpoint to the Superfone team, and we'll register it on your WhatsApp account so you start receiving the webhook at that endpoint. There is currently no self-serve API to manage webhook endpoints — registration is handled by Superfone.
Requirements for your endpoint:
- Must be reachable over HTTPS.
- Must accept an HTTP
POSTwith a JSON body. - Must respond
200quickly to acknowledge receipt.
The request Superfone sends
| Property | Value |
|---|---|
| Method | POST |
| Header | x-app-name: dragonfly — the only header Superfone sets; identifies the sender |
| Body | Meta's raw WhatsApp webhook payload, sent as JSON (see schema) |
x-app-name: dragonfly is the only header Superfone sets. Content-Type is not set
deliberately — the body is simply sent as JSON. There is no payload signature (no
X-Hub-Signature-256) and no hub.challenge verification handshake against your
endpoint — those apply only between Meta and Superfone, not on this hop. Don't treat the
request as authenticated; verify it on your side (for example, a secret token in the
endpoint path).
Payload schema
Every payload uses Meta's envelope:
object: "whatsapp_business_account"
entry[]
id // your WhatsApp Business Account ID
changes[]
field // "messages" (covers both inbound messages and statuses)
value
messaging_product // "whatsapp"
metadata
display_phone_number // your business number
phone_number_id // your phone number ID
contacts[] // sender profile (present for inbound messages)
profile.name
wa_id // sender's WhatsApp number
messages[] // present for inbound customer messages
statuses[] // present for delivery/read/failed receipts
Inbound text message
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "<WHATSAPP_BUSINESS_ACCOUNT_ID>",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "918888888888",
"phone_number_id": "1234567890"
},
"contacts": [
{ "profile": { "name": "Rahul" }, "wa_id": "919876543210" }
],
"messages": [
{
"from": "919876543210",
"id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgU...",
"timestamp": "1700000000",
"type": "text",
"text": { "body": "Hi, is this available?" }
}
]
}
}
]
}
]
}
Inbound media message
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "<WHATSAPP_BUSINESS_ACCOUNT_ID>",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "918888888888",
"phone_number_id": "1234567890"
},
"contacts": [
{ "profile": { "name": "Rahul" }, "wa_id": "919876543210" }
],
"messages": [
{
"from": "919876543210",
"id": "wamid.HBgMOTE5ODc2NTQzMjEw...",
"timestamp": "1700000000",
"type": "image",
"image": {
"id": "<MEDIA_ID>",
"mime_type": "image/jpeg",
"sha256": "<hash>",
"caption": "optional caption",
"media_url": "https://<superfone-cdn>/whatsapp_media/1234567890/<MEDIA_ID>.jpeg",
"media_status": "ok"
}
}
]
}
}
]
}
]
}
Meta delivers media as a media_id only, which you cannot download yourself (it needs
Superfone's authenticated Meta credentials, and Meta's media links expire). So before
forwarding a media message, Superfone downloads the file, hosts it on its CDN, and adds two
fields to the media object:
media_status | media_url | Meaning |
|---|---|---|
ok | A publicly fetchable HTTPS URL | The media is ready — download it directly from media_url. |
failed | null | Superfone could not fetch the media from Meta after retrying. The event is still forwarded so you don't lose the message; only id (the media_id) is available. |
Document, audio, video, and sticker messages use the same shape under document / audio /
video / sticker. The original id, mime_type, sha256, and caption fields are
unchanged.
Because the file is downloaded and re-hosted first, a media event reaches your endpoint a few
seconds after Meta delivers it — up to roughly 15–20 seconds when retries are needed before
media_status: "failed". A text message the customer sent after an image can therefore
arrive before it. Order messages by messages[].timestamp, not by arrival time.
media_url is absentThe enrichment applies when a webhook carries a single inbound media message, which is
how Meta delivers almost all inbound messages. If a single webhook batches several messages
together, its media messages are forwarded immediately without media_url / media_status.
Treat both fields as optional: if they are missing, only the media_id is available.
Status update (sent / delivered / read / failed)
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "<WHATSAPP_BUSINESS_ACCOUNT_ID>",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "918888888888",
"phone_number_id": "1234567890"
},
"statuses": [
{
"id": "wamid.HBgMOTE5ODc2NTQzMjEw...",
"status": "delivered",
"timestamp": "1700000000",
"recipient_id": "919876543210",
"conversation": {
"id": "b1afb7b7d806c1c32fc190d5ff422331",
"origin": { "type": "service" }
},
"pricing": {
"billable": true,
"category": "service",
"pricing_model": "CBP"
}
}
]
}
}
]
}
]
}
status values are sent, delivered, read, failed, and deleted (the customer
deleted their message). The conversation and pricing objects are delivered on most
receipts. For failed, Meta includes an errors[] array — see Status object.
Field descriptions
Envelope
| Field | Type | Description |
|---|---|---|
object | string | Always "whatsapp_business_account". Identifies the payload as a WhatsApp Business Account event. |
entry[] | object[] | One entry per WhatsApp Business Account. May contain multiple entries — always iterate. |
entry[].id | string | Your WhatsApp Business Account (WABA) ID. |
entry[].changes[] | object[] | The list of changes (events) in this entry. May contain multiple changes — always iterate. |
entry[].changes[].field | string | The change topic. For messages and status receipts this is "messages". |
entry[].changes[].value | object | The event payload. Contains metadata, and either messages or statuses. |
value object
| Field | Type | Description |
|---|---|---|
messaging_product | string | Always "whatsapp". |
metadata.display_phone_number | string | Your business number in display form (e.g. 918888888888). |
metadata.phone_number_id | string | Your business number's ID. This is the routing key — use it to tell which number the event is for. |
contacts[] | object[] | Sender profile. Present for inbound messages; absent on status receipts. |
contacts[].wa_id | string | The customer's WhatsApp number (their ID on WhatsApp). |
contacts[].profile.name | string | The customer's WhatsApp profile (display) name. |
messages[] | object[] | Present for inbound customer messages. See Message object. |
statuses[] | object[] | Present for delivery/read/failed receipts. See Status object. |
Message object
Each item in messages[] describes one inbound message.
| Field | Type | Description |
|---|---|---|
id | string | Message ID (wamid). Globally unique — useful as a key to dedupe defensively if you need it. |
from | string | The sender's WhatsApp number (the customer). |
timestamp | string | Unix epoch seconds (as a string) when Meta received the message. |
type | string | The message type: text, image, document, audio, video, sticker, location, contacts, interactive, reaction, button, order, system, request_welcome, or unsupported. Tolerate types you don't handle. |
<type> | object | Type-specific content, keyed by the value of type (e.g. a text message has a text object, an image message has an image object). |
context | object | Optional. Present when the message is a reply or forward. Contains from and the id (wamid) of the message being replied to. |
Type-specific content
When type is | Field | Type | Description |
|---|---|---|---|
text | text.body | string | The message text. |
image / video / document / audio / sticker | <type>.id | string | Meta media_id. Not directly downloadable — use media_url instead. |
<type>.media_url | string | null | Added by Superfone. Public HTTPS URL of the media on Superfone's CDN; null when media_status is failed. See Inbound media message. | |
<type>.media_status | string | Added by Superfone. ok or failed. | |
<type>.mime_type | string | MIME type of the media, e.g. image/jpeg. | |
<type>.sha256 | string | SHA-256 hash of the media file. | |
<type>.caption | string | Optional. Caption (image, video, document). | |
document.filename | string | Optional. Original filename (document only). | |
location | location.latitude, location.longitude | number | Coordinates shared by the customer. name and address may also be present. |
reaction | reaction.emoji | string | The emoji reaction. reaction.message_id is the wamid being reacted to. |
reaction.message_id | string | The wamid of the message the customer reacted to. | |
button | button.text, button.payload | string | A tap on a template quick-reply button. text is the button label; payload is the configured payload. |
interactive | interactive.type | string | A reply to an interactive message: button_reply or list_reply. |
interactive.button_reply | object | Present when interactive.type is button_reply. Contains id and title. | |
interactive.list_reply | object | Present when interactive.type is list_reply. Contains id, title, and optional description. |
Status object
Each item in statuses[] is a receipt for a message you sent.
| Field | Type | Description |
|---|---|---|
id | string | The wamid of the message this receipt is for. Match it to a message you previously sent. |
status | string | sent, delivered, read, failed, or deleted. |
timestamp | string | Unix epoch seconds (as a string) when the status changed. |
recipient_id | string | The customer's WhatsApp number the message was sent to. |
conversation | object | Usually present. The messaging conversation this receipt belongs to. Contains id and origin.type (e.g. service, marketing, utility, authentication). |
pricing | object | Usually present. Billing info for the conversation: billable (boolean), category, and pricing_model. |
errors[] | object[] | Present only when status is failed. See Error object. |
Error object
Each item in statuses[].errors[] describes why a message failed.
| Field | Type | Description |
|---|---|---|
code | number | Meta error code. Look it up in Meta's error-codes reference. |
title | string | Short error title, e.g. Message undeliverable. |
message | string | Optional. Human-readable error message (often equal to title). |
error_data.details | string | Optional. Additional detail about the failure. |
href | string | Optional. Link to the relevant Meta error-codes documentation. |
How your endpoint should behave
- Return
200immediately, then process asynchronously. Superfone does not retry, so a slow or failing response means the event is lost — keep your handler fast and do the heavy work after acknowledging. - Delivery is at-most-once. Superfone sends each event once and does not retry, so
if your endpoint is unreachable or returns a non-2xx, the event is missed (lost) — it
is never redelivered. Duplicates are unlikely and can only happen if Meta delivers the
same upstream event to Superfone more than once; dedupe by
messages[].id(wamid) as a defensive measure only. - Iterate
entry[] → changes[] → messages[] / statuses[]; handle batches. - Branch on
messagesvsstatuses— you'll receive both. - Tolerate event types you don't handle (you may also receive non-message account events). Ignore what you don't need.
Further reading
The payload Superfone forwards is Meta's standard WhatsApp Cloud API webhook payload. For the full, authoritative schema of every message and status type, refer to Meta's official Webhooks Components reference.
You don't need to set up webhooks on Meta yourself — Superfone handles registration with Meta. The link above is only for understanding the payload fields you'll receive.