#2737·Baileys

[BUG] QR pairing never completes: <notification type='companion_reg_refresh'> is unhandled (reproduced independently in whatsmeow)

Author: bankon1tCreated Jul 30, 2026Updated Sep 11, 2026

[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:

bash
$ 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

  1. Start from a fresh/unregistered auth state (useMultiFileAuthState on an empty dir).
  2. Create a socket with baileys' own logger at trace (see connectOptions below).
  3. Wait for connection.update with qr, render it.
  4. Scan with the WhatsApp mobile app → Settings → Linked Devices → Link a Device.
  5. 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:

xml
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:

xml
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/[email protected]:

[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
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?
    javascript
    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:

  1. Re-send the pair-device registration (i.e. refresh the companion registration) and await new refs.
  2. Send a follow-up companion_verify-style IQ before the server will emit pair-success.
  3. 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.