Webhook Payload
Structure of the webhook payload delivered to your endpoint.
Pipes.bot delivers a Meta-compatible webhook payload extended with a pipes object for Pipes.bot-specific metadata including media download information.
Payload structure
Every webhook delivery follows the same envelope structure. The messages[].type field determines the shape of the type-specific payload, and the pipes object carries media metadata when applicable.
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "pool_number_id",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+15551234567",
"phone_number_id": "pool_number_id"
},
"messages": [
{
"id": "msg_abc123",
"from": "15559876543",
"timestamp": "2025-01-15T10:30:00.000Z",
"type": "text",
"text": {
"body": "Hello from WhatsApp!"
}
}
],
"contacts": [
{
"profile": {
"name": "Jane Doe"
},
"wa_id": "15559876543"
}
]
},
"field": "messages"
}
]
}
],
"pipes": {
"conversationId": "conv_xyz789",
"poolNumberId": "pool_number_id",
"label": "support"
}
}Field reference
Root fields
| Field | Type | Description |
|---|---|---|
object | string | Always "whatsapp_business_account" |
entry | array | Array containing one entry object |
pipes | object | Pipes.bot extension fields (see below) |
entry[].changes[].value
| Field | Type | Description |
|---|---|---|
messaging_product | string | Always "whatsapp" |
metadata.display_phone_number | string | Your pool number's phone number |
metadata.phone_number_id | string | Your pool number ID |
messages | array | Array containing one message object |
contacts | array | Array containing one contact object |
messages[]
| Field | Type | Description |
|---|---|---|
id | string | Unique message identifier |
from | string | Sender's WhatsApp number |
timestamp | string | ISO 8601 timestamp |
type | string | Message type: text, image, audio, document, video, sticker, location, contacts, reaction |
The remaining fields depend on the message type. See Message types below.
contacts[]
| Field | Type | Description |
|---|---|---|
profile.name | string | Sender's WhatsApp profile name |
wa_id | string | Sender's WhatsApp ID |
pipes
| Field | Type | Description |
|---|---|---|
conversationId | string | Pipes.bot conversation ID |
poolNumberId | string | Pool number this message was received on |
label | string? | Activation label, if one was set |
test | boolean? | true only on test webhook deliveries |
media | object? | Media metadata, present for media message types |
pipes.media
Present when the message type is image, audio, document, video, or sticker.
| Field | Type | Description |
|---|---|---|
mediaId | string? | Obfuscated media ID for downloading |
downloadUrl | string? | Relative URL path: /v1/media/download/{mediaId} |
mimeType | string | MIME type of the file (e.g. image/jpeg, audio/ogg) |
fileName | string? | Original file name, when available (mainly for documents) |
byteSize | number | File size in bytes |
unavailable | boolean? | true if the media could not be downloaded from WhatsApp |
When unavailable is true, mediaId and downloadUrl will be absent. The message is still delivered so you can notify the user, but the file cannot be retrieved.
Message types
Text
{
"type": "text",
"text": {
"body": "Hello from WhatsApp!"
}
}Image
Images include an optional caption. The file can be downloaded via pipes.media.
{
"type": "image",
"image": {
"id": "media_id",
"mime_type": "image/jpeg",
"caption": "Check this out"
}
}{
"pipes": {
"conversationId": "conv_xyz789",
"poolNumberId": "pool_number_id",
"media": {
"mediaId": "aBcDeFgHiJkLmNoPqRs1t",
"downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
"mimeType": "image/jpeg",
"byteSize": 245120
}
}
}Audio
Audio messages include voice notes and audio file attachments.
{
"type": "audio",
"audio": {
"id": "media_id",
"mime_type": "audio/ogg"
}
}Video
Videos include an optional caption.
{
"type": "video",
"video": {
"id": "media_id",
"mime_type": "video/mp4",
"caption": "Watch this"
}
}Document
Documents include the original file name and an optional caption.
{
"type": "document",
"document": {
"id": "media_id",
"mime_type": "application/pdf",
"filename": "invoice.pdf",
"caption": "Here's the invoice"
}
}Sticker
{
"type": "sticker",
"sticker": {
"id": "media_id",
"mime_type": "image/webp"
}
}Location
{
"type": "location",
"location": {
"latitude": 37.7749,
"longitude": -122.4194,
"name": "San Francisco",
"address": "San Francisco, CA, USA"
}
}Location messages do not include pipes.media.
Contacts
{
"type": "contacts",
"contacts": [
{
"name": { "formatted_name": "Jane Doe" },
"phones": [{ "phone": "+15559876543", "type": "CELL" }]
}
]
}Reaction
Reactions reference the original message ID. An absent emoji means the reaction was removed.
{
"type": "reaction",
"reaction": {
"message_id": "msg_original123",
"emoji": "👍"
}
}Test payloads
Test payloads sent via the Test Connection button include pipes.test: true. Use this flag to filter out test deliveries:
app.post("/webhook", (req, res) => {
const payload = req.body;
if (payload.pipes?.test) {
// Acknowledge test delivery without processing
return res.sendStatus(200);
}
// Process real message...
});Handling all message types
Here's a complete example that routes by message type:
app.post("/webhook", (req, res) => {
const payload = req.body;
if (payload.pipes?.test) {
return res.sendStatus(200);
}
const message = payload.entry[0].changes[0].value.messages[0];
const pipes = payload.pipes;
switch (message.type) {
case "text":
console.log("Text:", message.text.body);
break;
case "image":
case "audio":
case "video":
case "document":
case "sticker":
if (pipes.media?.unavailable) {
console.log("Media unavailable for this message");
} else {
console.log("Download from:", pipes.media.downloadUrl);
console.log("MIME type:", pipes.media.mimeType);
console.log("Size:", pipes.media.byteSize, "bytes");
}
break;
case "location":
console.log("Location:", message.location.latitude, message.location.longitude);
break;
case "contacts":
console.log("Contacts:", message.contacts);
break;
case "reaction":
console.log("Reaction:", message.reaction.emoji, "on", message.reaction.message_id);
break;
}
res.sendStatus(200);
});