How to get a group invite link 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 invite-link work is four small fetch calls and the discipline sits around them: the code is a credential, so it belongs in your database and behind a redirect route rather than in a static page. A scheduled rotation is a one-file script; the resolve-then-join step is the part a bot runs on demand when someone sends it a link.
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
Read the current invite code
The code is the tail of a chat.whatsapp.com link. Treat it as a credential rather than as a URL: anyone who holds it can join, so it belongs behind your own redirect rather than pasted into a public page you cannot update.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/invite'; 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 the read is a GET whose parsed JSON exposes code and link; keep the code as a string in your store and generate the wa link at render time. Because the response is a credential, make sure a catch-all console.log(data) never runs in production for this call; log the group id and the timestamp only.
API reference for this stepRotate it on a schedule
Revoking generates a fresh code and invalidates every copy of the old link instantly. A nightly rotation means a link that leaks into a forum stops working within a day, without anyone having to notice that it leaked.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/invite'; const options = {method: 'DELETE', 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 rotate with fetch(url, { method: 'DELETE', headers }); the body of the reply holds the new code, so read it with await response.json() and write it to your table in the same async function. If the write throws, let the script exit non-zero so a monitor notices the redirect now serves a revoked code.
API reference for this stepResolve a code before joining
Given a code somebody sent you, read the group's name, size and owner before deciding. This is how a bot avoids joining a group it has no business being in.
const url = 'https://api.wapito.com/v1/groups/invite/HkQ2ZpL9vRtAeYm1'; 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 the lookup is a GET on /groups/invite/ plus encodeURIComponent(code), and the parsed object gives you the subject, participant count and owner. A bot should compare those against an allow-list before the join call; a plain if on data.owner is enough to refuse a lookalike group.
API reference for this stepAccept the invitation
Joining by code puts the linked number in the group as an ordinary member. Expect no admin rights, and expect the group's existing members to see the join as a system message.
const url = 'https://api.wapito.com/v1/groups/invite/accept'; const options = { method: 'POST', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'}, body: '{"invite_code":"https://chat.whatsapp.com/HkQ2ZpL9vRtAeYm1"}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); }In Node.js join with method: 'POST' and body: JSON.stringify({ code }); on success the linked number is a normal member with no admin rights. A revoked code produces a 404 with a JSON body, which fetch does not throw for, so check response.status and treat 404 as a clean outcome rather than an error.
API reference for this step
The whole script
Every step above in one runnable file. Save it as group-invite-link.mjs, put your token in the environment, and run it.
// Group Invite Link with the Wapito WhatsApp API.
//
// Read the code, rotate it on a schedule, resolve unknown codes before accepting, and join by code when you mean to.
//
// Run it with:
// export WAPITO_TOKEN="wpt_..."
// node group-invite-link.mjs
const BASE_URL = 'https://api.wapito.com/v1';
const TOKEN = process.env.WAPITO_TOKEN;
// --- Read the current invite code ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/invite';
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);
}
}
// --- Rotate it on a schedule ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/invite';
const options = {method: 'DELETE', headers: {Authorization: 'Bearer ' + TOKEN}};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
// --- Resolve a code before joining ---
{
const url = BASE_URL + '/groups/invite/HkQ2ZpL9vRtAeYm1';
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);
}
}
// --- Accept the invitation ---
{
const url = BASE_URL + '/groups/invite/accept';
const options = {
method: 'POST',
headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
body: '{"invite_code":"https://chat.whatsapp.com/HkQ2ZpL9vRtAeYm1"}'
};
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 groups, 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', '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));
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
- A setInterval that revokes the link every night keeps running after the process is told to stop unless you clear it; on a platform that restarts containers this can rotate the code twice in a row. Use a cron trigger, not an in-process timer.
- Serving the raw invite link from a template string in a static page means the next rotation breaks every visitor until the site redeploys; put the link behind a route that reads the current code from the database.
- fetch swallows nothing but also warns about nothing: a DELETE that came back 403 because the linked number lost admin rights looks like a normal resolution unless you read response.ok.
Frequently asked questions
How often should I rotate the invite link?
It depends on where it lives. A link on a private confirmation page can sit for months; a link on a public social profile is worth rotating weekly, because that is where scrapers find them. Rotating behind your own redirect costs nothing to your users, so err on the frequent side.
Can I see who joined through a particular link?
Not directly - WhatsApp reports that someone joined, not which link they used. If you need attribution, use a distinct group per source, or put your own redirect in front of each published link and correlate the click with the join event that follows it.
Does joining by link make my number an admin?
No. Anyone joining by link arrives as an ordinary member, including an automation. If the bot needs to moderate, an existing admin has to promote it after it joins, which is worth building into the onboarding rather than discovering when the first write fails.
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.