• Home
  • Blog
  • WhatsApp QR Code vs Pairing Code Login (Explained)
Guides
9 min read May 14, 2026

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.

Key takeaways
  • 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.

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

AspectQR codePairing code
Where the code livesDisplayed by the library, read by the phone cameraRequested by the library, typed into the phone
Needs a screen/cameraYes — must render a scannable imageNo — plain 8-character text is enough
Needs phone number up frontNoYes, in E.164 format
Headless / SSH / DockerAwkward (must surface the image)Ideal — log the code to stdout
Refresh behaviorRotates frequently (~20s)Regenerates on a longer interval (~3 min observed default)
Resulting sessionCompanion device, full E2E keysCompanion device, full E2E keys (identical)
Default in both librariesYesOpt-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.

BASH
npm install baileys @hapi/boom qrcode

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

JAVASCRIPT
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.

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

JAVASCRIPT
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.

JAVASCRIPT
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

Validate numbers before you link a session

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 API
Sponsored

Need 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 number

Sources & further reading

Related

More from the blog

Written by
Eduardo Airaudo

Developer and founder of the WhatsApp Profile API. Building WhatsApp tooling and APIs since 2022.

What Our Users Say

Real reviews from our satisfied customers

4.5/5 (170 reviews)