WhatsApp QR Code vs Pairing Code Login (Explained)
A practical comparison of the two ways unofficial WhatsApp libraries link a session: QR scan vs the 8-character pairing code. How each works, when to use which, and code for Baileys and whatsapp-web.js.
- WhatsApp's multi-device protocol officially supports two ways to link a companion device: scan a QR code or enter an 8-character pairing code under Link with phone number.
- Both methods create the exact same companion-device session and the same end-to-end-encrypted key material — the difference is purely operational, not a security tier.
- QR is the default and is easiest when a screen is available; the pairing code is the better choice for headless servers, SSH boxes, Docker, and CLI tools where rendering a QR is awkward.
- Baileys uses sock.requestPairingCode(phoneNumber) with the number in E.164 format and no plus sign; whatsapp-web.js uses the pairWithPhoneNumber client option (merged Aug 2025) and a client.on('code') event.
- Linking any account to unofficial libraries can get it banned. For production messaging, evaluate the official WhatsApp Business Cloud API; use these libraries at your own account risk.
Two official ways to link a device
Since WhatsApp moved to its multi-device architecture, your phone is no longer the only place a session can live. The phone remains the primary device, but it can authorize up to four companion devices that each hold their own encryption keys and keep working even when the phone is offline. WhatsApp Web, the desktop apps, and unofficial libraries like Baileys and whatsapp-web.js all connect as companion devices through this same protocol.
There are exactly two officially supported ways to link a companion device, and both are built directly into the WhatsApp app under Linked Devices. The first is the familiar QR code scan. The second is Link with phone number, which produces an 8-character pairing code you type on the primary phone. Unofficial libraries do not invent a third path — they invoke the same linking handshake the official clients use, then drive whichever method you choose.
How QR code login works
With QR login, the library opens a connection to WhatsApp and receives a QR payload that it renders as an image or terminal art. You open WhatsApp on your phone, go to Settings, then Linked Devices, then Link a Device, and point the camera at the code. The phone and the new companion device complete a key exchange, and the session is registered.
A key detail: the QR rotates. WhatsApp re-issues a fresh code on a short cycle (roughly every 20 seconds), so the library emits a new payload each time and you must keep surfacing the latest one until the scan succeeds. If your QR is a static screenshot, it will expire before a user gets to it.
- Best when a screen and camera are both available — desktop dashboards, local development, kiosk setups.
- User-friendly: no number to type, no typos, just point and scan.
- Default behavior in both Baileys and whatsapp-web.js.
- Drawback for servers: you must render the QR somewhere a human can see it, then transmit it safely.
How pairing code login works
The pairing code flow inverts the direction. Instead of the phone reading a code off a screen, the companion device requests a short alphanumeric code tied to a specific phone number, and the user types that code into the phone. On the phone you go to Linked Devices, then Link a Device, then Link with phone number, and enter the 8-character code the library printed.
Because nothing has to be displayed as a scannable image, this method is ideal for environments with no GUI: a server reached only over SSH, a Docker container, a CI job, or a CLI utility. You log the code to stdout (or push it to an admin via your own channel) and the operator types it on their phone. The trade-off is that you must know the target phone number up front, in the correct format, before requesting the code. Like the QR, the pairing code is time-limited and regenerates on an interval — in practice the libraries and the WhatsApp clients observe an interval of around three minutes (see the note below).
QR vs pairing code at a glance
| Aspect | QR code | Pairing code |
|---|---|---|
| Where the code lives | Displayed by the library, read by the phone camera | Requested by the library, typed into the phone |
| Needs a screen/camera | Yes — must render a scannable image | No — plain 8-character text is enough |
| Needs phone number up front | No | Yes, in E.164 format |
| Headless / SSH / Docker | Awkward (must surface the image) | Ideal — log the code to stdout |
| Refresh behavior | Rotates frequently (~20s) | Regenerates on a longer interval (~3 min observed default) |
| Resulting session | Companion device, full E2E keys | Companion device, full E2E keys (identical) |
| Default in both libraries | Yes | Opt-in |
Linking with Baileys (QR and pairing code)
Baileys is a TypeScript library that speaks WhatsApp's WebSocket protocol directly — no browser, no Puppeteer, no Selenium. That makes it lightweight and a natural fit for servers, which is exactly where the pairing code shines. It is maintained by the WhiskeySockets organization (originally authored by Adhiraj Singh), licensed MIT, and the recommended install is the unscoped package.
npm install baileys @hapi/boom qrcodeFor QR login, do not rely on the old printQRInTerminal option — it is deprecated and is a no-op in recent Baileys releases, so it no longer prints anything. The recommended pattern is to read the qr field from the connection.update event and render it yourself:
import makeWASocket, { useMultiFileAuthState } from 'baileys'
import QRCode from 'qrcode'
const { state, saveCreds } = await useMultiFileAuthState('auth')
const sock = makeWASocket({ auth: state })
sock.ev.on('creds.update', saveCreds)
sock.ev.on('connection.update', async ({ connection, qr }) => {
if (qr) {
// Render the latest rotating QR yourself
console.log(await QRCode.toString(qr, { type: 'terminal', small: true }))
}
if (connection === 'open') console.log('Linked!')
})For the pairing code, request it from inside the connection.update handler — only once the socket is actually connecting and a qr value has been surfaced. Calling sock.requestPairingCode() synchronously right after makeWASocket() (before the socket has begun connecting) can throw or return an invalid code, so wire it into the event instead. Guard it so you only request once per session and only when the account is not yet registered. The phone number must be in E.164 format with no plus sign — for example +1 (234) 567-8901 becomes 12345678901.
import makeWASocket, { useMultiFileAuthState } from 'baileys'
const { state, saveCreds } = await useMultiFileAuthState('auth')
const sock = makeWASocket({ auth: state })
sock.ev.on('creds.update', saveCreds)
const phoneNumber = '12345678901' // E.164, no '+'
let pairingRequested = false
sock.ev.on('connection.update', async ({ connection, qr }) => {
// Request the code only once the socket is connecting (qr present),
// and only if this account has not been linked yet.
if (qr && !pairingRequested && !sock.authState.creds.registered) {
pairingRequested = true
const code = await sock.requestPairingCode(phoneNumber)
console.log('Enter this on your phone:', code)
}
if (connection === 'open') console.log('Linked!')
})Linking with whatsapp-web.js
whatsapp-web.js takes a different architectural route: it drives the real WhatsApp Web app inside a headless Chromium instance via Puppeteer. That is heavier than Baileys' pure WebSocket approach, but it tracks the official web client closely. It is authored by Pedro S. Lopez, licensed Apache-2.0, and is not affiliated with or endorsed by WhatsApp or Meta.
Important: pick one method per client. Per PR #3180, when you set pairWithPhoneNumber the client switches into pairing mode and skips the QR flow entirely — so a qr listener will never fire in that configuration. Don't wire up both on the same client; choose QR or pairing code and use the matching snippet below.
QR is the default. Omit pairWithPhoneNumber, listen for the qr event, and render the string:
const { Client, LocalAuth } = require('whatsapp-web.js')
const qrcode = require('qrcode-terminal')
// QR flow: no pairWithPhoneNumber set
const client = new Client({
authStrategy: new LocalAuth(),
})
client.on('qr', (qr) => qrcode.generate(qr, { small: true }))
client.on('ready', () => console.log('Client is ready!'))
client.initialize()For the pairing code, set the pairWithPhoneNumber option (this disables QR) and listen for the code event instead. Pairing-code support was merged in August 2025 (PR #3180); confirm it is present in your installed version, since it landed on the project's main line before being widely tagged.
const { Client, LocalAuth } = require('whatsapp-web.js')
// Pairing-code flow: setting pairWithPhoneNumber turns OFF the QR flow,
// so do NOT also register a 'qr' listener here — it would never fire.
const client = new Client({
authStrategy: new LocalAuth(),
pairWithPhoneNumber: {
phoneNumber: '12345678901', // country code + number, no symbols
showNotification: true,
// intervalMs: 180000 // optional; defaults to WhatsApp's ~3-min interval
},
})
client.on('code', (code) => console.log('Linking code:', code))
client.on('ready', () => console.log('Client is ready!'))
client.initialize()Persist the session with an authStrategy or you will re-authenticate on every restart. LocalAuth stores credentials on the local filesystem and is fine for a single long-lived process; RemoteAuth keeps them in a database or remote store, which is what you want for containers, autoscaling, or ephemeral disks.
Which method should you use?
Pick based on where the linking actually happens, not on a vague sense of which is 'more secure' — they are equivalent on that front.
- Use QR when a human is sitting in front of a screen with a camera: local dev, a desktop admin panel, an onboarding wizard with a visible code.
- Use the pairing code for headless and remote deployments: SSH-only boxes, Docker, Kubernetes, CI pipelines, CLI tools, and dashboards where surfacing a scannable image is clumsy or insecure.
- If you are building a self-serve product where users link their own number, the pairing code can be friendlier — display a code and clear instructions instead of asking non-technical users to find and scan a QR.
- If you already capture the user's phone number, the pairing code removes a moving part (no rotating image to keep fresh in the UI).
Remember the shared constraints that apply no matter which method you choose: up to four linked companion devices per phone, and a linked device logs out if the primary phone goes unused for about 14 days. Build re-linking into your operations rather than assuming a session lasts forever.
Security and operational risk
Both flows produce the same E2E-encrypted companion session, so the meaningful differences are about how the linking secret can leak. A QR code is a visual secret: if it appears in a shared screenshot, a screen recording, or a screen-share, anyone who scans it in time can link a device to that account. A pairing code is a typed secret: the relevant attack is social engineering, where someone is tricked into entering an attacker-supplied code on their own phone, linking the attacker's device. Treat both the QR payload and the pairing code as live credentials — short-lived, but powerful while valid.
- Never log a QR payload or pairing code anywhere a third party could read it after the fact.
- Educate users that they should only ever enter a code they generated themselves — never one sent to them.
- Audit Linked Devices periodically and revoke sessions you do not recognize.
- Store persisted auth state encrypted, since it is the long-term equivalent of the linking secret.
Finally, weigh the platform risk honestly. Baileys' own README states its maintainers do not condone use that violates WhatsApp's Terms of Service and disclaim liability; whatsapp-web.js carries a similar non-affiliation notice. If you are sending business or transactional messages at scale, the official WhatsApp Business Cloud API is the path that will not put your number at ban risk.
Keep the linked session healthy
Whichever method you used to link, the real work starts once the session is live: keeping it connected. A companion-device session is not fire-and-forget. It can drop for reasons that have nothing to do with your code — a server-side logout, the 14-day inactivity expiry on the primary phone, a conflicting re-link, or a transient network blip — and a silent dead session looks identical to a healthy one until messages start failing.
- Watch the connection lifecycle. In Baileys, handle connection.update and inspect the DisconnectReason on close so you can distinguish a recoverable drop from a real loggedOut state (which needs a fresh link, not a reconnect). In whatsapp-web.js, listen for disconnected and authentication_failure.
- Implement bounded reconnect with backoff. Auto-reconnect on transient drops, but stop and alert an operator when the disconnect reason is a logout — blindly retrying a logged-out session just spins.
- Add a liveness signal. A periodic lightweight check (for example confirming the socket is open, or that a self-presence/heartbeat succeeds) lets monitoring catch a stale session before your users do.
- Alert on relink-required. When a session truly needs re-linking, surface it loudly to whoever can scan the QR or type the next pairing code — don't let it fail quietly.
- Persist and back up auth state. With credentials in a database (not useMultiFileAuthState/LocalAuth on ephemeral disk), a restart resumes the existing session instead of forcing a new link.
Treating session health as a first-class operational concern — not an afterthought — is what separates a demo that works once from a bot that stays linked in production.
Verify numbers before you link or send
Whichever login method you choose, a session is only useful against numbers that are actually on WhatsApp. Trying to message numbers that are not registered wastes session capacity and is exactly the kind of low-quality behavior that increases ban risk. Before you ever spin up a linked session for outreach, it pays to validate your list.
Our hosted WhatsApp Profile API does this without any linked session of its own: send a number and get back whether it's on WhatsApp, plus the public profile picture, display name, about text, and whether it's a business account. It runs over RapidAPI and Apify with a bulk checker, so you can clean a list before it ever touches your Baileys or whatsapp-web.js bot — keeping your linked number cleaner and your sends more deliberate.
Frequently asked questions
Clean your list with the WhatsApp Profile API — check if a number is on WhatsApp and pull its public profile, no linked session required. RapidAPI, Apify, and bulk checking included.
Explore the APINeed cheap numbers to create, verify or warm up WhatsApp accounts for testing? GrizzlySMS rents virtual numbers for WhatsApp verification from under $1.
Get a WhatsApp numberSources & further reading
- Baileys repository (WhiskeySockets, MIT)
- Baileys docs — Connecting (QR and requestPairingCode)
- whatsapp-web.js repository (pedroslopez, Apache-2.0)
- whatsapp-web.js PR #3180 — pairWithPhoneNumber
- whatsapp-web.js authentication guide (LocalAuth/RemoteAuth)
- WhatsApp — About linked devices
- WhatsApp — How to link a device