Skip to main content

Remote pairing — hosted pairing link

For SaaS platforms that manage numbers for end customers, Remote Pairing lets you generate a pairing link instead of displaying the QR code yourself. Your customer opens the link, scans the QR (or runs Meta Embedded Signup), and the number connects to your account.
Pairing Link modal with the hosted connect link

A remote pairing session — send the hosted link to your customer; when they scan the QR, the number.connected webhook fires.

POST /v1/numbers/remote-pairing

Same body as POST /v1/numbers, plus pairing-specific options:
Response 201:
Differences from POST /v1/numbers:
  • The instance is created in CLOSE state (disconnected) — no QR code is returned.
  • remotePairingUrl is the public link for the end customer. The endpoint only hands the link back — you deliver it to your customer yourself (your own UI, email, chat, wherever they are).
  • When the end customer opens the URL and connects, the number.connected webhook fires normally.

linkToApiKey — retired

Accepted but does nothing. Sending it is not an error, and no API key is touched: linkedApiKeyId in the response is always null. It used to re-point a number-scoped key at the number you had just paired, silently unscoping it from the number it was bound to.

warnings on the 201

A successful pairing may come back with a warnings array of strings. It is present only when there is something to report. For provider=PILOT_STATUS, externalRef and redirectUrl are accepted but cannot be honored: they ride the Meta signed session, and the QR pairing link carries no session. Each one you send is reported in warnings — and the pairing still succeeds. It is not a 400:
They used to be discarded in silence. If you need either of them, use provider=META — the Meta branch is the one that carries them through the signed session.

History: decide it on this call

Whoever opens the link connects on the spot, and the provider hands the device history over right after — once. There is no moment in between for a PATCH, so this route accepts the settings block of number settings, for both providers:
Response 201 for META:
  • settings.historyImportEnabled defaults to true. Omit the block to keep the device history; send false to drop it.
  • numberId is the placeholder the connection lands on. Use it to follow the connection (GET /v1/numbers/{id}/status) or to PATCH it before the person finishes.
  • An invalid block answers 400 INVALID_SETTINGS and nothing is created.
  • For PILOT_STATUS, the block is applied only when this call creates the number.
Changed on 2026-09-16. Before that, provider=META ignored settings without a word and the 201 carried no numberId: the only way to set history on a Meta link was a PATCH on the placeholder, with the id taken from inside the link’s token. Read numberId from the body — the token is an internal format and can change.
The body also accepts an optional branding object to override the connect page’s logo/colors/title for just this link (snapshotted onto the token). It overrides the tenant’s saved branding. See Connect page branding.

Meta numbers: metaFlow

For official Meta Cloud API numbers the pairing link can run Meta flows instead of QR pairing:
  • metaFlow: "embedded" — the hosted page runs Meta Embedded Signup. Whoever opens the link authenticates with Facebook inside Meta’s own dialog and picks the business portfolio and the phone number there.
  • metaFlow: "credentials" — the hosted page collects an existing WABA’s credentials. It also shows a route inbound webhooks to Pilot Status checkbox (on by default) that auto-subscribes that WABA to Pilot’s callback, so no manual webhook setup is needed — see webhook delivery.

Who authenticates decides who owns the WABA

The hosted page runs the same Embedded Signup component the dashboard runs, with the same app and the same configuration. Nothing in the pairing token pins a business portfolio. So the WABA lands in the portfolio of whoever logs into Facebook in that dialog — the end customer if they open the link, or you if an operator on your team opens it and selects your own portfolio. If your model is “the WABA is ours and we pay Meta”, have your operator run the link; if it is “the customer owns their WABA”, send the link to the customer. The API call is identical either way.
Embedded Signup here is configured with featureType: "whatsapp_business_app_onboarding" — Meta’s coexistence onboarding. The dialog shows the QR the number’s owner scans with the WhatsApp Business app on their phone, so the phone still has to be reachable at that moment, whoever is driving the dialog.
The portfolio picker does not appear for the Facebook profile that owns the developer app. That is Meta’s rule, not a bug: a portfolio that owns the app is excluded from its own Embedded Signup, and Meta hides an empty picker. Any other profile — including yours as a customer of this platform — sees it. If you are testing, use a Facebook profile with no role on the app.

POST /v1/numbers/meta — the BYO-WABA alternative

If you already hold metaPhoneId + metaWabaId + a system-user token server-side, POST /v1/numbers/meta attaches the number with no link and no end-customer interaction. It is agnostic about coexistence — it attaches a coexistence number and a pure Cloud API number alike. What it does not do is anything Embedded Signup does for you:
BYO-WABA cannot perform the coexistence pairing itself. Turning a phone that is live in the WhatsApp Business app into a Cloud API number happens inside Meta’s Embedded Signup dialog — there is no API for it, ours or Meta’s. POST /v1/numbers/meta is how you register a number that pairing already produced, not how you pair one.It also never asks Meta for the on-device history, so nothing is imported for that number. If you need the history, pair through Embedded Signup.
The two providers mint two different credentials, and they do not expire on the same clock. Read expiresAt off the 201 rather than hardcoding either number. For a META link, the number also shows up immediately as a placeholder whose GET /v1/numbers/{id}/status answers PENDING and then EXPIRED — that is the signal to show your customer an “the link expired” message and mint a new one.

The connect page origin (postMessage)

When you embed the connect page in your own site, it talks to the host page over postMessage (connect:paired, connect:error, connect:expired). Both providers use the same host — it comes from one platform-wide setting, not from the provider — and in production that host is:
Lock your listener to it, and reject every other origin.
Better than hardcoding it: derive it from the link the API just handed you — new URL(response.remotePairingUrl).origin. The host is a deployment setting; a receiver pinned to a literal breaks silently if it ever changes, while one derived from remotePairingUrl cannot drift.

Errors

Both 402s carry the numbers behind the refusal — plan, maxNumbers, currentNumberCount, extras, proratedTotal, walletBalance and currency — alongside error and code. The Free-plan refusal for a phone that already exists carries only plan, maxNumbers and currentNumberCount. Branch on the code: the two share a status but not a remedy. See capacity errors.
Fixed: INSUFFICIENT_FUNDS on this route now answers 402, the same status as POST /v1/numbers. It used to come back as a 500.
Changed. A plan whose allowance was simply full used to answer INSUFFICIENT_FUNDS here too, and the body carried nothing but the code repeated in both fields. It now answers PLAN_NUMBER_LIMIT_REACHED, with the capacity figures attached.

Public token endpoints (no authentication)

The pairing page uses these token-based endpoints — you can also call them to build your own connect UI: Notes:
  • Free plan: when the workspace that issued the link is on Free, its allowance is already full and this number never connected, POST /v1/remote-pairing/{token}/connect answers 402 with { "error": "PLAN_NUMBER_LIMIT_REACHED", "code": "PLAN_NUMBER_LIMIT_REACHED", "message": "…", "plan": "FREE", "maxNumbers": 1, "currentNumberCount": 1 }, and no QR code is generated. message is a bilingual sentence, English | Português. The person holding the link cannot fix it: the workspace has to buy an extra number or move up a plan. See Free plan.
  • This is the PILOT_STATUS (QR) token: it expires after 24 hours or upon successful connection (single-use). A META Embedded Signup link is a different credential with a 30-minute TTL — see Link lifetime.
  • POST .../connect only works when the state is not OPEN.
  • The same connection state shown in the dashboard and the webhooks (number.created, number.connected) apply.