How to change group settings in Node.js with the Wapito WhatsApp API

Read the settings, restrict posting when you are broadcasting, reopen on schedule, and keep an audit trail from the group 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.

In Node.js the settings flip is a tiny script that a scheduler runs twice a day, once to restrict and once to reopen. The read, the PATCH and the audit hook are all fetch calls with await, and the one thing to get right is the PATCH body: only the field you are changing, as a JSON string, with response.ok checked afterwards.

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 settings

    The group object carries who may send messages, who may edit the subject and icon, and whether new members need approval. Read before you write so a scheduled job does not flip a setting an admin changed deliberately an hour ago.

    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 read the group with a GET and look at the settings object on the parsed JSON before deciding anything; if the announcement flag already has the value you want, skip the write entirely. Comparing first keeps a rerun of the job idempotent and spares the group a duplicate system message.

    API reference for this step
  2. Restrict posting to admins

    Announcement mode is a single field. It is the right default for any group you use to broadcast, because it removes the whole class of accidental replies to hundreds of people.

    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 restrict posting with fetch(url, { method: 'PATCH', headers, body: JSON.stringify({ ...one field }) }); PATCH needs the Content-Type header exactly as POST does, and the generated options object includes it. Read response.status: a 403 means your number lost its admin role and no amount of retrying will help.

    API reference for this step
  3. Open it again on a schedule

    Flip the same field back when a discussion window opens. Running this from a scheduler is how a large group can hold a question hour without a moderator sitting on the mute button.

    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 reopening is the same PATCH with the opposite boolean, so export one function flip(groupId, announce) and let the scheduler import it twice. Build the window with the Intl.DateTimeFormat timeZone option or a date library that understands zones, not with Date alone, which reasons in the server's local time.

    API reference for this step
  4. Record the change from the webhook

    Settings changes arrive as group events, including ones made by other admins on their phones, so your audit trail reflects what actually happened rather than what your job intended.

    Arrives on your webhook as groups.

    In Node.js the Express handler gets a groups event whose payload.data carries the new settings and who changed them; append it to an audit log and res.sendStatus(200). Diffing the event against the last value your job wrote is how you learn an admin reverted the schedule by hand.

The whole script

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

// Group Settings with the Wapito WhatsApp API.
//
// Read the settings, restrict posting when you are broadcasting, reopen on schedule, and keep an audit trail from the group webhook.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   node group-settings.mjs

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

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

// --- Restrict posting to admins ---
{
  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);
  }
}

// --- Open it again on a schedule ---
{
  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);
  }
}

Receive the webhook

Express 5 receiver for groups — 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']);

/** 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

  • Two scheduler instances on a platform that runs replicas will both PATCH the group at the same minute, and the group sees two announcements; make the job read the current value first and skip when it already matches.
  • Without a Content-Type header a PATCH body is sent as text/plain and the API replies 400 invalid_request; fetch does not add the header for a string body the way it does for a FormData one.

Frequently asked questions

What settings can I change from the API?

The ones an admin sees on the phone: who may send messages, who may edit the group's subject, icon and description, and whether people joining by link need approval first. Read the group object to see the current values, because other admins can change them at any time.

Does announcement mode stop replies entirely?

It stops non-admins from posting in the group, which is what makes it suitable for broadcasts. People can still react to messages, and they can still message the linked number privately, so plan a path for the replies you do want rather than assuming nobody will try.

Can I make a group members-only invisible to search?

Groups are not searchable on WhatsApp in the first place - they are reachable only through an invite link or a direct add. The nearest control is to revoke the link and require approval, which together mean nobody joins without an admin acting.

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.