Skip to main content

Troubleshooting: WhatsApp re-authentication and Embedded Sign-up recovery

Eleven error codes cover the connect and re-authenticate lifecycle of a WhatsApp Business Account (WABA): the Embedded Sign-up run itself, the selection you make inside it, and the token the connection stores afterward. When any of them fires, the fix is almost always one clean re-run of the connect flow — this page decodes the family so you run it once, not five times. The recovery endpoint for the whole family is POST /api/v1/whatsapp/connect, the same call Embedded Sign-up makes. Every family below resolves back to “run connect again” — the differences are what you change in Meta Business Manager first, and how you confirm the recovery took. The send-layer codes (WHATSAPP_NOT_CONNECTED, WHATSAPP_CONNECTION_INVALID, WHATSAPP_TOKEN_EXPIRED) are decoded on WhatsApp connection recovery; this page owns everything the connect flow itself returns.
These are tenant-owned controls: the recovery steps act on your Meta Business Manager, your WABA, and your Orbit organization’s channel settings. WHATSAPP_META_APP_DISABLED is the one exception — it is a platform-side flag and is marked accordingly below.

Owner split: re-auth vs selection vs ownership

The single most common mis-route in this family is treating a selection error as a re-auth error — the dashboard says “connection failed,” you re-run the sign-up popup, and it fails identically because the mistake was the (WABA, phone) pair, not the token. Split the code first:

Re-auth family

WHATSAPP_TOKEN_EXCHANGE_FAILED — the sign-up handshake itself

Meta auth codes are single-use and expire roughly ten minutes after the OAuth dialog issues them. The exchange fails when the popup lingers past the window, when the same code value is POSTed twice (the second POST always loses), or when a browser extension rewrites the redirect. The server rejects the exchange with a 400 and this code; a malformed 200 without a token body surfaces the same code as a 502. Recovery — re-run the exchange with a fresh code:
  1. Open a fresh Embedded Sign-up run from Settings → Channels → WhatsApp → Connect and finish the Meta dialog without pausing between steps.
  2. Send the new code immediately — the dashboard does this in one motion; over the API it is:
  1. Back to green: a {"data": {"status": "connected"}} response, then GET /api/v1/whatsapp/status returns connected: true.
Retrying the same code is the anti-pattern to avoid — it burned the code’s single use on the first attempt, and every subsequent POST returns WHATSAPP_TOKEN_EXCHANGE_FAILED again.

WHATSAPP_ACCESS_TOKEN_EXPIRED — the stored long-lived token is dead

Meta long-lived tokens cap at 60 days, and a token revoked in Meta Business Manager dies the same way. Orbit stores the resolved expiry at connect time, so a connection that reports WHATSAPP_CONNECTION_INVALID at send time is the lap-over of this same class — the connect-side decoder here is the one the recovery popup surfaces. Recovery — refresh the token in place:
  1. Re-run Settings → Channels → WhatsApp → Connect with the same Facebook account and Business Manager as the original connect. Meta mints a fresh token for the existing WABA; Orbit overwrites the stored credential and re-links templates and phone numbers — nothing is deleted.
  2. Back to green: GET /api/v1/whatsapp/status returns the same waba_id + phone_number_id pair as before, now with connected: true and status: "active". A template send to a known-good recipient completes the check.
Deleting the channel and starting over is the refusal path you do not want — it orphan-links the templates and phone numbers the refresh would have re-bound.

WHATSAPP_CONNECTION_NOT_FOUND — the row does not exist

A connect, rename, or default-setting call referenced a phone_number_id the organization does not hold. The typical trigger is an integration that cached a phone_number_id before someone disconnected (or renamed) the channel, or a send scoped to a specific connection that was removed. Check the actual rows before you re-run anything:
Each row carries waba_id, phone_number_id, label, connected, and status — this is the field-level check the whole family leans on. If the row you need is genuinely gone, re-run the connect flow to recreate it; if it exists under a different label, either re-point the caller to the current phone_number_id or rename it back with the scoped rename call. Back to green: the caller’s cached id matches a row in this list and GET /api/v1/whatsapp/status reports the picked connection active.

WHATSAPP_META_APP_DISABLED — the one platform-side flag

New connections fail while Devotel’s Meta app is unavailable (typically a Meta-side review). Existing connections keep working — this flag blocks only the connect flow. Recovery is to wait for restoration; status updates post to status.orbit.devotel.io. Retrying connect against this flag returns the same code, so pause the loop and re-try on the next status check.

Selection family

WHATSAPP_NO_WABA — zero WABAs on the login

The token exchange succeeded, debug_token resolved, and the granted WABA id list came back empty. The Meta login has no WhatsApp Business Account — typically the wrong Facebook account, a Business Manager that never claimed a WABA, or granular scopes granted without whatsapp_business_management. Fix the account, then re-run: log in to Meta Business Manager with the account you will present, confirm a WABA exists under WhatsApp Manager, and re-run connect. Back to green: the connect response is connected with a real waba_id — no picker needed when the login carries exactly one (WABA, phone) pair.

WHATSAPP_NO_PHONE_NUMBER — a WABA with nothing to send from

Every granted WABA was enumerated and none of them carried a registered phone number. A WABA without a phone can be picked in Embedded Sign-up but cannot connect. Register and verify a number in Meta Business Manager → WhatsApp Manager → Phone numbers, then re-run connect. Back to green: GET /api/v1/whatsapp/status returns a non-null phone_number_id and phone_display.

WHATSAPP_INVALID_SELECTION — the pair you sent is not offered

Multi-WABA logins return a needs_selection response listing every (waba_id, phone_number_id) option the token grants; the dashboard (or your integration) then re-POSTs /connect with the chosen pair. The server validates the pair against the discovered options — a mismatched, typo’d, or replayed pair is refused with a 422 and this code. That guard exists so a connect call cannot be steered onto a WABA the granted token cannot see. Re-read the option list from the first POST (or from GET /api/v1/whatsapp/connections when a partial state was stashed), then re-send with an option that appears verbatim:
Back to green: connected with the waba_id and phone_number_id you named, visible in GET /api/v1/whatsapp/status.

WHATSAPP_BUSINESS_STATUS_PENDING — verification still under review

The connection can complete while Meta’s business review is open, but higher messaging tiers and marketing-template approval stay locked until the review resolves. This is a warning, not a hard failure. Watch Meta Business Settings → Business Info; a pending decision older than roughly two weeks is worth a resubmission with a cleaner document set rather than a dispute. Back to green: the review resolves at Meta and the tier reopens without a reconnect.

WHATSAPP_BUSINESS_VERIFICATION_REQUIRED — verification never started

Meta blocks connects outright when the business has not completed verification. The fix lives entirely in Meta Business Manager — submit the verification documents under Business Info → Business verification, then re-run connect. Back to green: a Meta-approved verification followed by a successful connected response.

Ownership family

WHATSAPP_PHONE_NOT_OWNED — the number is not on the WABA you picked

You selected a phone number that is not registered under the WABA you chose — a stale picker option, or a number that lives on a different WABA of the same login. Re-running with the same pair returns the same code. Attach the number to the intended WABA in WhatsApp Manager → Phone numbers, or re-run the picker and choose the pair as Meta reports them. Back to green: the needs_selection option list (or GET /api/v1/whatsapp/connections) contains the pair and the re-POST connects.

WHATSAPP_WABA_ALREADY_LINKED — claimed by another organization

A WABA binds to exactly one Orbit organization. Connecting it to a second organization is refused even when the Meta token and selection are clean. Resolve the ownership dispute first: disconnect the WABA on the organization that holds it (Settings → Channels → WhatsApp → Disconnect, or the scoped DELETE /api/v1/whatsapp/connections/:phoneNumberId), then connect it here. If neither organization is one you control, treat it as an account dispute and contact whatsapp-support@devotel.io with the WABA id rather than attempting a takeover. Back to green: the GET /api/v1/whatsapp/connections list on this organization picks up the released WABA.

What not to do

  • Do not re-POST the same code after a token-exchange failure. The code is single-use; the second POST burns nothing and returns the same refusal. Always mint a fresh code from a fresh dialog run.
  • Do not delete the channel to “reset” a bad token. A reconnect overwrites credentials in place; a delete orphans the phone-number mapping and forces a rebuild the refresh would have skipped.
  • Do not pick a second WABA to route around a selection error. WHATSAPP_INVALID_SELECTION is the picker guarding the granted options — splitting sends across WABAs over one bad pair splits your quality rating too.
  • Do not loop POST /whatsapp/connect against WHATSAPP_META_APP_DISABLED. The flag releases on Devotel’s side; each retry returns the same code and burns your rate budget.
  • Do not escalate a selection error without the (waba_id, phone_number_id) you sent. Support reconstructs the picker from those ids — a report without them starts from nothing.

Escalation payload

Worked the family and still red? Email whatsapp-support@devotel.io with these four items so the recovery can be traced without a back-and-forth:
  1. The exact error code from the family above, with the timestamp of the failing call.
  2. The waba_id and phone_number_id you sent — even on a failed selection, those ids name which pick failed.
  3. The current GET /api/v1/whatsapp/status payload — it carries the connected, status, and account_review_status fields that decide which family member is live.
  4. For WHATSAPP_META_APP_DISABLED only, the status.orbit.devotel.io check you saw at retry time, to confirm the flag is still open.

See also