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, or their BSUID when WhatsApp doesn't share the number (see BSUIDs)
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. Holds the BSUID when the phone number isn't shared
user_idstring?Sender's BSUID, when WhatsApp provides one
parent_user_idstring?Sender's parent BSUID, if enabled for your business portfolio
profile.usernamestring?Sender's WhatsApp username, if they have set one. Passed through as Meta sends it; Meta's own samples include a leading @

pipes

FieldTypeDescription
conversationIdstringPipes.bot conversation ID
poolNumberIdstringPool number this message was received on
labelstring?Activation label, if one was set
webhookIdstring?Unique per delivery and identical across retries (msg_...). On App webhooks it is also the webhook-id header. Use it to deduplicate
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.

BSUIDs

WhatsApp is rolling out usernames. A user with a username can message your number without sharing their phone number. WhatsApp then identifies them with a BSUID (business-scoped user ID), which is unique per business portfolio and user.

  • A BSUID looks like US.13491208655302741918: a two-letter country code, a dot, then up to 128 alphanumeric characters.
  • A parent BSUID (if enabled for your portfolio) looks like US.ENT.13491208655302741918.
  • A BSUID changes when the user changes their phone number. A username change does not change it.

On the legacy API, Pipes.bot keeps the payload shape and falls back to the BSUID when there's no phone number:

  • messages[].from and contacts[].wa_id carry the BSUID instead of a phone number.
  • contacts[0] gains user_id, parent_user_id (if present) and profile.username (if present). Missing fields are omitted, never sent as empty strings.
  • You can send to a BSUID by passing it as toNumber (/v1/messages/app/send) or to (/v1/messages/passthrough). See BSUIDs and usernames.
  • A conversation is keyed by the sender identifier. When a user's phone number appears or disappears, the identifier changes and you get a new conversationId.
{
  "messages": [
    {
      "id": "wamid.abc123",
      "from": "US.13491208655302741918",
      "timestamp": "2025-01-15T10:30:00.000Z",
      "type": "text",
      "text": { "body": "Hello" }
    }
  ],
  "contacts": [
    {
      "profile": { "name": "Jane Doe", "username": "@jane" },
      "wa_id": "US.13491208655302741918",
      "user_id": "US.13491208655302741918"
    }
  ]
}

Don't parse from as a phone number. To ask the user for their number, see Asking for a phone number.

API v2 envelope

Apps on BYON API v2 don't receive the reshaped payload above. Each change that Meta sends is delivered unmodified inside a small envelope, one envelope per change:

{
  "id": "evt_3f9c1a7b2d4e4f6a8b0c1d2e3f4a5b6c",
  "field": "messages",
  "createdAt": "2025-01-15T10:30:01.123Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {
      "1234567890": {
        "mediaId": "aBcDeFgHiJkLmNoPqRs1t",
        "downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
        "mimeType": "image/jpeg",
        "byteSize": 48213
      }
    }
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": { "display_phone_number": "15551234567", "phone_number_id": "1098765432" },
    "contacts": [{ "profile": { "name": "Jane Doe" }, "wa_id": "15559876543" }],
    "messages": [
      {
        "from": "15559876543",
        "id": "wamid.abc123",
        "timestamp": "1736937000",
        "type": "image",
        "image": { "id": "1234567890", "mime_type": "image/jpeg" }
      }
    ]
  }
}
FieldTypeDescription
idstringEnvelope ID (evt_...). Unique and stable across retries: use it to deduplicate
fieldstringMeta webhook field, for example messages or message_template_status_update
createdAtstringISO 8601 time the envelope was created
pipes.appIdstringYour App ID
pipes.poolNumberIdstringThe BYON number the event belongs to
pipes.mediaobjectStored media keyed by Meta media ID. Empty ({}) when the change has no media
whatsappobjectMeta's change.value, untouched. Statuses, identity changes and any field Meta adds appear here

Each pipes.media entry has the fields described in pipes.media. If a file could not be downloaded, the entry is { "unavailable": true } and the envelope is still delivered.

v2 envelopes are signed with Standard Webhooks, and the webhook-id header equals the envelope id. See Webhook Security. The same envelope is pushed over the WebSocket as an event frame, see Message Types.

For a complete envelope of every event type, see API v2 Example Payloads.

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"
}

The Test Webhook button in a BYON App's settings sends a different body, signed with Standard Webhooks (its webhook-id starts with evt_test_):

{
  "event": "test",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "app": { "id": "app_abc123", "name": "My App" },
  "message": "This is a test webhook from Pipes.bot"
}

Neither is a Meta-shaped message envelope — they have no entry / messages array. Detect them by test: true (Test Connection) or event: "test" (Test Webhook) and acknowledge with 200 without parsing message fields:

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

  if (payload.test === true || payload.event === "test") {
    // Test 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