How to check 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.
From Node.js the number check is a batched loop with libphonenumber-js in front and a database behind: normalise, post a chunk with fetch, write each result with today's date, read the usage endpoint, repeat. It runs as a one-off script or a queued job, sequentially, and never in a Promise.all. A top-level await in an ES module is all the scaffolding it needs.
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
Normalise the numbers first
Convert to international format with a real phone-number library before you check anything. A number with a national trunk prefix left on is a different number, and checking it wastes an allowance and teaches you nothing.
const url = 'https://api.wapito.com/v1/contacts/+15551234567/exists'; 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 a single lookup is a GET on /contacts/{id}/exists with the E.164 number from parsePhoneNumber(raw, region).number in the path; encodeURIComponent keeps the plus sign intact. The parsed object carries exists as a boolean and the jid the number maps to. Reserve it for an interactive lookup and use the batch call for anything that comes from a spreadsheet.
API reference for this stepCheck a batch
Send a modest batch rather than one request per number. The response tells you, per number, whether an account exists and what identity it maps to, which is the part worth storing.
const url = 'https://api.wapito.com/v1/contacts/check'; const options = { method: 'POST', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'}, body: '{"phones":["+15551234567","+15559876543","+15550000000"]}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); }In Node.js post a chunk with body: JSON.stringify({ phones: chunk }) and iterate data.results, which carries one entry per number with the phone, the exists flag and the jid; write each row with the date before moving to the next chunk. Check response.ok, because a 429 for quota is a resolved promise, and compare data.checked with chunk.length for dropped rows.
API reference for this stepStore the result and watch the allowance
Write each answer back to your own database with the date you checked, then read the usage endpoint before the next batch. Re-checking numbers you already know about is the easiest way to burn an allowance and attract attention at the same time.
const url = 'https://api.wapito.com/v1/usage?from=2026-09-01&to=2026-09-15'; 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 /usage before each chunk and compute the headroom as data.limits.number_checks_per_day minus data.totals.number_checks; when it is smaller than chunk.length, stop and log rather than posting a batch that will come back 429 quota_exceeded. The counters are per day, so the scheduler's next run can pick up the remaining rows from your own table.
API reference for this step
The whole script
Every step above in one runnable file. Save it as check-number.mjs, put your token in the environment, and run it.
// Check Number with the Wapito WhatsApp API.
//
// Normalise to international format, check in modest batches, and store every result with its date so you never check the same number twice.
//
// Run it with:
// export WAPITO_TOKEN="wpt_..."
// node check-number.mjs
const BASE_URL = 'https://api.wapito.com/v1';
const TOKEN = process.env.WAPITO_TOKEN;
// --- Normalise the numbers first ---
{
const url = BASE_URL + '/contacts/+15551234567/exists';
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);
}
}
// --- Check a batch ---
{
const url = BASE_URL + '/contacts/check';
const options = {
method: 'POST',
headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
body: '{"phones":["+15551234567","+15559876543","+15550000000"]}'
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
// --- Store the result and watch the allowance ---
{
const url = BASE_URL + '/usage?from=2026-09-01&to=2026-09-15';
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
- Promise.all over the chunks posts them at once and the API answers most of them with 429; loop with await, one chunk at a time, and read usage in between. A p-limit wrapper with concurrency one is the same thing with more dependencies.
- parsePhoneNumber throws on an unparseable string; a map over the list without a try block ends the run on the first bad row, so wrap each parse and collect the failures, or use the isValidPhoneNumber helper as a filter first.
- Reading the spreadsheet with a CSV parser that has dynamic typing enabled converts the number column to a float, and a float with sixteen digits comes back with a trailing zero or an exponent; disable casting for that column and keep it a string from the file to the request body.
Frequently asked questions
How many numbers can I check per day?
Your plan sets an explicit daily allowance, and the API tells you how much is left through the usage endpoint. The harder limit is behavioural: even inside your allowance, checking a large list in a short burst is the pattern that draws attention, so spread it out.
Why is this riskier than sending a message?
Because a check involves no relationship at all. Sending a message to someone who wrote to you is normal behaviour; asking the network about thousands of numbers you have never contacted is what a scraper does. The abuse systems weight that difference heavily, and so does Wapito's metering.
Does a check tell me the person's name?
No. It tells you whether an account exists and gives you the identity you would address, nothing more. Profile details are a separate call with their own privacy rules, and a contact who has restricted their profile will not reveal a name or a photo to a stranger's number.
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.