Pipes.bot
BYON

BYON API v2 Migration

What changes when a BYON App moves to API v2, how to upgrade, and how to handle BSUIDs.

API v2 gives BYON Apps Meta's webhook data as-is: no conversations, no reshaping, and nothing lost when a WhatsApp user hides their phone number behind a username. New Apps start on API v2. Apps created before it stay on the legacy API until you upgrade them. New behavior is opt-in and the upgrade is one-way.

What changes

AreaLegacyAPI v2
Inbound payloadMeta-like payload reshaped by Pipes.bot, plus pipes.conversationIdOne envelope per Meta change: { id, field, createdAt, pipes, whatsapp }, with whatsapp being Meta's change.value untouched
conversationIdCreated per sender, used for repliesRemoved. Nothing is stored. A conversationId WebSocket parameter is rejected with 403
StatusesNot deliveredDelivered (sent, delivered, read, failed) as messages-field events
Other Meta fieldsTemplate status in a Pipes.bot shapeEvery change Meta sends for your numbers, including message_template_status_update
WebSocket inboundwhatsapp_messageevent carrying the envelope
WebSocket sendwhatsapp_reply { conversationId, toNumber, text }send { requestId, poolNumberId, message }, answered by send_result
REST /app/sendtoNumber, response { success, messageId, conversationId }to (or toNumber), response { messageId, contacts }
REST /passthroughtoto or recipient; body forwarded to Meta untouched
Signingwebhook-id / webhook-timestamp / webhook-signatureSame, with webhook-id equal to the envelope id
Offline queuePer sender, replayed as whatsapp_messageOne queue per App, replayed as event

Webhook envelope

{
  "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": {}, "messages": [], "statuses": [] }
}

Field details and the pipes.media map are in Webhook Payload. For complete envelopes of every event type (messages, media, statuses, identity changes, template updates), see API v2 Example Payloads.

WebSocket

{ "type": "event", "data": { "id": "evt_...", "field": "messages", "...": "..." } }

Send with a send frame and read the send_result:

{
  "type": "send",
  "requestId": "req_001",
  "poolNumberId": "pn_abc123",
  "message": {
    "messaging_product": "whatsapp",
    "to": "15559876543",
    "type": "text",
    "text": { "body": "Hello" }
  }
}
{ "type": "send_result", "requestId": "req_001", "success": true, "messageId": "wamid...", "contacts": [] }

Error codes and details: Sending via WebSocket on API v2. Legacy connections that send send get UNSUPPORTED_IN_LEGACY; v2 connections that send whatsapp_reply get UNSUPPORTED_IN_V2.

Upgrading

Upgrade from the dashboard: open your App, go to Settings, and use Upgrade to API v2. The confirmation dialog shows how many stored conversations and queued messages the upgrade will delete.

The upgrade:

  • Switches the App to API v2. It cannot be undone.
  • Deletes every stored conversation for the App's numbers (conversationId values stop being valid).
  • Discards legacy queued messages that were waiting for a WebSocket client. v2 queues start empty.
  • Closes the App's open WebSocket connections with code 4000 and reason api_version_changed. Clients must reconnect, and they will then be v2 connections.

Message history on WhatsApp is not affected. Webhook URL, API keys, numbers and the signing secret stay the same.

Before upgrading:

  1. Update your webhook handler to read envelopes (it can support both shapes during the switch: legacy payloads have object: "whatsapp_business_account", v2 envelopes have field and whatsapp).
  2. Switch sending to to / send frames, and stop relying on conversationId.
  3. Verify signatures with Standard Webhooks. See Webhook Security.
  4. Test your handler against the example payloads. The page includes a script that signs and replays them against your server.
  5. Upgrade, then reconnect your WebSocket clients.

Deduplicate events

Deliveries are at-least-once. A failed webhook is retried with the same id, and an event can arrive over both the webhook and the WebSocket. Dedupe on either:

  • envelope.id (evt_...), also sent as the webhook-id header. Identical across retries and across both channels.
  • The WhatsApp message id (wamid...) in whatsapp.messages[].id for messages, or whatsapp.statuses[].id plus status for statuses.
const seen = new Set<string>(); // use a durable store with expiry in production

function handleEnvelope(envelope: { id: string; field: string; whatsapp: any }) {
  if (seen.has(envelope.id)) return;
  seen.add(envelope.id);
  // process envelope.whatsapp ...
}

BSUIDs

WhatsApp identifies users who have adopted a username with a BSUID (business-scoped user ID) and may omit their phone number. Because v2 passes Meta's data through, you see exactly what Meta sent.

  • BSUID format: CC.<id>; parent BSUID: CC.ENT.<id>.
  • Inbound messages: messages[].from is optional, and messages[].from_user_id / from_parent_user_id are added. contacts[].wa_id is optional, and contacts[].user_id, parent_user_id and profile.username are added.
  • Statuses: statuses[].recipient_user_id and recipient_parent_user_id are added, and recipient_id is omitted when the phone can't be shared.
  • Missing fields are omitted, not empty strings.
  • Treat the BSUID as the stable identifier of a user, and the phone number as an optional attribute you may learn later.
function userKey(message: { from?: string; from_user_id?: string }) {
  return message.from_user_id ?? message.from;
}

To send to a BSUID use to on /v1/messages/app/send, or recipient in a passthrough or send body. If you need a phone number, see Asking for a phone number and error 131062.

Identity changes

Identity changes arrive as messages[] entries with type: "system". Meta's BSUID guide also describes a separate user_id_update webhook field, but Meta doesn't offer it for subscription yet, so today you only get the system messages. If that changes and Pipes.bot subscribes to it, v2 delivers it as an envelope with field: "user_id_update", like any other field:

system.typeMeaning
user_changed_numberThe user changed their phone number. This also regenerates the BSUID: previous_user_id holds the old one, wa_id the new number
user_changed_user_idThe user's BSUID was regenerated; previous_user_id holds the old one

A BSUID is regenerated when the user changes their phone number, so use these events to move your records from the old identifier to the new one:

for (const message of envelope.whatsapp.messages ?? []) {
  if (message.type !== "system") continue;
  if (message.system.type === "user_changed_user_id" || message.system.type === "user_changed_number") {
    migrateUser(message.system.previous_user_id, message.system.user_id);
  }
}

On the legacy API

If you stay on legacy, BSUIDs still work, with a smaller footprint. fromNumber, from and wa_id can hold a BSUID, the contact gains user_id, parent_user_id and profile.username (and WebSocket whatsapp_message gains userId, parentUserId and username), and you can reply to a BSUID with toNumber / to. When a phone number appears or disappears the sender gets a new conversationId. See Webhook Payload.

On this page