Pipes.bot
BYON

API v2 Example Payloads

Ready-to-use API v2 webhook envelopes (fixtures) for testing your handler, with a script that signs and replays them.

These are complete API v2 envelopes, exactly as Pipes.bot delivers them to your webhook and inside WebSocket event frames. Use them as test fixtures for your handler, or replay them against your local server to check signature verification and deduplication.

Every fixture is available as a file at https://docs.pipes.bot/fixtures/byon-v2/<name>.json. The IDs (app_abc123, pn_abc123, wamid..., US.1349...) are examples.

Using the fixtures

In tests

Download the files you need and feed them to your handler:

mkdir -p test/fixtures && cd test/fixtures
for name in message-text message-text-username-with-phone message-text-bsuid-only message-text-parent-bsuid message-image message-contact-request status-delivered-bsuid status-failed system-user-changed-number system-user-changed-user-id template-status-update; do
  curl -sO https://docs.pipes.bot/fixtures/byon-v2/$name.json
done
import envelope from "./fixtures/message-text-bsuid-only.json";

test("keys the user on the BSUID when there is no phone number", async () => {
  const result = await handleEnvelope(envelope);
  expect(result.userKey).toBe("US.13491208655302741918");
});

Replaying against your server

This script fetches a fixture, signs it with Standard Webhooks and POSTs it to your endpoint, the same way Pipes.bot does. Run it with your App's signing secret (whsec_...) or with a test secret your local server is configured to use. Node.js 18 or later, no dependencies.

replay.mjs
// Usage: node replay.mjs <fixture-name> <webhook-url> <whsec_secret>
import crypto from "node:crypto";

const [name, url, secret] = process.argv.slice(2);
if (!name || !url || !secret) {
  console.error("Usage: node replay.mjs <fixture-name> <webhook-url> <whsec_secret>");
  process.exit(1);
}

const res = await fetch(`https://docs.pipes.bot/fixtures/byon-v2/${name}.json`);
if (!res.ok) throw new Error(`Unknown fixture: ${name}`);
const body = JSON.stringify(await res.json()); // the exact bytes that get signed and sent

const id = JSON.parse(body).id; // on v2, webhook-id equals the envelope id
const timestamp = Math.floor(Date.now() / 1000).toString();
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const signature = crypto
  .createHmac("sha256", key)
  .update(`${id}.${timestamp}.${body}`)
  .digest("base64");

const response = await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "webhook-id": id,
    "webhook-timestamp": timestamp,
    "webhook-signature": `v1,${signature}`,
  },
  body,
});
console.log(response.status, await response.text());
node replay.mjs message-text http://localhost:3000/webhook whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw

Run the same fixture twice to check deduplication: the envelope id doesn't change, so your handler should process it once.

Messages

Text message

A user without a username. Meta shares the phone number (from, wa_id) and also sends the BSUID (from_user_id, user_id).

message-text.json

message-text.json
{
  "id": "evt_0f1e2d3c4b5a69788796a5b4c3d2e1f0",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "contacts": [
      {
        "profile": {
          "name": "Sheena Nelson"
        },
        "wa_id": "16505551234",
        "user_id": "US.13491208655302741918"
      }
    ],
    "messages": [
      {
        "from": "16505551234",
        "from_user_id": "US.13491208655302741918",
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTRBNjU5OUFFRTAzODEwMTQ0RgA=",
        "timestamp": "1749416383",
        "type": "text",
        "text": {
          "body": "Does it come in another color?"
        }
      }
    ]
  }
}

Text from a user with a username (phone shared)

The user has a username, but the phone number is still shared, for example because your number messaged them in the last 30 days. contacts[].profile.username is present.

message-text-username-with-phone.json

message-text-username-with-phone.json
{
  "id": "evt_1a2b3c4d5e6f708192a3b4c5d6e7f801",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "contacts": [
      {
        "profile": {
          "name": "Sheena Nelson",
          "username": "@realsheenanelson"
        },
        "wa_id": "16505551234",
        "user_id": "US.13491208655302741918"
      }
    ],
    "messages": [
      {
        "from": "16505551234",
        "from_user_id": "US.13491208655302741918",
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTRBNjU5OUFFRTAzODEwMTQ0RgA=",
        "timestamp": "1749416383",
        "type": "text",
        "text": {
          "body": "Does it come in another color?"
        }
      }
    ]
  }
}

Text from a user with a username (no phone)

The phone number is hidden. messages[].from and contacts[].wa_id are omitted; identify the user by from_user_id. Reply by sending to the BSUID.

message-text-bsuid-only.json

message-text-bsuid-only.json
{
  "id": "evt_2b3c4d5e6f708192a3b4c5d6e7f80912",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "contacts": [
      {
        "profile": {
          "name": "Sheena Nelson",
          "username": "@realsheenanelson"
        },
        "user_id": "US.13491208655302741918"
      }
    ],
    "messages": [
      {
        "from_user_id": "US.13491208655302741918",
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTRBNjU5OUFFRTAzODEwMTQ0RgA=",
        "timestamp": "1749416383",
        "type": "text",
        "text": {
          "body": "Does it come in another color?"
        }
      }
    ]
  }
}

Text with a parent BSUID

Parent BSUIDs (CC.ENT.<id>) are opt-in, for businesses with multiple portfolios. When enabled, from_parent_user_id and contacts[].parent_user_id are added.

message-text-parent-bsuid.json

message-text-parent-bsuid.json
{
  "id": "evt_3c4d5e6f708192a3b4c5d6e7f8091a2b",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "contacts": [
      {
        "profile": {
          "name": "Sheena Nelson",
          "username": "@realsheenanelson"
        },
        "user_id": "US.13491208655302741918",
        "parent_user_id": "US.ENT.11815799212886844830"
      }
    ],
    "messages": [
      {
        "from_user_id": "US.13491208655302741918",
        "from_parent_user_id": "US.ENT.11815799212886844830",
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTRBNjU5OUFFRTAzODEwMTQ0RgA=",
        "timestamp": "1749416383",
        "type": "text",
        "text": {
          "body": "Does it come in another color?"
        }
      }
    ]
  }
}

Image message

Media is downloaded and stored before delivery. pipes.media is keyed by Meta's media ID (messages[].image.id). If a download fails, the entry is { "unavailable": true } and the event is still delivered.

message-image.json

message-image.json
{
  "id": "evt_4d5e6f708192a3b4c5d6e7f8091a2b3c",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {
      "1003383421387256": {
        "mediaId": "aBcDeFgHiJkLmNoPqRs1t",
        "downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
        "mimeType": "image/jpeg",
        "byteSize": 48213
      }
    }
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "contacts": [
      {
        "profile": {
          "name": "Sheena Nelson"
        },
        "wa_id": "16505551234",
        "user_id": "US.13491208655302741918"
      }
    ],
    "messages": [
      {
        "from": "16505551234",
        "from_user_id": "US.13491208655302741918",
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQ1QjhGQzE0MjE5RDZBRTlBMjMA",
        "timestamp": "1749416383",
        "type": "image",
        "image": {
          "caption": "Do you have this one in blue?",
          "mime_type": "image/jpeg",
          "sha256": "N4Ts1d9cU0tQdPZ2r8uC5n3o2kE8tHn6yq3tJjv0wTU=",
          "id": "1003383421387256"
        }
      }
    ]
  }
}

Phone number shared after a contact request

The reply to a REQUEST_CONTACT_INFO button or interactive message. origin is contact_request. Store the mapping from from_user_id to phones[].wa_id. vcard is a base64-encoded vCard. See Asking for a phone number.

message-contact-request.json

message-contact-request.json
{
  "id": "evt_5e6f708192a3b4c5d6e7f8091a2b3c4d",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "contacts": [
      {
        "profile": {
          "name": "Sheena Nelson",
          "username": "@realsheenanelson"
        },
        "user_id": "US.13491208655302741918"
      }
    ],
    "messages": [
      {
        "from_user_id": "US.13491208655302741918",
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQ2QzlBRDI1MzBFN0JGMEFCMzQA",
        "timestamp": "1749416383",
        "type": "contacts",
        "contacts": [
          {
            "origin": "contact_request",
            "name": {
              "formatted_name": "Sheena Nelson",
              "first_name": "Sheena",
              "last_name": "Nelson"
            },
            "phones": [
              {
                "phone": "+1 650-555-1234",
                "wa_id": "16505551234",
                "type": "CELL"
              }
            ],
            "vcard": "QkVHSU46VkNBUkQKVkVSU0lPTjozLjAKTjpOZWxzb247U2hlZW5hOzs7CkZOOlNoZWVuYSBOZWxzb24KVEVMO3R5cGU9Q0VMTDt0eXBlPVZPSUNFO3dhaWQ9MTY1MDU1NTEyMzQ6KzEgNjUwLTU1NS0xMjM0CkVORDpWQ0FSRA=="
          }
        ]
      }
    ]
  }
}

Statuses

Delivered status for a BSUID recipient

Statuses arrive under field: "messages" in whatsapp.statuses[]. recipient_user_id and a contacts array are included. For a recipient without a shared phone number, recipient_id and contacts[].wa_id are omitted.

status-delivered-bsuid.json

status-delivered-bsuid.json
{
  "id": "evt_6f708192a3b4c5d6e7f8091a2b3c4d5e",
  "field": "messages",
  "createdAt": "2025-06-15T23:27:54.031Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "contacts": [
      {
        "user_id": "US.13491208655302741918",
        "parent_user_id": "US.ENT.11815799212886844830"
      }
    ],
    "statuses": [
      {
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMAA=",
        "status": "delivered",
        "timestamp": "1750030073",
        "recipient_user_id": "US.13491208655302741918",
        "recipient_parent_user_id": "US.ENT.11815799212886844830",
        "pricing": {
          "billable": true,
          "pricing_model": "PMP",
          "type": "regular",
          "category": "marketing"
        }
      }
    ]
  }
}

Failed status

Meta's errors[] array is passed through untouched, so you can act on Meta's numeric code.

status-failed.json

status-failed.json
{
  "id": "evt_708192a3b4c5d6e7f8091a2b3c4d5e6f",
  "field": "messages",
  "createdAt": "2025-06-15T23:27:54.031Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "statuses": [
      {
        "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMAA=",
        "status": "failed",
        "timestamp": "1750030073",
        "recipient_id": "16505551234",
        "errors": [
          {
            "code": 131049,
            "title": "This message was not delivered to maintain healthy ecosystem engagement.",
            "message": "This message was not delivered to maintain healthy ecosystem engagement.",
            "error_data": {
              "details": "In order to maintain a healthy ecosystem engagement, the message failed to be delivered."
            },
            "href": "/documentation/business-messaging/whatsapp/support/error-codes"
          }
        ]
      }
    ]
  }
}

Identity changes

User changed their phone number

Changing the phone number also regenerates the BSUID: system.previous_user_id is the old BSUID, system.user_id the new one and system.wa_id the new phone number.

system-user-changed-number.json

system-user-changed-number.json
{
  "id": "evt_8192a3b4c5d6e7f8091a2b3c4d5e6f70",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "messages": [
      {
        "from": "16505559999",
        "id": "wamid.HBgLMTY1MDU1NTk5OTkVAgASGBQ3RDBCRTM2NDFGOENBMUJDNDUA",
        "timestamp": "1749416383",
        "type": "system",
        "system": {
          "body": "User A changed from 16505551234 to 16505559999",
          "wa_id": "16505559999",
          "user_id": "US.99999999999999999999",
          "previous_user_id": "US.13491208655302741918",
          "type": "user_changed_number"
        }
      }
    ]
  }
}

User's BSUID was regenerated

Move your records from system.previous_user_id to system.user_id.

system-user-changed-user-id.json

system-user-changed-user-id.json
{
  "id": "evt_92a3b4c5d6e7f8091a2b3c4d5e6f7081",
  "field": "messages",
  "createdAt": "2025-06-08T20:59:44.512Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "messaging_product": "whatsapp",
    "metadata": {
      "display_phone_number": "15550001111",
      "phone_number_id": "106540352242922"
    },
    "messages": [
      {
        "id": "wamid.HBgLMTY1MDU1NTk5OTkVAgASGBQ4RTFDRjQ3NTJBOURCMkNENTYA",
        "timestamp": "1749416383",
        "type": "system",
        "system": {
          "body": "User changed from US.13491208655302741918 to US.99999999999999999999",
          "user_id": "US.99999999999999999999",
          "previous_user_id": "US.13491208655302741918",
          "type": "user_changed_user_id"
        }
      }
    ]
  }
}

Templates

Template status update

Every Meta webhook field for your numbers is delivered, not only messages. Here whatsapp is Meta's template status change.value. pipes.media is always {}.

template-status-update.json

template-status-update.json
{
  "id": "evt_a3b4c5d6e7f8091a2b3c4d5e6f708192",
  "field": "message_template_status_update",
  "createdAt": "2025-06-10T14:02:11.870Z",
  "pipes": {
    "appId": "app_abc123",
    "poolNumberId": "pn_abc123",
    "media": {}
  },
  "whatsapp": {
    "event": "APPROVED",
    "message_template_id": 1234567890123456,
    "message_template_name": "order_shipped",
    "message_template_language": "en_US",
    "reason": null,
    "message_template_category": "UTILITY"
  }
}

On this page