Skip to content

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

Description

@bankon1t

[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

  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:

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:

  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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions