How to promote group admins 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 promoting and demoting admins is a handful of fetch calls, and the code is mostly about set arithmetic on the participant list: who should be an admin according to your rota, who is one now, and which member is the creator you may never touch. Everything runs sequentially with await in one .mjs file.
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
See who holds a role today
The participant list carries each member's role, including which one is the creator. Read it before you change anything: the creator cannot be demoted, so an automation that tries will fail on exactly that member.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/participants'; 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 GET returns an array you can turn into a Map from identity to role with a single loop; find the creator with participants.find(p => p.role === 'superadmin') and exclude that identity from every later set. Guard the parse with response.ok, because a 404 body has no participants array to iterate.
API reference for this stepPromote the people who should moderate
Promotion is a single call that can carry several participants. Promote from your own source of truth - a team list, a rota, a role in your CRM - rather than from whoever happens to be talking in the group.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/admins'; const options = { method: 'POST', headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'}, body: '{"participants":["+15551234567"]}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); }In Node.js post body: JSON.stringify({ participants: toPromote }) where toPromote is the rota minus the current admins, computed with Array.prototype.filter against the Map from the first step. The call returns the updated roles; read them back from the parsed JSON rather than assuming the whole list was accepted.
API reference for this stepDemote anyone who no longer needs it
Demotion is visible to the group, so do it as part of a clear process rather than silently. The linked number must itself be an admin, and it cannot demote the group's creator.
const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/admins/+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 each demotion is fetch with method: 'DELETE' on /admins/ plus the encoded participant id, awaited in sequence. A 403 in response.status here means your own number is no longer an admin, so break out of the loop and alert instead of continuing to collect the same failure for every remaining member.
API reference for this stepTrack role changes over the webhook
Promotions and demotions made by anyone, including other admins on their phones, arrive as events. This is how your system learns that the linked number was demoted before its next write fails.
Arrives on your webhook as
groups.participants.In Node.js the Express handler receives role changes as participants events; check payload.data.action and update the role for payload.data.participant in your store. When the participant is the linked number itself, flag the channel as demoted so the next scheduled run refuses to start rather than failing halfway.
The whole script
Every step above in one runnable file. Save it as group-admins.mjs, put your token in the environment, and run it.
// Group Admins with the Wapito WhatsApp API.
//
// Read the current roles, promote from your own source of truth, demote what is stale, and follow role changes on the webhook.
//
// Run it with:
// export WAPITO_TOKEN="wpt_..."
// node group-admins.mjs
const BASE_URL = 'https://api.wapito.com/v1';
const TOKEN = process.env.WAPITO_TOKEN;
// --- See who holds a role today ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/participants';
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);
}
}
// --- Promote the people who should moderate ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/admins';
const options = {
method: 'POST',
headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
body: '{"participants":["+15551234567"]}'
};
try {
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
// --- Demote anyone who no longer needs it ---
{
const url = BASE_URL + '/groups/120363041234567890@g.us/admins/+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
- fetch resolves a 403 like a success, so a demotion loop that only awaits the call and moves on will report every demotion done while none happened. Read response.status on each DELETE.
- Building the promote list from phone numbers while the API reports admins by linked identity makes every run promote the same people again, which the group sees as a repeated system message. Compare identities, not numbers.
- Node's global fetch has no default timeout; a hung connection during a demotion holds the loop forever. Pass signal: AbortSignal.timeout(30000) in the options object.
Frequently asked questions
Can I promote someone who is not in the group?
No. Roles apply to participants, so the person has to be a member first. Add or invite them, wait for the participants event that confirms they actually joined, and only then promote - a promotion aimed at a non-member fails rather than adding them.
What is the difference between admin and superadmin?
An admin can change the group's settings, its icon and its membership, and can promote or demote other admins. The superadmin is the creator: they have the same powers and additionally cannot be demoted or removed by anyone else, which makes the choice of creating number a long-lived decision.
Will people be notified that they were promoted?
Yes. Role changes appear as system messages in the group, visible to everyone, and the affected person sees it in their chat. Treat promotion as a public act: it is not a quiet permission change, and demoting someone without warning is usually worth a message first.
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.