Browse guides

Parsing Inbound Payloads

How to parse the JSON payload Meta sends to your webhook for each inbound message type — text, image, video, audio, document, and interactive button/list replies.

On this page

Every inbound message Meta sends to your webhook has the same outer wrapper. Inside that wrapper, the content differs by message type. Parsing means digging into the JSON to extract what matters — who sent it, what type it is, and what the content is.

The outer wrapper — always the same#

Code
body.entry[0].changes[0].value.messages[0]
                                    ↑
                          the actual message object

Your server always digs to messages[0] first, then checks message.type:

js
const changes = body.entry?.[0]?.changes?.[0]?.value;
if (!changes?.messages) return;   // delivery status, not a message

const message = changes.messages[0];
const from    = message.from;     // sender's phone number e.g. "919959623255"
const type    = message.type;     // "text", "image", "audio", etc.

Per-type parsing#

Text message

js
// { "type": "text", "text": { "body": "Hello" } }
const text = message.text.body;   // → "Hello"

Image message

js
// { "type": "image", "image": { "id": "1234567890", "caption": "..." } }
const mediaId = message.image.id;        // use Media API to download
const caption = message.image.caption;   // optional, may be undefined

Audio / voice note

js
const mediaId = message.audio.id;

Video

js
const mediaId = message.video.id;
const caption = message.video.caption;

Document / file

js
// { "type": "document", "document": { "id": "111222333", "filename": "invoice.pdf" } }
const mediaId  = message.document.id;
const filename = message.document.filename;

Interactive — button tap

js
const id    = message.interactive.button_reply.id;     // → "yes_button"
const title = message.interactive.button_reply.title;  // → "Yes"

Interactive — list row selection

js
const id    = message.interactive.list_reply.id;
const title = message.interactive.list_reply.title;

Why media gives you an ID, not the file#

Meta never sends the actual file in the webhook — only the id. To get the file, call the Media API separately:

Code
GET https://graph.facebook.com/v21.0/{media-id}
→ returns a download URL
→ download the file from that URL

See Media API for the full download flow.

Testing with Postman#

You can simulate any inbound message type without a real phone by sending a POST to your webhook URL:

Code
POST http://localhost:3003/webhook
Content-Type: application/json

{
  "object": "whatsapp_business_account",
  "entry": [{ "changes": [{ "value": {
    "messages": [{
      "from": "919959623255",
      "type": "text",
      "text": { "body": "Hello from Postman" }
    }]
  }}]}]
}

Your server processes it exactly as if it came from a real customer.

Frequently asked#

How do I get the text a customer sent me?

Check message.type === 'text', then read message.text.body.

How do I get an image a customer sent me?

Read message.image.id from the payload, then call the Media API with that ID to get a download URL for the actual file. See Media API.

How do I know which button the customer tapped?

Check message.type === 'interactive'. For a button tap, read message.interactive.button_reply.id — this matches the id you set when you sent the button message.

What if changes.messages is missing?

The webhook also fires for delivery/read status updates, which have statuses instead of messages. Always guard with if (!changes?.messages) return before parsing.

Gotchas & common mistakes#

Still stuck? Open a support ticket and our team will help.