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

List what exists, create the channel with an honest description, store its id, and delete only when you have archived the history.

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 a Channel is created through the /newsletters endpoints with four fetch calls, and the vocabulary is the thing to internalise: the API calls the broadcast object a newsletter, and the id that comes back is a string with its own suffix. The script lists, creates when the name is missing, reads the id back, and only ever deletes after an archive.

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. List the channels this number owns

    Start from the live list so a create job does not make a second channel with the same name. Channels are called newsletters in the protocol, which is the vocabulary the API uses.

    const url = 'https://api.wapito.com/v1/newsletters?role=owner&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 fetch the list with a GET and build a Map from name to id; a single parsed array is enough because one number owns few channels. Use Map.has with a normalised name so the create step is idempotent across runs and never makes a second channel with the same title.

    API reference for this step
  2. Create the channel

    Give it a name and a description that say plainly what will be published and how often. Followers cannot reply, so the description is the only place to set expectations before someone follows.

    const url = 'https://api.wapito.com/v1/newsletters';
    const options = {
      method: 'POST',
      headers: {Authorization: 'Bearer wpt_YOUR_TOKEN', 'Content-Type': 'application/json'},
      body: '{"name":"Acme Release Notes","description":"Every shipped change, once a week.","picture":"https://acme.example/brand/channel-cover.png"}'
    };
    
    try {
      const response = await fetch(url, options);
      const data = await response.json();
      console.log(data);
    } catch (error) {
      console.error(error);
    }

    In Node.js create with fetch(url, { method: 'POST', headers, body: JSON.stringify({ name, description }) }) and keep data.id and data.invite from the parsed reply; check response.ok, because a description that is too long comes back as a 400 that fetch will not throw for. Put the description in a constant so it is reviewed, not typed inline.

    API reference for this step
  3. Read it back and store the id

    The channel id ends in its own suffix and is the recipient you post to later. Store it as a string alongside the invite link, which is what you actually publish.

    const url = 'https://api.wapito.com/v1/newsletters/120363099887766554@newsletter';
    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 channel back with a GET on /newsletters/{id} and store the id string exactly as returned; it is the recipient for later posts. Store the invite link alongside it, since that is what goes on your website while the id stays internal.

    API reference for this step
  4. Delete a channel you no longer run

    Deletion removes the channel for its followers too, so archive the posts you care about first. A channel that is finished but worth keeping is better left in place with a final post.

    const url = 'https://api.wapito.com/v1/newsletters/120363099887766554@newsletter';
    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 delete with method: 'DELETE' only after you have paged the channel's messages into an archive; the deletion removes it for followers too. Read response.status and treat 501 engine_unsupported_feature as an expected outcome on engines that cannot delete, and surface it rather than looping.

    API reference for this step

The whole script

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

// Create Channel with the Wapito WhatsApp API.
//
// List what exists, create the channel with an honest description, store its id, and delete only when you have archived the history.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   node create-channel.mjs

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

// --- List the channels this number owns ---
{
  const url = BASE_URL + '/newsletters?role=owner&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);
  }
}

// --- Create the channel ---
{
  const url = BASE_URL + '/newsletters';
  const options = {
    method: 'POST',
    headers: {Authorization: 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
    body: '{"name":"Acme Release Notes","description":"Every shipped change, once a week.","picture":"https://acme.example/brand/channel-cover.png"}'
  };

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

// --- Read it back and store the id ---
{
  const url = BASE_URL + '/newsletters/120363099887766554@newsletter';
  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);
  }
}

// --- Delete a channel you no longer run ---
{
  const url = BASE_URL + '/newsletters/120363099887766554@newsletter';
  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 channel — 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(['channel']);

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

  • Naming the variable channelId for a newsletter id collides with the channel that means your linked number in every other Wapito call, and a later refactor will pass the wrong string to the wrong endpoint.
  • A retry wrapper around the create call after a timeout can create two channels with the same name; check the list again before any retry instead of resending blindly.
  • fetch resolves a 501 like any other response, so a delete that the engine cannot perform looks successful unless response.ok is checked, and the channel stays live while your database says it is gone.

Frequently asked questions

Why does the API call it a newsletter?

Because that is the object's name in the protocol and in every engine library. WhatsApp marketed the feature to users as Channels, but the wire format never changed. Wapito keeps the protocol name in the API so the field you see matches what the engine returns, and uses channel for your linked number.

Can I see who follows my channel?

No. Follower identities are hidden from the owner by design - you see a count, not a list. That is a real difference from a group, and it means a channel is a publishing tool rather than a contact-collection tool. Put a link in your posts if you need people to identify themselves.

How many channels can one number own?

WhatsApp does not publish a figure, and creating them in bulk is exactly the pattern that draws attention. Create the channels you will actually publish to, on a warmed-up number, and space the creations out rather than provisioning a batch in one afternoon.

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.