#1653·OpenWA

[Bug]: whatsapp-web.js pairing-code request for an already-linked number can unlink the live device

Author: rmyndharisCreated Sep 18, 2026Updated Sep 18, 2026

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

  1. Run an OpenWA instance with ENGINE_TYPE=whatsapp-web.js and link a session by QR. Confirm it reads ready with its phone number set.
  2. Bring a session on that instance to qr_ready.
  3. POST /api/sessions/{sessionId}/pairing-code with the phone number of the already linked account.
  4. 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-699 refuses a session whose stored status is READY; src/engine/adapters/wwebjs-lifecycle.ts:1030-1032 and src/engine/adapters/baileys-lifecycle.ts:950-952 refuse an engine that is not at QR_READY. The requested phoneNumber is never compared with sessions.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-511 emits DISCONNECTED 'LOGOUT' and then awaits authStrategy.logout(), which removes the user data directory, whenever the page navigates to a post_logout=1 URL. OpenWA's handler in wwebjs-lifecycle.ts only 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'), then initializeAltDeviceLinking(), then startAltLinkingFlow(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 setInterval that regenerates a code every 3 minutes while the socket is UNPAIRED (Client.js:553-564), which OpenWA cannot cancel because cancelPairingCode is deliberately not exposed (docs/29-engine-capability-matrix.md:638); and showNotification defaults to true, so every generated code pushes a notification to the account's phone.
  • Baileys is structurally different and is not implicated. requestPairingCode there writes only that socket's own credentials and sends one link_code_companion_reg IQ, 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.md and in the route's entry in docs/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 requestPairingCode in src/engine/engine-capability-matrix.ts recording 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 main at 62dbe393
  • Engine: whatsapp-web.js 1.34.7
  • Baileys 7.0.0-rc14 for the contrast runs