What is a webhook payload?
A webhook payload is the JSON body Meta POSTs to your callback URL. Whatever the event — an inbound message, a delivery status, a template approval, an account alert — it arrives in the same envelope: object set to whatsapp_business_account, an entry array whose items each carry the WhatsApp Business Account ID in id, and inside each entry a changes array whose items name one of your subscribed webhook fields in field and carry a value.
The envelope is generic; the value is not. For the messages field, value holds messaging_product, a metadata object with display_phone_number and phone_number_id, and then contacts and messages for inbound traffic, statuses for updates on messages you sent, or an errors array. For fields such as message_template_status_update or account_update, value has a completely different shape, with keys like event and reason. Your code therefore branches on field first and on the keys present in value second.
This page is about the envelope and how to walk it safely. The objects inside value each have their own page: the messages webhook for inbound message types, the statuses webhook for updates on what you sent, webhook error objects, and the webhook contacts object that identifies the sender. Meta says payloads can be up to 3 MB and that it aggregates up to 1,000 updates into one POST — though batching is not guaranteed — and every POST is signed in X-Hub-Signature-256 over the raw bytes, so you parse only after verifying.
Key takeaways
- A webhook payload is the JSON body of every WhatsApp webhook:
object,entry,changes, then a field-specificvalue. objectis alwayswhatsapp_business_account,entry[].idis the WABA ID andchanges[].fieldnames the field that fired.- Meta batches up to 1,000 updates into one POST, but batching is not guaranteed, so loop over every entry and change.
- Payloads can be up to 3 MB; verify
X-Hub-Signature-256over the raw bytes before parsing them. - Branch on
fieldfirst, then on the keys present invalue:messages,statusesorerrors.
Walk the envelope, layer by layer
{"object": "whatsapp_business_account","entry": [{"id": "102290129340398","changes": [{"field": "messages","value": {"messaging_product": "whatsapp","metadata": {"display_phone_number": "919876543210","phone_number_id": "106540352242922"},"statuses": [{"id": "wamid.HBgMOTE5ODEyMzQ1Njc4FQIAERgSMDQ0QjU3RkM2","status": "delivered","timestamp": "1760850000","recipient_id": "919812345678"}]}}]},{"id": "102290129340398","time": 1760850012,"changes": [{"field": "message_template_status_update","value": {"event": "APPROVED","message_template_id": 1189056642410321,"message_template_name": "diwali_catalogue_2026","message_template_language": "en","reason": "NONE"}}]}]}
Field 9: entry.1.changes.0.value.event — Template events use event and reason instead of messages or statuses.
How does a webhook payload work?
Verify, then parse
Check
X-Hub-Signature-256against the raw body before callingJSON.parse, then reject anything whoseobjectis notwhatsapp_business_account.Loop over every entry
Each
entryitem belongs to one WhatsApp Business Account, identified byentry[].id. Template and account events also carry a Unixtimeon the entry.Loop over every change and read its field
changes[].fieldnames the subscription that fired —messages,message_template_status_update,account_updateand so on. Dispatch to one handler per field.Inspect value by its keys
Inside a
messageschange,value.metadata.phone_number_idtells you which business number is involved; then check which arrays exist —messages(inbound),statuses(outbound updates) orerrors.Ignore what you do not know
Log unknown fields and keys, return 200 anyway and move on. New fields appear over time, and a handler that throws on them only triggers retries.
A webhook payload in code
type Change = { field: string; value: Record<string, any> };type Entry = { id: string; time?: number; changes: Change[] };type Payload = { object: string; entry: Entry[] };export function walk(payload: Payload) {if (payload.object !== "whatsapp_business_account") return;for (const entry of payload.entry ?? []) {const wabaId = entry.id;for (const { field, value } of entry.changes ?? []) {switch (field) {case "messages": {const phoneNumberId = value.metadata?.phone_number_id;for (const msg of value.messages ?? []) onInbound(wabaId, phoneNumberId, msg, value.contacts);for (const st of value.statuses ?? []) onStatus(wabaId, phoneNumberId, st);for (const err of value.errors ?? []) onSystemError(wabaId, err);break;}case "message_template_status_update":onTemplateEvent(wabaId, value); // { event, message_template_id, reason, ... }break;default:log.info("unhandled webhook field", { field }); // never throw}}}}
Iterate both arrays, dispatch on field, then on the keys inside value — never on position.
Common wrong turns
Common mistakes with a webhook payload
Reading only the first entry and change
entry[0].changes[0]works in testing and fails in production when Meta batches. Always iterate both arrays, and every item insidemessagesorstatuses.Assuming a messages change means a message
The
messagesfield carries inbound messages, outboundstatusesand systemerrors. Check which arrays are present before readingmessages[0].Closed schemas that reject new keys
Validating against a strict schema turns every new Meta key into a failed webhook and a retry storm. Validate the keys you use and pass the rest through untouched.
Logging whole payloads with personal data
Payloads include names, phone numbers and message text. Mask
wa_id,fromand message bodies before logging, and keep raw copies only briefly.
Key facts at a glance
A webhook payload: key facts and limits
- object
- Always whatsapp_business_account
- entry[].id
- The WhatsApp Business Account ID
- changes[].field
- The subscribed field that fired
- changes[].value
- Shape depends on the field
- Maximum size
- 3 MB per payload (Meta)
- Batching
- Up to 1,000 updates per POST (not guaranteed)
- Signature
- X-Hub-Signature-256 over the raw body
Example: The approval that was already there
A Surat textile exporter subscribes to messages and message_template_status_update. One morning a single POST carries two entries: a delivered status for an order update and an APPROVED event for a new Diwali catalogue template. The current handler routes the first by value.statuses to the order timeline and the second by field to the template sync job. Its first version read only entry[0] — and the campaign team once spent a day waiting for a template approval that had already arrived and been silently dropped.
Why does a webhook payload matter?
Most webhook bugs are envelope bugs, not business-logic bugs: reading only entry[0].changes[0], assuming every messages change contains messages, or crashing on a field nobody planned for. A handler that walks every entry and change, branches on field and tolerates unknown keys keeps working when Meta adds fields or batches events — and it is the base every other webhook feature, from routing to deduplication, is built on.
Good practice
Best practices for a webhook payload
- Use metadata.phone_number_id, not display_phone_number, as the key for the business number.
- Persist each raw payload (masked) with the WABA ID and received time for replay and debugging.
- Keep one fixture per subscribed field and run the walker against all of them in CI.
- Return 200 for fields you have not built handlers for yet, so they do not pile up as retries.