How to link a number in Node.js with the Wapito WhatsApp API
A script, a token and an HTTP client is the whole stack. Everything below runs from a file you can execute today, and the same code moves to a worker or a container without changing shape.
In Node.js the connect flow is an event-driven state machine: fetch the channel state, request a pairing code while someone is on the phone, stream the QR through a websocket only if you must, and let the Express receiver's channel events drive the UI. A short polling loop with a timer promise covers the gap until the first event, and an AbortController ends it when the browser tab closes.
Before you start
npm install express (fetch is built in from Node 18)process.env.WAPITO_TOKEN- A channel with a WhatsApp number linked to it, and its API token. Create one in the dashboard — the authentication guide shows where the token goes.
How it works
Check the channel state
Read the state before you ask for anything. A code can only be issued while the session is waiting to be linked, so polling for one against a connected channel just produces errors.
const url = 'https://api.wapito.com/v1/channel'; const options = {method: 'GET', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN'}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); }In Node.js read the state with a GET on /channel and branch on data.status before requesting anything; a code is only issued while the session is waiting to be linked. Poll in a for loop with await setTimeout(2000) from node:timers/promises and a fixed iteration count, so an abandoned attempt ends by itself, and pass the loop's AbortSignal into the timer.
API reference for this stepRequest a pairing code
Ask for a code for the number you intend to link, and have the person type it into the linked-devices screen on that phone. Codes expire quickly, so request one while they are already looking at the phone.
const url = 'https://api.wapito.com/v1/channel/pairing-code'; const options = { method: 'POST', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'}, body: '{"phone":"+15557654321"}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); }In Node.js request a code with fetch(url, { method: 'POST', headers, body: JSON.stringify({ phone }) }) and show data.code to the person at once, since data.expires_in is short; check response.status for the 409 channel_not_in_qr_state that fetch resolves normally, and re-read the state when you see it. A hyphen in the middle makes the code easier to type.
API reference for this stepFall back to a QR if you need to
The QR refreshes every few seconds, so stream it to the browser rather than emailing a screenshot. New-device QR linking has been unreliable across engines since mid-2026, which is why the code is the primary path.
const url = 'https://api.wapito.com/v1/channel/qr'; const options = {method: 'GET', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN'}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); }In Node.js fetch the QR with a GET on /channel/qr on a short interval and push data.data to the browser over a websocket or server-sent events; the code refreshes every few seconds, so a screenshot is stale before it is opened. Stop the interval on the first connected event, and render the string client-side with a QR library.
API reference for this stepWatch the connection over the webhook
Every state change arrives as an event carrying the new state, the previous one and a reason. A ban needs a human and a restart does not, so branch on the reason rather than treating every disconnect the same.
Arrives on your webhook as
channel.In Node.js the Express handler receives channel events carrying the new state, the previous one and a reason; switch on payload.data.reason, because a ban needs a human and a restart does not, and push the transition to the UI through the same socket the QR used. res.sendStatus(200) first, then the work, so a slow UI push never causes a redelivery.
The whole script
Every step above in one runnable file. Save it as connect-number-qr-pairing.mjs, put your token in the environment, and run it.
// Connect a Number with the Wapito WhatsApp API.
//
// Read the state, request a pairing code, use the QR only as a fallback, and drive everything else from the channel webhook.
//
// Run it with:
// export WAPITO_TOKEN="wpt_..."
// node connect-number-qr-pairing.mjs
const BASE_URL = 'https://api.wapito.com/v1';
const TOKEN = process.env.WAPITO_TOKEN;
// --- Check the channel state ---
{
const url = BASE_URL + '/channel';
const options = {method: 'GET', headers: {Authorization: 'Bearer ' + TOKEN}};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
// --- Request a pairing code ---
{
const url = BASE_URL + '/channel/pairing-code';
const options = {
method: 'POST',
headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
body: '{"phone":"+15557654321"}'
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
// --- Fall back to a QR if you need to ---
{
const url = BASE_URL + '/channel/qr';
const options = {method: 'GET', headers: {Authorization: 'Bearer ' + TOKEN}};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
Receive the webhook
Express 5 receiver for channel — it verifies the signature, answers immediately and does the work after. Install it with npm install express and save it as webhook.mjs.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.WAPITO_WEBHOOK_SECRET;
const EVENTS = new Set(['channel']);
/** Checks the X-Wapito-Signature header: t=<ms>,v1=<hex hmac-sha256>. */
function verify(raw, header) {
if (!header) return false;
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (!parts.t || !parts.v1) return false;
if (Math.abs(Date.now() - Number(parts.t)) > 300_000) return false; // 5 minute clock skew
const expected = crypto.createHmac('sha256', SECRET).update(`${parts.t}.`).update(raw).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/wapito', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, req.get('X-Wapito-Signature'))) return res.sendStatus(401);
const payload = JSON.parse(req.body.toString('utf8'));
if (EVENTS.has(payload.event)) console.log(payload.event, payload.data);
res.sendStatus(200); // answer 2xx fast; do the real work in a queue
});
app.listen(Number(process.env.PORT ?? 3000));
Errors you may hit
Gotchas
- fetch never rejects on a 4xx or 5xx. Check response.ok yourself; an await fetch(...) that "worked" can still be a 402 plan_required with a JSON error body.
- Do not let express.json() run before your webhook route. Signature verification needs the exact raw bytes, so mount express.raw({ type: "application/json" }) on the webhook path and parse the JSON yourself afterwards.
- Node 18+ has global fetch, but node-fetch v2, undici and axios all disagree about how they surface non-2xx responses. Pick one client per project or your error handling will be inconsistent.
- Promise.all over a list of recipients defeats the send queue: the API will serialise them anyway and return 429 send_rate_limited for the overflow. Send sequentially with await, or use a queue with concurrency 1 per channel.
- Large numbers in JSON.parse lose precision. Treat every WhatsApp id, group id and timestamp that arrives as a string as a string - never Number() it.
Where to run it
- Google Cloud Run (container, min instances 0, one process per channel worker)
- Railway or Render web service for the Express receiver
- Vercel or Netlify Functions for the webhook receiver only - they cannot hold the sender queue open
- Fly.io machine with a persistent volume if you queue sends locally
Pitfalls in Node.js
- setInterval for the QR refresh keeps running after the channel connects unless clearInterval is called from the event handler, and the process keeps polling forever; keep the interval handle in a Map keyed by channel id so the handler can find it.
- A poll loop with while (true) and no cap holds the process open when nobody types the code; use a for loop with a fixed number of attempts and a clear exit message, and log the last state you saw so the operator knows why it gave up.
- Serving the pairing page from the same Express app that receives webhooks means express.json() mounted globally for the page's API will consume the raw body your signature check needs; mount the webhook route with express.raw before any body parser, or put the page on its own router.
Frequently asked questions
Should I use a pairing code or a QR?
A pairing code, in almost every case. It can be read aloud, pasted into a chat or typed from a support ticket, and it does not depend on a camera pointed at a refreshing image. QR linking of new devices has also been unreliable across engine libraries since mid-2026, so treat it as the fallback.
Can I keep using WhatsApp on the phone afterwards?
Yes - that is the central difference from the official platform. Wapito links a companion device, exactly like WhatsApp Web, so the human keeps their app, their chats and their history while your automation works alongside them on the same number.
What happens if the session drops?
The channel webhook fires with the new state, the previous state and a reason. A transient engine restart reconnects by itself; a logout by the phone's owner or a ban does not, and both need a person. Branch on the reason rather than retrying blindly into a session that is gone.
Related
Try it on your own number
Create a channel, link a WhatsApp number by QR or pairing code, and call the API in a couple of minutes. The Sandbox plan is free and needs no card.