How to create a group in Node.js with the Wapito WhatsApp API

Create the group, read it back by id, publish the invite link, and let the participants webhook keep your database in step.

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 whole flow is a single .mjs file with top-level await: three fetch calls run one after the other, and nothing else is needed beyond Node 18. The part that catches people is that fetch treats a 400 as a resolved promise, so the create step can look like it worked while the body says invalid_recipient.

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. Create the group

    Post a subject and the founding participants. The linked number becomes the creator and superadmin, so every later change - settings, admins, icon - is allowed without any extra step. Keep the founding list to people who already expect the group; adding strangers here is the fastest route to a report.

    const url = 'https://api.wapito.com/v1/groups';
    const options = {
      method: 'POST',
      headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'},
      body: '{"subject":"Acme Launch Team","participants":["+15551234567","+15559876543"],"description":"Coordination for the Q3 launch. Keep it on topic."}'
    };
    
    try {
      const response = await fetch(url, options);
      const data = await response.json();
      console.log(data);
    } catch (error) {
      console.error(error);
    }

    In Node.js the create is fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) }) inside a try block that only ever catches network failures. Check response.ok before reading, then take data.id from await response.json(); a rejected participant comes back as a 400 with a JSON error body, not as an exception.

    API reference for this step
  2. Read the group back

    Fetch the group by the id you just received and store that id as a string. It is longer than a 64-bit integer, so a numeric column or an eager JSON parser will silently corrupt it and every later call will answer 404.

    const url = 'https://api.wapito.com/v1/groups/120363041234567890@g.us';
    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-back is a GET whose URL you build with a template literal around the id string; do not pass the id through Number() or JSON.parse's default number handling, or the trailing digits are gone before the request leaves. The response is an object whose participants array you can diff against the payload you sent.

    API reference for this step
  3. Share the invite link

    Read the invite code and publish the link rather than adding people directly. Joining by link is a deliberate act by the person, which is both better manners and materially safer for the number than pushing unknown participants into a group.

    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 this is one more GET on the same base path with /invite appended and no body, so the options object is just the method and the Authorization header. The invite link arrives as a property on the parsed JSON; wrap it behind a route on your own domain before you put it anywhere public.

    API reference for this step
  4. Confirm membership over the webhook

    Each join or leave arrives as its own event with the participant and the action. Key your own records on that event rather than on the create response, because people who join by link never appear in it.

    Arrives on your webhook as groups.participants.

    In Node.js the Express receiver mounts express.raw({ type: 'application/json' }) on the /wapito route so req.body is a Buffer the HMAC can be computed over, then parses it and switches on payload.event === 'groups.participants'. Push the participant into a queue and res.sendStatus(200) at once; awaiting a database write here is what causes retries.

The whole script

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

// Create Group with the Wapito WhatsApp API.
//
// Create the group, read it back by id, publish the invite link, and let the participants webhook keep your database in step.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   node create-group.mjs

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

// --- Create the group ---
{
  const url = BASE_URL + '/groups';
  const options = {
    method: 'POST',
    headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
    body: '{"subject":"Acme Launch Team","participants":["+15551234567","+15559876543"],"description":"Coordination for the Q3 launch. Keep it on topic."}'
  };

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

// --- Read the group back ---
{
  const url = BASE_URL + '/groups/120363041234567890@g.us';
  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);
  }
}

// --- Share the invite link ---
{
  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);
  }
}

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));
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 missing WAPITO_TOKEN does not throw in Node: process.env returns undefined, the header becomes 'Bearer undefined', and every call answers 401 unauthorized. Check the variable at startup and exit early with a message.
  • Making groups from a Promise.all over a list defeats the per-channel send queue; the API serialises them and hands back 429 for the overflow. Loop with await, one group at a time.
  • JSON.parse turns the numeric-looking part of a group id into a double if you ever strip the @g.us suffix and parse the rest. Keep the id as the string the API returned and use it verbatim in URLs.

Frequently asked questions

How many groups can I create in a day?

WhatsApp publishes no number, and anyone who quotes you one is guessing. What is observable is that new numbers get restricted far sooner than established ones. Start with a handful a day on a warmed-up number, watch for timelocks, and treat the first restriction as a signal to slow down rather than as a quota to probe.

Can I add people to the group as I create it?

Yes, the create call takes a participant list, but it is the riskiest way to fill a group. Many people have privacy settings that stop strangers adding them, so they will silently not appear, and those who do appear may report the number. Publishing the invite link is slower and much safer.

Does the linked number stay in the group forever?

It stays until it leaves or is removed. Because the creator holds the superadmin role, leaving a group you created hands nothing over automatically, so promote a human admin before the automation departs. Otherwise the group is left without anyone who can change its settings or membership.

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.