Pipes.bot

Message Types

All WhatsApp message types delivered over the WebSocket connection.

Every incoming WhatsApp message arrives as a whatsapp_message event. (Apps on API v2 receive event frames instead.) The data.type field determines the shape of the payload.

Which messages you receive depends on your connection scope:

  • User-scoped connections receive all messages from all your activated pool numbers
  • Pool-number-scoped connections receive only messages from the specified pool number
  • Conversation-scoped connections receive only messages matching the specified conversationId

Envelope structure

All messages share this common structure:

{
  "type": "whatsapp_message",
  "data": {
    "messageId": "msg_abc123",
    "conversationId": "conv_xyz789",
    "poolNumberId": "pool_123",
    "poolNumberPhoneNumber": "+15551234567",
    "fromNumber": "+15559876543",
    "fromName": "Jane Doe",
    "text": "Message text or caption",
    "timestamp": "2025-01-15T10:30:00.000Z",
    "label": "support",
    "type": "text",
    "body": "Message text or caption"
  }
}

Common fields

FieldTypeDescription
messageIdstringUnique message identifier
conversationIdstringPipes.bot conversation ID (use this for replies)
poolNumberIdstringPool number that received the message
poolNumberPhoneNumberstringPool number's phone number
fromNumberstringSender's WhatsApp number, or their BSUID when WhatsApp doesn't share the number
fromNamestring?Sender's WhatsApp profile name
userIdstring?Sender's BSUID (CC.<id>), when WhatsApp provides one
parentUserIdstring?Sender's parent BSUID (CC.ENT.<id>), if enabled
usernamestring?Sender's WhatsApp username, if they have set one, as Meta sends it (may include a leading @)
textstringText content or caption
timestampstringISO 8601 timestamp
labelstring?Activation label, if one was set
typestringMessage type (see sections below)
bodystringSame as text — the message body

Additional fields appear depending on the message type.

Text

{
  "type": "text",
  "body": "Hello from WhatsApp!",
  "text": "Hello from WhatsApp!"
}

The simplest message type. text and body both contain the message content.

Image

{
  "type": "image",
  "body": "Check this out",
  "text": "Check this out",
  "media": {
    "mediaId": "aBcDeFgHiJkLmNoPqRs1t",
    "downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
    "mimeType": "image/jpeg",
    "byteSize": 245120
  }
}

Images include an optional caption in body/text. See Media Handling for download details.

Audio

{
  "type": "audio",
  "media": {
    "mediaId": "aBcDeFgHiJkLmNoPqRs1t",
    "downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
    "mimeType": "audio/ogg",
    "byteSize": 52480
  }
}

Includes voice notes and audio file attachments.

Video

{
  "type": "video",
  "body": "Watch this",
  "text": "Watch this",
  "media": {
    "mediaId": "aBcDeFgHiJkLmNoPqRs1t",
    "downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
    "mimeType": "video/mp4",
    "byteSize": 1048576
  }
}

Videos include an optional caption.

Document

{
  "type": "document",
  "body": "Here's the invoice",
  "text": "Here's the invoice",
  "media": {
    "mediaId": "aBcDeFgHiJkLmNoPqRs1t",
    "downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
    "mimeType": "application/pdf",
    "fileName": "invoice.pdf",
    "byteSize": 102400
  }
}

Documents include the original fileName and an optional caption.

Sticker

{
  "type": "sticker",
  "media": {
    "mediaId": "aBcDeFgHiJkLmNoPqRs1t",
    "downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
    "mimeType": "image/webp",
    "byteSize": 25600
  }
}

Location

{
  "type": "location",
  "location": {
    "latitude": 37.7749,
    "longitude": -122.4194,
    "name": "San Francisco",
    "address": "San Francisco, CA, USA"
  }
}
FieldTypeDescription
latitudenumberLatitude coordinate
longitudenumberLongitude coordinate
namestring?Location name
addressstring?Full address string

Location messages do not include a media object.

Contacts

{
  "type": "contacts",
  "contacts": [
    {
      "name": { "formatted_name": "Jane Doe" },
      "phones": [{ "phone": "+15559876543", "type": "CELL" }]
    }
  ]
}

The contacts array follows WhatsApp's native contact structure. Each contact may include name, phones, emails, addresses, org, and urls fields.

Reaction

{
  "type": "reaction",
  "reaction": {
    "messageId": "msg_original123",
    "emoji": "👍"
  }
}
FieldTypeDescription
messageIdstringID of the message being reacted to
emojistring?The emoji. Absent when the reaction has been removed

Unrecognized types

Messages that don't match a modeled type are delivered with data.type set to the raw WhatsApp type string (e.g. "order", "system") and empty text/body. The original WhatsApp payload is not forwarded over the WebSocket — only the type and empty text arrive. Switch on data.type and treat any unknown value as an unmodeled message.

Button

Template quick-reply taps arrive as type: "button" with an interactive object. The button's payload is delivered as interactive.id and its label as interactive.title (and body/text):

{
  "type": "button",
  "body": "Confirm",
  "text": "Confirm",
  "interactive": {
    "type": "button_reply",
    "id": "confirm_order",
    "title": "Confirm"
  }
}

Interactive

Replies to interactive messages (button_reply, list_reply, nfm_reply) arrive as type: "interactive" with an interactive object:

{
  "type": "interactive",
  "interactive": {
    "type": "button_reply",
    "id": "confirm",
    "title": "Confirm"
  }
}
FieldTypeDescription
typestringbutton_reply, list_reply, nfm_reply, or the raw subtype for unknown interactive messages
idstring?Selected option ID (or the button payload for type: "button" messages)
titlestring?Selected option title
descriptionstring?Selected list-row description
payloadstring?Template button payload
responseJsonstring?Raw JSON response for Flows (nfm_reply)

Handling all types

ws.on("message", (raw) => {
  const event = JSON.parse(raw.toString());
  if (event.type !== "whatsapp_message") return;

  const msg = event.data;

  switch (msg.type) {
    case "text":
      console.log("Text:", msg.body);
      break;

    case "image":
    case "audio":
    case "video":
    case "document":
    case "sticker":
      if (msg.media?.unavailable) {
        console.log("Media unavailable");
      } else {
        console.log("Download:", msg.media.downloadUrl);
        console.log("MIME:", msg.media.mimeType);
        console.log("Size:", msg.media.byteSize, "bytes");
      }
      if (msg.body) console.log("Caption:", msg.body);
      break;

    case "location":
      console.log("Location:", msg.location.latitude, msg.location.longitude);
      break;

    case "contacts":
      console.log("Contacts:", msg.contacts.length);
      break;

    case "reaction":
      if (msg.reaction.emoji) {
        console.log("Reaction:", msg.reaction.emoji, "on", msg.reaction.messageId);
      } else {
        console.log("Reaction removed on", msg.reaction.messageId);
      }
      break;

    case "button":
    case "interactive":
      console.log("Interactive:", msg.interactive.type, msg.interactive.title ?? msg.interactive.payload);
      break;

    default:
      console.log("Unrecognized type:", msg.type); // raw WhatsApp type, empty text
  }
});

BSUIDs

When a user hasn't shared their phone number, fromNumber holds their BSUID (for example US.13491208655302741918) and userId carries the same value. See BSUIDs for the format and for how a phone number appearing or disappearing changes the conversationId. To reply, pass the BSUID as toNumber.

API v2 event

Apps on BYON API v2 receive event frames instead of whatsapp_message. data is the same envelope that v2 sends to your webhook (see API v2 envelope, and API v2 Example Payloads for every event type):

{
  "type": "event",
  "data": {
    "id": "evt_3f9c1a7b2d4e4f6a8b0c1d2e3f4a5b6c",
    "field": "messages",
    "createdAt": "2025-01-15T10:30:01.123Z",
    "pipes": {
      "appId": "app_abc123",
      "poolNumberId": "pn_abc123",
      "media": {}
    },
    "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": "text",
          "text": { "body": "Hello" }
        }
      ]
    }
  }
}

On v2 connections:

  • Message statuses (sent, delivered, read, failed), identity changes and template status updates all arrive as event frames. Read data.field and the contents of data.whatsapp.
  • Events arrive over the WebSocket and the webhook (if you configured one). Deduplicate on data.id, or on the message id (wamid...) inside data.whatsapp.
  • A conversationId query parameter is rejected during the handshake with HTTP 403. v2 has no conversations.
  • An event is queued only when no WebSocket connection received it and the webhook either is not configured or failed without being recorded. A webhook that fails all 3 delivery attempts counts as handled: the failure is recorded as a pending webhook delivery and the event is not queued. The queue holds up to 100 events per App for 24 hours, and the oldest event is dropped when it is full. Queued events are replayed as event frames, oldest first, when an App-scoped connection opens. A connection scoped with poolNumberId does not drain the queue.
  • If your App is upgraded to v2 while connected, the server closes your socket with code 4000 and reason api_version_changed. Reconnect to continue on v2.
ws.on("message", (raw) => {
  const frame = JSON.parse(raw.toString());
  if (frame.type !== "event") return;

  const envelope = frame.data;
  if (seen.has(envelope.id)) return;
  seen.add(envelope.id);

  if (envelope.field === "messages") {
    const value = envelope.whatsapp;
    for (const message of value.messages ?? []) {
      console.log(message.type, message.from ?? message.from_user_id);
    }
    for (const status of value.statuses ?? []) {
      console.log("status", status.id, status.status);
    }
  }
});

On this page