How to add group members in Node.js with the Wapito WhatsApp API

List who is actually in the group, add the missing people, remove the ones who left, and reconcile from the participants webhook.

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 this feature is an async reconciliation loop: fetch the live members, compute two diffs against your database, then await the adds and removes sequentially. The add endpoint returns a per-person array that you have to read even on a 200, and the participants webhook is what keeps the copy honest between runs.

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

  1. Read the current membership

    Always start from the real list rather than from your own copy. Members join by link, leave on their own, and are removed by other admins, so a database that has not seen a participants event in a while is usually out of date.

    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's parsed body is an array of participant objects; build a Map keyed on the identity string so the diff is a couple of filter calls. Because fetch resolves on any status, test response.ok first and read the JSON error when it is false, rather than iterating an error object as members.

    API reference for this step
  2. Add the people who are missing

    Send the numbers in a single call and read the per-participant result. Some will be added, some will be invited instead because their privacy settings forbid direct adds, and some will fail outright - the response says which is which.

    const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/participants';
    const options = {
      method: 'POST',
      headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'},
      body: '{"participants":["+15551234567","+15559876543"]}'
    };
    
    try {
      const response = await fetch(url, options);
      const data = await response.json();
      console.log(data);
    } catch (error) {
      console.error(error);
    }

    In Node.js send the batch as body: JSON.stringify({ participants }) and then walk the returned array: each element has the number and a status such as added, invited or failed. Log the invited ones separately, because from the group's point of view they are not members yet and your next run will see them missing again.

    API reference for this step
  3. Remove someone who has left the team

    Removal is immediate and the person sees that they were removed. Do it from a scheduled job that reads your own source of truth, so nobody is removed because of a transient CRM sync failure.

    const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us/participants/+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 removal is a separate fetch with method: 'DELETE' and the participant id encoded into the path with encodeURIComponent, since a linked identity can contain characters a raw template literal would leave ambiguous. Await them one at a time inside a for...of loop; a 404 for someone already gone is expected, not fatal.

    API reference for this step
  4. Reconcile from the webhook

    Every join, leave, add and remove arrives as an event carrying the action and the participant, who may be a linked identity rather than a phone number. Apply it to your own records so the two never drift apart.

    Arrives on your webhook as groups.participants.

    In Node.js the Express handler reads payload.data.action and payload.data.participant off the raw-body parse and applies it to your store; the participant may carry only a linked identity, so treat the phone field as optional. Acknowledge with res.sendStatus(200) before the write, or queue the write, so a slow database never triggers a redelivery.

The whole script

Every step above in one runnable file. Save it as group-participants.mjs, put your token in the environment, and run it.

// Group Participants with the Wapito WhatsApp API.
//
// List who is actually in the group, add the missing people, remove the ones who left, and reconcile from the participants webhook.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   node group-participants.mjs

const BASE_URL = 'https://api.wapito.com/v1';
const TOKEN = process.env.WAPITO_TOKEN;

// --- Read the current membership ---
{
  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);
  }
}

// --- Add the people who are missing ---
{
  const url = BASE_URL + '/groups/120363041234567890@g.us/participants';
  const options = {
    method: 'POST',
    headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
    body: '{"participants":["+15551234567","+15559876543"]}'
  };

  try {
    const response = await fetch(url, options);
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

// --- Remove someone who has left the team ---
{
  const url = BASE_URL + '/groups/120363041234567890@g.us/participants/+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));
Every event, its payload and the retry rules

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

  • A Promise.all over the removal list fires every DELETE at once, and the API answers the overflow with 429 send_rate_limited; the ones that failed are then still in the group and your diff repeats them next run.
  • Checking response.ok on the add call tells you the batch was accepted, not that anyone joined. The per-participant statuses in the body are what to persist, and 'invited' means the person has a pending invite, not a seat.

Frequently asked questions

Why are some people invited instead of added?

WhatsApp lets everyone choose who may add them to groups. If a person has restricted that to their contacts, an add from an unknown number becomes an invitation they have to accept. The API reports this per participant, so your code can tell the difference between someone who is in the group and someone who has merely been asked.

Is there a limit to how many I can add at once?

The group itself has a member ceiling set by WhatsApp, and practical experience says that adding many people in quick succession draws attention regardless of the ceiling. Add in small batches with a pause between them, and if the group is large, publish an invite link and let people join at their own pace.

Can I re-add someone who left?

Yes, technically, but think about whether you should. Someone who left a group and is immediately put back in is very likely to report the number, and repeated re-adds of the same person are a strong abuse signal. Send them the invite link instead and let them decide.

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.