How to follow a channel in Node.js with the Wapito WhatsApp API

Search or resolve a link, verify it is the right channel, page back through the history, then handle new posts from the 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 following a Channel and bridging its posts is a read-heavy script: a search or a link resolution, a paged history read through an async generator, and an Express route that forwards new posts by sender id. Idempotency lives in a persisted Set of processed message ids, because a restart otherwise replays the archive into whatever you bridge to.

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. Search the directory

    Search returns public channels matching a term. Treat the results as candidates rather than as matches: names are not unique and a lookalike channel is a real risk when you are following on a customer's behalf.

    const url = 'https://api.wapito.com/v1/newsletters/find?q=release%20notes&country=GB&count=50';
    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 search with a GET whose URL is built with new URL and searchParams.set('q', term) so the term is encoded; the parsed array is a list of candidates with names that are not unique. Choose with an explicit filter on the fields you trust, such as follower count or a known id, never with results[0].

    API reference for this step
  2. Resolve an invite link

    If you already have a channel link, resolve its code to get the name, description and follower count before doing anything else. This is how you confirm you have the official channel and not an imitation.

    const url = 'https://api.wapito.com/v1/newsletters/invite/0029VaAbCdEfGhIjKlMnOp';
    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 resolve a link with a GET on /newsletters/invite/ plus encodeURIComponent(code) and compare data.name and data.followers against what the customer gave you before following. A mismatch here is the lookalike case, and refusing at this step is far cheaper than unfollowing later.

    API reference for this step
  3. Read what has been published

    Page back through the channel's posts to seed your own archive or to catch up after an outage. Store the message ids so a re-run does not process the same post twice.

    const url = 'https://api.wapito.com/v1/newsletters/120363099887766554@newsletter/messages?count=50&offset=0';
    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 page the history with an async function* that yields each message and follows the cursor; persist the processed ids with fs.promises.writeFile after each page so a crash resumes rather than restarting. Keep the newest id and page forward from it on the next run.

    API reference for this step
  4. React to new posts

    New posts arrive as message events from the channel id. Route them by that id so a bridge to another system knows which feed a post belongs to.

    Arrives on your webhook as messages.

    In Node.js the Express handler receives messages events whose sender is the newsletter id; a Map from id to destination decides where the post is forwarded. Send res.sendStatus(200) first and forward from a queue, because the destination system's latency must never become a webhook retry.

The whole script

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

// Follow Channels with the Wapito WhatsApp API.
//
// Search or resolve a link, verify it is the right channel, page back through the history, then handle new posts from the webhook.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   node follow-channels.mjs

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

// --- Search the directory ---
{
  const url = BASE_URL + '/newsletters/find?q=release%20notes&country=GB&count=50';
  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);
  }
}

// --- Resolve an invite link ---
{
  const url = BASE_URL + '/newsletters/invite/0029VaAbCdEfGhIjKlMnOp';
  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);
  }
}

// --- Read what has been published ---
{
  const url = BASE_URL + '/newsletters/120363099887766554@newsletter/messages?count=50&offset=0';
  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 messages — 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(['messages']);

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

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 Set held in memory and never written to disk is empty after a redeploy, and the bridge reposts every message it can page; write the ids to a file or a database after each batch.
  • Concatenating the search term into the URL string sends spaces and accents unencoded and the request fails or searches for the wrong thing; use URL and searchParams.
  • Paging a long history with Promise.all over cursors is impossible anyway, since each cursor comes from the previous page; the loop is inherently sequential, so write it as one.

Frequently asked questions

Can I follow a private channel?

Only if you have its invite link, which is how private channels are shared in the first place. There is no way to discover a private channel through search, and resolving a code you were not given will simply fail. Treat a channel link like any other credential.

Do I get every post as a webhook?

New posts from channels the linked number follows arrive as message events keyed to the channel id, so yes for anything published after you follow. History is a separate problem: page back through the messages endpoint once, then rely on the webhook for everything after that.

Is it legal to republish what I read?

That is a copyright and terms question rather than an API one, and the answer depends on the publisher and your jurisdiction. Reading a public channel to trigger your own workflow is uncontroversial; republishing someone else's posts wholesale is a decision to take with your own legal advice.

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.