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
| Area | Legacy | API v2 |
|---|---|---|
| Inbound payload | Meta-like payload reshaped by Pipes.bot, plus pipes.conversationId | One envelope per Meta change: { id, field, createdAt, pipes, whatsapp }, with whatsapp being Meta's change.value untouched |
conversationId | Created per sender, used for replies | Removed. Nothing is stored. A conversationId WebSocket parameter is rejected with 403 |
| Statuses | Not delivered | Delivered (sent, delivered, read, failed) as messages-field events |
| Other Meta fields | Template status in a Pipes.bot shape | Every change Meta sends for your numbers, including message_template_status_update |
| WebSocket inbound | whatsapp_message | event carrying the envelope |
| WebSocket send | whatsapp_reply { conversationId, toNumber, text } | send { requestId, poolNumberId, message }, answered by send_result |
REST /app/send | toNumber, response { success, messageId, conversationId } | to (or toNumber), response { messageId, contacts } |
REST /passthrough | to | to or recipient; body forwarded to Meta untouched |
| Signing | webhook-id / webhook-timestamp / webhook-signature | Same, with webhook-id equal to the envelope id |
| Offline queue | Per sender, replayed as whatsapp_message | One 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 (
conversationIdvalues 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
4000and reasonapi_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:
- Update your webhook handler to read envelopes (it can support both shapes during the switch: legacy payloads have
object: "whatsapp_business_account", v2 envelopes havefieldandwhatsapp). - Switch sending to
to/sendframes, and stop relying onconversationId. - Verify signatures with Standard Webhooks. See Webhook Security.
- Test your handler against the example payloads. The page includes a script that signs and replays them against your server.
- 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 thewebhook-idheader. Identical across retries and across both channels.- The WhatsApp message id (
wamid...) inwhatsapp.messages[].idfor messages, orwhatsapp.statuses[].idplusstatusfor 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[].fromis optional, andmessages[].from_user_id/from_parent_user_idare added.contacts[].wa_idis optional, andcontacts[].user_id,parent_user_idandprofile.usernameare added. - Statuses:
statuses[].recipient_user_idandrecipient_parent_user_idare added, andrecipient_idis 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.type | Meaning |
|---|---|
user_changed_number | The 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_id | The 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.
Passthrough Messages
Send template messages, interactive buttons, lists, CTA URL buttons, and other advanced WhatsApp message types via the passthrough endpoint.
API v2 Example Payloads
Ready-to-use API v2 webhook envelopes (fixtures) for testing your handler, with a script that signs and replays them.