Dev

Send WhatsApp messages from Node.js in 5 minutes

You can send a WhatsApp message from Node.js with nothing but the built-in fetch: no WhatsApp library and no headless browser. It takes about five minutes if you already have a connected number. Disclosure: ZapZap API is unofficial and connects a normal number by QR code. That means it is outside WhatsApp's terms and numbers can be limited or banned, so read the checklist at the end before sending to real people.

Published on · 4 min read

This article is also available in: Português · Español

0. What you need (2 minutes)

1) A ZapZap account and an instance connected by scanning the QR code. New accounts get a welcome credit when the phone is verified, subject to the current campaign; the quick start shows where to click. 2) Your API key, API secret and instance ID. 3) Node 18 or newer (node -v).

Put the credentials in environment variables, never in the code. Authentication is two headers on every request, x-api-key and x-api-secret.

Shell

export ZAPZAP_INSTANCE_ID="your-instance-id"
export ZAPZAP_API_KEY="your-api-key"
export ZAPZAP_API_SECRET="your-api-secret"

1. Send a text (1 minute)

Create the file send.mjs with the code below and run it with node send.mjs. The .mjs extension enables top-level await; in CommonJS, wrap the call in an async function. Use your own number first. The number goes with country code and digits only, with no +, spaces or dashes.

Node.js: send.mjs

const BASE = "https://api.zapzapapi.com/api/v1";
const { ZAPZAP_INSTANCE_ID, ZAPZAP_API_KEY, ZAPZAP_API_SECRET } = process.env;

async function zapzap(path, body) {
  const res = await fetch(BASE + "/" + ZAPZAP_INSTANCE_ID + path, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": ZAPZAP_API_KEY,
      "x-api-secret": ZAPZAP_API_SECRET,
    },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(20_000), // never leave a call hanging
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error("ZapZap " + res.status + ": " + JSON.stringify(data));
  return data;
}

// number: country code + digits only (no +, spaces or dashes)
console.log(await zapzap("/send/text", { number: "5511999999999", text: "Hello from Node.js" }));

2. Send a PDF or an image (1 minute)

Files go through /send/media. The type field picks the kind (image, video, audio, ptt, document, sticker), file is a public URL, text is the caption and docName is the filename for documents. The file must be reachable at a public URL; if it lives on your disk, upload it to storage and use a temporary link.

Node.js

await zapzap("/send/media", {
  number: "5511999999999",
  type: "document",
  file: "https://example.com/invoice-1042.pdf",
  text: "Your invoice",
  docName: "invoice-1042.pdf",
});

3. Receive replies with Express (1 minute)

Register your public URL as the instance webhook with the messages event enabled (see configure webhook). For local development you need a tunnel, because the API has to reach you over the internet.

Always drop fromMe events, or an auto-reply will answer itself in a loop. Check the exact payload with a real message first; to validate the X-ZapZap-Signature header and deduplicate repeated deliveries, follow webhooks in production.

Node.js: Express

import express from "express";

const app = express();
app.use(express.json());

app.post("/webhook", (req, res) => {
  res.sendStatus(200); // answer first, process after

  const msg = req.body?.data?.message;
  if (!msg || msg.fromMe) return; // ignore your own messages

  console.log("from " + msg.chatid + ": " + msg.text);
  // reply, save to a database, call an AI agent...
});

app.listen(3000, () => console.log("listening on :3000"));

4. Retry the right errors

Retry only 429 and 5xx, with growing waits. A 400, 401 or 403 means the request or the credentials are wrong, and retrying only produces more errors.

Node.js

async function withRetry(fn, tries = 4) {
  for (let i = 0; ; i++) {
    try {
      return await fn();
    } catch (err) {
      const retryable = /ZapZap (429|5\d\d)/.test(err.message);
      if (!retryable || i >= tries - 1) throw err;
      await new Promise((r) => setTimeout(r, 2 ** i * 1000));
    }
  }
}

await withRetry(() => zapzap("/send/text", { number: "5511999999999", text: "Retried send" }));

5. Before you send to real people

Credentials only in environment variables, never in git. A timeout on every call and retry only on 429 and 5xx. For lists, send one at a time with a random pause between messages instead of firing everything in parallel; treat the pause values as your own starting point, not a WhatsApp rule, and for big campaigns the server-side queue takes this out of your app. Send only to people who agreed to receive messages from you, and honor opt-outs.

Unofficial means ban risk. Pacing, warm-up and clean lists reduce it, nothing removes it. Use a number you can afford to lose while testing, and read the anti-ban guide. If you need a sanctioned channel, see official API vs ZapZap. For planning: R$15 per active number per month, prepaid, no per-message fee (pricing).

Full endpoint list: API reference and the OpenAPI file. Prefer another language? The same endpoints work from Python and the other guides in the blog, or without code in n8n. For a longer version of this guide, see send WhatsApp from Node.js. The official SDKs are not published on npm or PyPI yet; the source is on GitHub at zapzap-sdk-node, zapzap-sdk-python and zapzap-sdk-php, and the REST API works directly with fetch, as above.

Quick summary

  • Node 18's built-in fetch is enough: one POST to /send/text with x-api-key and x-api-secret.
  • Files go through /send/media with a public URL; replies arrive by webhook, and fromMe events must be dropped.
  • Retry only 429 and 5xx, with growing waits. 400, 401 and 403 are request or credential errors.
  • The SDKs are not on npm or PyPI yet (repositories are on GitHub); the REST API works directly, with no library.

Also read

Frequently asked questions

Do I need a library to send WhatsApp from Node.js?

No. Node 18's built-in fetch makes the POST to /send/text. Express only comes in if you want to receive replies by webhook.

Why does my bot answer itself in a loop?

Because the webhook also delivers the messages you sent yourself. Drop events where fromMe is true before replying.

Is there a ZapZap SDK for Node.js on npm?

It is not published on npm yet. The code is at github.com/zapzapapi/zapzap-sdk-node, and you can use the REST API directly with fetch, as in this guide.

Can sending through this API get my number banned?

Yes. The API is unofficial and goes against WhatsApp's terms. Pacing, warm-up, opt-in and clean lists reduce the risk, but nothing removes it. Test with a number you can afford to lose.

Send your first message now

Create the account, connect a test number by QR code and run send.mjs. R$15 per active number per month, no per-message fee.