[Bug]: whatsapp-web.js pairing-code request for an already-linked number can unlink the live device
On the whatsapp-web.js engine, POST /api/sessions/{sessionId}/pairing-code for a phone number that already has a linked session can end with WhatsApp revoking that device. Within about 60 seconds the linked session logs WhatsApp unlinked this device (LOGOUT), whatsapp-web.js deletes the stored credentials, and the session returns to qr_ready with phone: null. Nothing in the API reference, the pairing example, the dashboard or the engine capability matrix warns that this endpoint carries that risk.
Steps to reproduce
- Run an OpenWA instance with
ENGINE_TYPE=whatsapp-web.jsand link a session by QR. Confirm it readsreadywith its phone number set. - Bring a session on that instance to
qr_ready. POST /api/sessions/{sessionId}/pairing-codewith the phone number of the already linked account.- Watch the linked session for about a minute.
Expected
Either a pairing code with no effect on the existing link, or a refusal that names the risk before anything is sent.
Actual
The HTTP request frequently fails first, with either a page error whose message is literally t: t or requestPairingCode attempt timed out, both answered as HTTP 500 (tracked separately). Within about a minute the linked session logs WhatsApp unlinked this device (LOGOUT), the stored credentials are removed, and the session falls back to qr_ready with phone: null. Reproduced twice with real numbers, on 2026-09-17 and 2026-09-18, the first time against a live session that then needed a fresh QR scan.
Analysis
The guards today are state-only, and none of them looks at the number.
src/modules/session/session.service.ts:697-699refuses a session whose stored status isREADY;src/engine/adapters/wwebjs-lifecycle.ts:1030-1032andsrc/engine/adapters/baileys-lifecycle.ts:950-952refuse an engine that is not atQR_READY. The requestedphoneNumberis never compared withsessions.phone, with the engine's own number, or with any other session's number.- The credential deletion is performed upstream.
node_modules/whatsapp-web.js/src/Client.js:504-511emitsDISCONNECTED 'LOGOUT'and then awaitsauthStrategy.logout(), which removes the user data directory, whenever the page navigates to apost_logout=1URL. OpenWA's handler inwwebjs-lifecycle.tsonly mirrors that removal idempotently so the lifecycle has something to await. - The whatsapp-web.js pairing call mutates WhatsApp Web's own linking state in the page before it requests anything:
setPairingType('ALT_DEVICE_LINKING'), theninitializeAltDeviceLinking(), thenstartAltLinkingFlow(phoneNumber, showNotification)(Client.js:544-551). It documents no precondition and checks none; the UNPAIRED state test exists only inside the repeating interval it installs, not before the first request. - Two further whatsapp-web.js side effects are worth recording: the call arms an in-page
setIntervalthat regenerates a code every 3 minutes while the socket is UNPAIRED (Client.js:553-564), which OpenWA cannot cancel becausecancelPairingCodeis deliberately not exposed (docs/29-engine-capability-matrix.md:638); andshowNotificationdefaults totrue, so every generated code pushes a notification to the account's phone. - Baileys is structurally different and is not implicated.
requestPairingCodethere writes only that socket's own credentials and sends onelink_code_companion_regIQ, with no shared page and no global linking-mode reset. It was exercised repeatedly without side effects.
What the source cannot settle: why WhatsApp revoked a companion in response, whether the in-page socket was genuinely UNPAIRED at the moment of the call, and what t: t is (a minified in-page exception with no counterpart in any readable source).
Suggested direction
The destructive step belongs to whatsapp-web.js and to WhatsApp, so the useful work is to stop presenting phone pairing as a risk-free peer of QR on this engine:
- A warning in
docs/examples/session-phone-number-pairing.mdand in the route's entry indocs/06-api-specification.md. - A line in the dashboard's phone-pairing panel, which today shows four neutral instruction steps and no engine-specific text.
- A curated entry for
requestPairingCodeinsrc/engine/engine-capability-matrix.tsrecording that the two engines differ in blast radius. The method has no curated row today, so it renders as plainly supported on both.
A local safety net that refuses when the requested number matches the phone of a ready session on the same node is defensible, as long as it is described as a net rather than a fix: it cannot see a device linked on another node, another gateway, or any other WhatsApp client, which is exactly the topology where this was measured.
Not proposed here: closing the whatsapp-web.js window between the link being accepted and the authenticated event, which Baileys closes by moving to AUTHENTICATING on isNewLogin (src/engine/adapters/baileys-lifecycle.ts:486-495). whatsapp-web.js exposes no earlier signal, and no evidence yet points at that window as the mechanism.
Environment
- OpenWA 0.23.4 and a build of
mainat 62dbe393 - Engine: whatsapp-web.js 1.34.7
- Baileys 7.0.0-rc14 for the contrast runs
Source: rmyndharis/OpenWA