[BUG] QR pairing never completes: <notification type='companion_reg_refresh'> is unhandled (reproduced independently in whatsmeow)
Describe the bug
As of 2026-07-28/29, WhatsApp sends a <notification type='companion_reg_refresh'> to the
companion client after the user scans the QR code. Baileys acks it and discards it.
pair-success is never emitted, the QR ref pool drains, and the phone shows
"Couldn't link device — try connection again."
I captured the same notification in whatsmeow (an independent Go implementation), where it
is logged verbatim as Unhandled notification with type companion_reg_refresh. Both libraries
receive it; neither implements it. This appears to be a server-side addition to the companion
registration flow that no open client currently handles.
This is likely the concrete mechanism behind #2488 (closed as not planned), which hypothesised
"a fourth pair stage that the client must wait for / ack before the server emits pair-success"
but could not identify it. companion_reg_refresh is that stage.
companion_reg_refresh appears zero times in the source of rc13, rc14, and master:
$ grep -rn "companion_reg_refresh" lib/ # 7.0.0-rc13, 7.0.0-rc14
$ grep -rn "companion_reg_refresh" src/ # master@74af8ee (2026-07-29)
$ # no output in any of the three
To Reproduce
- Start from a fresh/unregistered auth state (
useMultiFileAuthState on an empty dir).
- Create a socket with baileys' own logger at
trace (see connectOptions below).
- Wait for
connection.update with qr, render it.
- Scan with the WhatsApp mobile app → Settings → Linked Devices → Link a Device.
- Observe:
companion_reg_refresh arrives and is acked, nothing else happens, QR refs keep
rotating until QR refs attempts ended. Phone reports failure.
Expected behavior
After a successful scan, the server emits pair-success, baileys persists creds.me, emits
isNewLogin, and the connection reaches open (typically via a 515 restart).
Actual behavior — trace
Handshake and ref issuance are healthy:
info connected to WA browser=["Mac OS","Desktop","14.4.1"]
trace handshake recv from WA
info not logged in, attempting registration...
userAgent.appVersion = { primary: 2, secondary: 3000, tertiary: 1043857760 }
trace Noise handler transitioned to Transport state
trace recv xml <iq from='@s.whatsapp.net' type='set' id='…' xmlns='md'>
<pair-device><ref>…</ref>… 6 refs …</pair-device>
</iq>
trace xml send <iq to='@s.whatsapp.net' type='result' id='…'/>
EVENT connection.update hasQR=true
Then the phone scans (t+57s). This arrives:
trace recv xml <notification from='@s.whatsapp.net' type='companion_reg_refresh'
id='510447984' t='1785369275'>
<companion_reg_refresh />
</notification>
debug Event buffer activated
debug sent ack { recv: { tag: 'notification',
attrs: { type: 'companion_reg_refresh', … } },
sent: { class: 'notification', type: 'companion_reg_refresh' } }
trace xml send <ack id='510447984' to='@s.whatsapp.net'
class='notification' type='companion_reg_refresh'/>
debug Flushing event buffer bufferCount=1
After which baileys resumes rotating QR refs — no state change, no pair-success:
EVENT connection.update hasQR=true (t+60s)
EVENT connection.update hasQR=true (t+80s)
…
info connection errored
Error: QR refs attempts ended
at Timeout.genPairQR (lib/Socket/socket.js:713:26)
Note the notification only ever arrives after a successful scan — so QR legibility,
device-slot limits, and scan timing are all excluded as causes.
Independent corroboration (whatsmeow)
Same account, same machine, same network, minutes later, using
go.mau.fi/whatsmeow@v0.0.0-20260722203353-e9a033b24933:
[WM/Recv] <notification from="s.whatsapp.net" id="3675248091" t="1785370543"
type="companion_reg_refresh"><companion_reg_refresh/></notification>
[WM DEBUG] Unhandled notification with type companion_reg_refresh
[WM/Send] <ack class="notification" id="3675248091"
to="s.whatsapp.net" type="companion_reg_refresh"/>
whatsmeow never reaches PairSuccess either. It encodes the QR in the newer
https://wa.me/settings/linked_devices#2@… URL form (baileys uses the bare comma-joined form)
and still fails identically — so QR payload encoding is not the variable.
Secondary finding: the pair-code flow is also broken (400 bad-request)
requestPairingCode() fails at stage 1, so code entry is not a workaround:
xml send <iq to='@s.whatsapp.net' type='set' id='…' xmlns='md'>
<link_code_companion_reg jid='<REDACTED>@s.whatsapp.net'
stage='companion_hello'
should_show_push_notification='true'>
<link_code_pairing_wrapped_companion_ephemeral_pub>…
<companion_server_auth_key_pub>…
<companion_platform_display>…
<companion_platform_id>…
<link_code_pairing_nonce>…
</iq>
recv xml <iq from='@s.whatsapp.net' type='error' id='…'>
<error code='400' text='bad-request'/>
</iq>
There is a reporting bug on top of this: requestPairingCode() resolves and returns a code
before the server's response is evaluated. The 400 above arrived after the promise
resolved, so callers display a plausible-looking code that was never registered server-side,
and the user is told to enter a code that does not exist. Independent of the protocol issue,
this should reject rather than return a bogus code.
Environment
- Is this on a server? No. Local macOS desktop (darwin arm64), residential connection.
- baileys version:
7.0.0-rc13. Also verified 7.0.0-rc14 and master@74af8ee (2026-07-29) — the QR path is byte-identical and none contain companion_reg_refresh.
- Node: 24.18.0
- WA Web version:
2.3000.1043857760, obtained via fetchLatestBaileysVersion() which reports isLatest: true. (Worth noting: rc13's hardcoded default is 2.3000.1035194821, but the runtime fetch supplies the current one, so this is not a stale-version issue.)
- What do your
connectOptions look like?
makeWASocket({
auth: state, // useMultiFileAuthState, empty dir
version, // from fetchLatestBaileysVersion()
browser: Browsers.macOS('Desktop'), // also reproduced with a custom browser triple
logger, // pino-like, level 'trace'
printQRInTerminal: false,
syncFullHistory: false,
})
- Do you have multiple clients on the same IP? Not during the captured runs — exactly one client was connected each time (verified by process list). Earlier the same day there had been concurrent attempts from two processes; I eliminated that and the failure is unchanged.
- Are you using a proxy? No.
HTTP_PROXY/HTTPS_PROXY/NO_PROXY unset in both the shell and the service environment; macOS system web proxy disabled.
- Account: personal, multi-device, 0 linked devices at time of testing (4 free slots).
- Clock: verified against NTP, +11ms.
Additional context
What I could not determine is what the client is expected to do on receiving
companion_reg_refresh. Acking it is evidently not sufficient. Plausible readings — I have no
way to confirm which:
- Re-send the
pair-device registration (i.e. refresh the companion registration) and await new refs.
- Send a follow-up
companion_verify-style IQ before the server will emit pair-success.
- Re-run the ADV signing step using a value carried in/implied by the refresh.
Anyone with visibility into current WA Web client behaviour could likely confirm quickly.
Impact: new device links appear impossible on any baileys release. Already-registered
sessions keep working — the break is specific to the pairing path, which is why it only
surfaces for users who re-link.
Happy to re-run either harness with extra instrumentation, or to test a patch.
[BUG] QR pairing never completes:
<notification type='companion_reg_refresh'>is unhandled (reproduced independently in whatsmeow)Describe the bug
As of 2026-07-28/29, WhatsApp sends a
<notification type='companion_reg_refresh'>to thecompanion client after the user scans the QR code. Baileys acks it and discards it.
pair-successis never emitted, the QR ref pool drains, and the phone shows"Couldn't link device — try connection again."
I captured the same notification in whatsmeow (an independent Go implementation), where it
is logged verbatim as
Unhandled notification with type companion_reg_refresh. Both librariesreceive it; neither implements it. This appears to be a server-side addition to the companion
registration flow that no open client currently handles.
This is likely the concrete mechanism behind #2488 (closed as not planned), which hypothesised
"a fourth pair stage that the client must wait for / ack before the server emits
pair-success"but could not identify it.
companion_reg_refreshis that stage.companion_reg_refreshappears zero times in the source of rc13, rc14, and master:To Reproduce
useMultiFileAuthStateon an empty dir).trace(seeconnectOptionsbelow).connection.updatewithqr, render it.companion_reg_refresharrives and is acked, nothing else happens, QR refs keeprotating until
QR refs attempts ended. Phone reports failure.Expected behavior
After a successful scan, the server emits
pair-success, baileys persistscreds.me, emitsisNewLogin, and the connection reachesopen(typically via a515restart).Actual behavior — trace
Handshake and ref issuance are healthy:
info connected to WA browser=["Mac OS","Desktop","14.4.1"] trace handshake recv from WA info not logged in, attempting registration... userAgent.appVersion = { primary: 2, secondary: 3000, tertiary: 1043857760 } trace Noise handler transitioned to Transport state trace recv xml <iq from='@s.whatsapp.net' type='set' id='…' xmlns='md'> <pair-device><ref>…</ref>… 6 refs …</pair-device> </iq> trace xml send <iq to='@s.whatsapp.net' type='result' id='…'/> EVENT connection.update hasQR=trueThen the phone scans (t+57s). This arrives:
After which baileys resumes rotating QR refs — no state change, no
pair-success:Note the notification only ever arrives after a successful scan — so QR legibility,
device-slot limits, and scan timing are all excluded as causes.
Independent corroboration (whatsmeow)
Same account, same machine, same network, minutes later, using
go.mau.fi/whatsmeow@v0.0.0-20260722203353-e9a033b24933:whatsmeow never reaches
PairSuccesseither. It encodes the QR in the newerhttps://wa.me/settings/linked_devices#2@…URL form (baileys uses the bare comma-joined form)and still fails identically — so QR payload encoding is not the variable.
Secondary finding: the pair-code flow is also broken (
400 bad-request)requestPairingCode()fails at stage 1, so code entry is not a workaround:There is a reporting bug on top of this:
requestPairingCode()resolves and returns a codebefore the server's response is evaluated. The
400above arrived after the promiseresolved, so callers display a plausible-looking code that was never registered server-side,
and the user is told to enter a code that does not exist. Independent of the protocol issue,
this should reject rather than return a bogus code.
Environment
7.0.0-rc13. Also verified7.0.0-rc14andmaster@74af8ee(2026-07-29) — the QR path is byte-identical and none containcompanion_reg_refresh.2.3000.1043857760, obtained viafetchLatestBaileysVersion()which reportsisLatest: true. (Worth noting: rc13's hardcoded default is2.3000.1035194821, but the runtime fetch supplies the current one, so this is not a stale-version issue.)connectOptionslook like?HTTP_PROXY/HTTPS_PROXY/NO_PROXYunset in both the shell and the service environment; macOS system web proxy disabled.Additional context
What I could not determine is what the client is expected to do on receiving
companion_reg_refresh. Acking it is evidently not sufficient. Plausible readings — I have noway to confirm which:
pair-deviceregistration (i.e. refresh the companion registration) and await new refs.companion_verify-style IQ before the server will emitpair-success.Anyone with visibility into current WA Web client behaviour could likely confirm quickly.
Impact: new device links appear impossible on any baileys release. Already-registered
sessions keep working — the break is specific to the pairing path, which is why it only
surfaces for users who re-link.
Happy to re-run either harness with extra instrumentation, or to test a patch.