How to approve join requests 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.
Handling join requests in Node.js means an async loop over a paged list, with one fetch per page and one fetch per decision. An async generator is the natural shape for the queue, and the approve and reject calls are plain POST and DELETE requests without bodies; everything else is looking the identity up in your own database before you decide.
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
Turn on approval for the group
Join approval is a group setting. Switch it on before you publish the invite link anywhere public, otherwise the first hour of a campaign fills the group with accounts nobody has looked at.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/settings'; const options = { method: 'PATCH', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'}, body: '{"messages_admin_only":false,"membership_approval":true}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); }In Node.js switch approval on with a PATCH whose body is JSON.stringify of the single setting; do it in the same script that publishes the link, before the link is posted. Check response.ok, because a 403 arrives as a resolved promise and the group would stay open while your log says it was locked.
API reference for this stepRead the pending queue
The queue lists everyone waiting, with the identity that will become the participant. Page through it rather than assuming it is short; a link that was shared widely can produce hundreds of requests overnight.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/applications'; 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 write an async function* that fetches a page, yields each application, and loops while the response carries a next cursor; the approval code then becomes for await (const request of pending(groupId)). This keeps the paging in one function and means nothing else ever sees a page boundary.
API reference for this stepApprove the ones you recognise
Match each request against your own list - paid subscribers, enrolled students, staff numbers - and approve only those. Approving everything defeats the point of turning the setting on.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/applications/+15551234567/approve'; const options = {method: 'POST', 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 approve with fetch(url, { method: 'POST', headers }) on /applications/{pid}/approve, no body needed. Look the identity up in a Set you loaded from your database at the start of the run, so the decision is O(1) and the run does not make a query per pending person.
API reference for this stepReject the rest and record why
Rejection is quiet: the person is not told why. Keep your own log so a mistaken rejection can be explained when the person asks, because WhatsApp gives them no way to appeal.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/applications/+15551234567'; const options = {method: 'DELETE', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN'}}; try { const response = await fetch(url, options); console.log(response.status); } catch (error) { console.error(error); }In Node.js reject with method: 'DELETE' on /applications/{pid}, and write your own record of the rejection with the reason before awaiting the call, because the person is never told why. A 404 here means they withdrew the request in the meantime and can be logged and skipped.
API reference for this step
The whole script
Every step above in one runnable file. Save it as group-join-requests.mjs, put your token in the environment, and run it.
// Group Join Requests with the Wapito WhatsApp API.
//
// Turn approval on, read the pending queue, approve the people you can identify, reject the rest and keep your own audit log.
//
// Run it with:
// export WAPITO_TOKEN="wpt_..."
// node group-join-requests.mjs
const BASE_URL = 'https://api.wapito.com/v1';
const TOKEN = process.env.WAPITO_TOKEN;
// --- Turn on approval for the group ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/settings';
const options = {
method: 'PATCH',
headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
body: '{"messages_admin_only":false,"membership_approval":true}'
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
// --- Read the pending queue ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/applications';
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);
}
}
// --- Approve the ones you recognise ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/applications/+15551234567/approve';
const options = {method: 'POST', headers: {Authorization: 'Bearer ' + TOKEN}};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
// --- Reject the rest and record why ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/applications/+15551234567';
const options = {method: 'DELETE', headers: {Authorization: 'Bearer ' + TOKEN}};
try {
const response = await fetch(url, options);
console.log(response.status);
} catch (error) {
console.error(error);
}
}
Receive the webhook
Express 5 receiver for groups.participants — 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(['groups.participants']);
/** 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
- Approving inside Promise.all over a page fires every POST at once; the API paces them and returns 429 for the rest, so half the page stays pending and the next run sees them again. Use a for await loop.
- A pending request's identity may be a linked identity with no phone number; a lookup keyed on phone silently rejects every such person. Store both identity forms in your allow-list.
- Cursor strings should be passed back verbatim; running them through encodeURIComponent twice, once by you and once by URLSearchParams, produces a cursor the API cannot decode and an empty second page.
Frequently asked questions
How long do pending requests stay in the queue?
WhatsApp expires them after a period rather than keeping them indefinitely, so a queue that is only drained weekly will lose requests. Drain it on a schedule measured in minutes or hours, and tell people roughly how long approval takes on the page where you publish the link.
Does the person know they were rejected?
They see that they are not in the group, but they are not given a reason and there is no appeal inside WhatsApp. If rejection is part of a business process - an expired subscription, for example - tell them through the channel you already have with them rather than leaving them guessing.
Can I approve everyone automatically?
You can, but then the setting is doing nothing except adding delay. Approval is worth turning on only when you have something to check against: a subscriber list, an accreditation list, a CRM segment. Otherwise leave it off and let people join directly by link.
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.