Pipes.bot

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

FieldTypeDescription
objectstringAlways "whatsapp_business_account"
entryarrayArray containing one entry object
pipesobjectPipes.bot extension fields (see below)

entry[].changes[].value

FieldTypeDescription
messaging_productstringAlways "whatsapp"
metadata.display_phone_numberstringYour pool number's phone number
metadata.phone_number_idstringYour pool number ID
messagesarrayArray containing one message object
contactsarrayArray containing one contact object

messages[]

FieldTypeDescription
idstringUnique message identifier
fromstringSender's WhatsApp number
timestampstringISO 8601 timestamp
typestringMessage type: text, image, audio, document, video, sticker, location, contacts, reaction, button, interactive, or a passthrough type (see Button, Interactive, Other types)

The remaining fields depend on the message type. See Message types below.

contacts[]

FieldTypeDescription
profile.namestringSender's WhatsApp profile name
wa_idstringSender's WhatsApp ID

pipes

FieldTypeDescription
conversationIdstringPipes.bot conversation ID
poolNumberIdstringPool number this message was received on
labelstring?Activation label, if one was set
mediaobject?Media metadata, present for media message types

pipes.media

Present when the message type is image, audio, document, video, or sticker.

FieldTypeDescription
mediaIdstring?Obfuscated media ID for downloading
downloadUrlstring?Relative URL path: /v1/media/download/{mediaId}
mimeTypestringMIME type of the file (e.g. image/jpeg, audio/ogg)
fileNamestring?Original file name, when available (mainly for documents)
byteSizenumberFile size in bytes
unavailableboolean?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 object
{
  "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": "👍"
  }
}

Button

Template quick-reply buttons the customer tapped arrive as type: "button".

{
  "type": "button",
  "button": {
    "text": "Confirm",
    "payload": "confirm_order"
  }
}

Interactive

Replies to interactive messages arrive as type: "interactive". The nested object is one of button_reply, list_reply, or nfm_reply (Flows).

{
  "type": "interactive",
  "interactive": {
    "type": "button_reply",
    "button_reply": { "id": "confirm", "title": "Confirm" }
  }
}

Other types

Any inbound type Pipes.bot does not specifically model (e.g. order, system, or a future Meta type) is passed through with its original type string and an empty text block:

{
  "type": "order",
  "text": { "body": "" }
}

Note: Non-text passthrough types always carry a text: { body: "" } block even though there is no text. Switch on type and read the type-specific object rather than relying on text.

Test payloads

The Test Connection button sends a small JSON body to your endpoint (signed with X-Pipes-Signature when a secret is configured):

{
  "test": true,
  "source": "pipes.bot"
}

This is not a Meta-shaped message envelope — it has no entry / messages array. Detect it by the top-level test flag and acknowledge with 200 without parsing message fields:

app.post("/webhook", (req, res) => {
  const payload = req.body;

  if (payload.test === true) {
    // Test Connection ping — acknowledge 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.test === true) {
    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;

    case "button":
      console.log("Button tapped:", message.button.text);
      break;

    case "interactive":
      console.log("Interactive reply:", message.interactive.type);
      break;

    default:
      // order, system, unknown, future Meta types — type string preserved
      console.log("Other type:", message.type);
  }

  res.sendStatus(200);
});

On this page