> ## Documentation Index
> Fetch the complete documentation index at: https://zapo.to/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Pair a device with a QR code or 8-character pairing code, persist Noise credentials across restarts, and cleanly log out of a WhatsApp session.

`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](/en/concepts/stores) are reused automatically.

## The pairing flow

Pairing is driven entirely through events emitted during `connect()`:

```mermaid theme={null}
sequenceDiagram
  participant App as Your app
  participant C as WaClient
  participant WA as WhatsApp
  App->>C: connect()
  C->>WA: open + Noise handshake
  WA-->>C: pairing required
  C-->>App: auth_qr (or auth_pairing_code)
  Note over App,WA: user scans the QR / enters the code on their phone
  WA-->>C: paired
  C-->>App: auth_paired · credentials persisted
```

| Event                   | Payload                              | When                                                                                                                       |
| ----------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `auth_qr`               | `{ qr: string, ttlMs: number }`      | A new QR is available to render. Re-emitted as it rotates.                                                                 |
| `auth_pairing_code`     | `{ code: string }`                   | An 8-character pairing code was issued (code flow).                                                                        |
| `auth_pairing_required` | `{ forceManual: boolean }`           | The session needs pairing input.                                                                                           |
| `auth_passkey_required` | `{ hasSigner: boolean }`             | The server is forcing a passkey to link this device (see [Passkey-gated linking](#passkey-gated-linking-shortcake) below). |
| `auth_paired`           | `{ credentials: WaAuthCredentials }` | Pairing succeeded; credentials are now persisted.                                                                          |

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.

<Check>
  Paired. Credentials now live in your store — restart the process and `connect()` resumes the session with no new QR.
</Check>

## 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**.

```ts theme={null}
import qrcode from 'qrcode-terminal'

client.on('auth_qr', ({ qr, ttlMs }) => {
  qrcode.generate(qr, { small: true })
  console.log(`QR valid for ${ttlMs}ms`)
})

client.on('auth_paired', ({ credentials }) => {
  console.log('Paired as', credentials.meJid)
})

await client.connect()
```

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

```ts theme={null}
client.on('auth_pairing_required', async () => {
  const code = await client.auth.requestPairingCode('5511999999999')
  // Format for display, e.g. "ABCD-1234"
  console.log('Enter on your phone:', code.match(/.{1,4}/g)?.join('-'))
})

client.once('auth_paired', () => console.log('Paired!'))

await client.connect()
```

<Note>
  `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**.
</Note>

## 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](/en/reverse-engineering/passkey-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:

```ts theme={null}
import { WaClient, type WaShortcakeAssertionSigner } from 'zapo-js'

const signPasskeyAssertion: WaShortcakeAssertionSigner = async (requestOptions) => {
  // requestOptions is a Uint8Array — the raw PublicKeyCredentialRequestOptions
  // JSON the server issued. Route it through your authenticator of choice.
  const { credentialId, webauthnAssertion } = await myAuthenticator.sign(requestOptions)
  return { credentialId, webauthnAssertion }
}

const client = new WaClient({
  store,
  sessionId: 'default',
  signPasskeyAssertion
}, logger)
```

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.

<Warning>
  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.
</Warning>

```ts theme={null}
client.on('auth_passkey_required', ({ hasSigner }) => {
  if (hasSigner) {
    console.log('server is forcing a passkey — running Shortcake handshake…')
  } else {
    console.warn('passkey required but no signPasskeyAssertion is configured; link cannot proceed')
  }
})
```

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

```ts theme={null}
const credentials = client.getCredentials() // WaAuthCredentials | null
console.log(credentials?.meJid)
```

<Warning>
  `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.
</Warning>

## Logging out

`logout()` unpairs the companion device server-side (it removes this device from the account's linked devices). It requires an authenticated session:

```ts theme={null}
await client.logout()
```

By default this also clears stored state. You can control exactly which store domains are wiped on logout via the `logoutStoreClear` option — see [Configuration](/en/concepts/configuration#logout-store-clearing).

## Disconnect vs. logout

|                    | `disconnect()`                               | `logout()`                |
| ------------------ | -------------------------------------------- | ------------------------- |
| Closes the socket  | Yes                                          | Yes                       |
| Keeps credentials  | **Yes** — reconnect later without re-pairing | No — device is unlinked   |
| Server-side effect | None                                         | Removes the linked device |

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

## Next

<CardGroup cols={2}>
  <Card title="Stores" icon="database" href="/en/concepts/stores" arrow>
    Where credentials and Signal state are persisted.
  </Card>

  <Card title="Reconnection" icon="arrows-rotate" href="/en/guides/reconnection" arrow>
    Handle `connection: close` and reconnect.
  </Card>
</CardGroup>
