Skip to main content
zapo connects as a companion device — exactly like linking WhatsApp Web or Desktop. The first connection pairs the device; after that, credentials stored in your store are reused automatically.

The pairing flow

Pairing is driven entirely through events emitted during connect(): Once auth_paired fires, the credentials are written to the store and reused on every subsequent connect() — you will not see auth_qr again unless the session is unlinked or cleared.
Paired. Credentials now live in your store — restart the process and connect() resumes the session with no new QR.

Pairing with a QR code

This is the default flow. Render the qr string as a QR image and scan it from WhatsApp → Linked devices → Link a device.
The QR rotates automatically; auth_qr fires again with a fresh value each time, so always render the latest one.

Pairing with a code

Prefer entering an 8-character code on the phone instead of scanning? Request one through client.auth after the connection is established. Listen for auth_pairing_required, then request the code for the target phone number (digits only, with country code):
requestPairingCode(phoneNumber, shouldShowPushNotification?, customCode?) requires an active connection and returns the code as a string. On the phone, open Linked devices → Link with phone number instead.

Passkey-gated linking (Shortcake)

For some accounts, WhatsApp’s server refuses the plain QR / pairing-code flow and demands a WebAuthn passkey assertion from the account owner’s authenticator before a companion can link. The wire protocol behind this is documented in depth in WhatsApp passkey & Shortcake linking; this section covers how to opt in on the client side. The trigger is server-driven. When it fires, zapo emits auth_passkey_required as a heads-up (hasSigner tells you whether the handshake will actually proceed) and then runs the Shortcake handshake internally — provided you configured a signer.

The signer

Set signPasskeyAssertion on WaClientOptions. zapo hands you the server’s raw WebAuthn request options, and you hand back the assertion + credential id:
The credential source stays outside the library — a real hardware/OS authenticator, a virtual authenticator, or a relay to another process — zapo never touches passkey material directly.
There is no headless bypass. Without a signer that produces an assertion the account owner’s own authenticator would sign (see the reverse-engineering page for why the wall holds), the link cannot complete on a passkey-gated account. When hasSigner is false on auth_passkey_required, the lib acks the prologue but the handshake stops — surface a UI prompt telling the user a passkey is required, and either configure a signer for the next attempt or fall back to a device that can complete it.
auth_paired still fires on success — the Shortcake handshake is just an additional step slotted into the normal pairing flow (usually right after pair-device or a pairing-code companion_finish). No new “paired” event is introduced.

Credentials

After pairing, the current credentials are available synchronously:
WaAuthCredentials contains the device’s secret keys. It is marked @sensitive for a reason: anything that can read these can impersonate the device. If you persist them outside the built-in store, encrypt them at rest.

Logging out

logout() unpairs the companion device server-side (it removes this device from the account’s linked devices). It requires an authenticated session:
By default this also clears stored state. You can control exactly which store domains are wiped on logout via the logoutStoreClear option — see Configuration.

Disconnect vs. logout

Use disconnect() for a graceful shutdown you intend to resume; use logout() to permanently unlink.

Next

Stores

Where credentials and Signal state are persisted.

Reconnection

Handle connection: close and reconnect.
Last modified on July 2, 2026