Send WhatsApp with Node.js: REST API example with fetch
To send WhatsApp with Node.js you POST with fetch to /api/v1/{instanceId}/send/text, with the x-api-key and x-api-secret headers and a JSON body with number and text. fetch is native since Node 18, so you need neither axios nor a WhatsApp package. Below: text and file sends, retry, a batch with a pause and an Express webhook.
Published on · 3 min read
What you need before starting
A ZapZap account, an instance connected by QR code, the API key and secret and the instance ID. Authentication is two headers. Use Node 18 or later and keep credentials in environment variables.
Validate the instance with cURL before coding, to tell code problems from account problems.
cURL
curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/send/text \
-H "Content-Type: application/json" \
-H "x-api-key: SUA_API_KEY" \
-H "x-api-secret: SEU_API_SECRET" \
-d '{"number":"5511999999999","text":"Olá! Teste de envio pela API."}'How to send a text message with fetch
The function below builds the request, sets a timeout with AbortSignal.timeout and handles HTTP errors. Without a timeout, a stuck call holds your process.
An ES module (.mjs file or "type": "module") lets you use top-level await. In CommonJS, wrap the call in an async function.
Node.js
const BASE = "https://api.zapzapapi.com/api/v1/" + process.env.ZAPZAP_INSTANCE_ID;
const HEADERS = {
"Content-Type": "application/json",
"x-api-key": process.env.ZAPZAP_API_KEY,
"x-api-secret": process.env.ZAPZAP_API_SECRET,
};
export async function enviarTexto(number, text) {
const r = await fetch(BASE + "/send/text", {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ number, text }),
signal: AbortSignal.timeout(15000),
});
const corpo = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error("ZapZap " + r.status), { status: r.status, corpo });
return corpo; // { messageId, status }
}
console.log(await enviarTexto("5511999999999", "Olá! Seu pedido saiu para entrega."));How to send an image, PDF and audio with Node.js
The route is /send/media. The type field picks the kind (image, video, audio, ptt, document, sticker and others), file is the public URL, text is the caption and docName the document name. For a PDF use type document.
The file must be at a public URL. If it is on your disk or in memory, publish it to storage with a temporary link first.
Node.js
export async function enviarPdf(number, file, docName, text = "") {
const r = await fetch(BASE + "/send/media", {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ number, type: "document", file, docName, text }),
signal: AbortSignal.timeout(60000),
});
if (!r.ok) throw new Error("ZapZap " + r.status + " " + (await r.text()));
return r.json();
}How to retry and send a list with a pause
Retry only 429 and 5xx, with growing waits. 400, 401 and 403 point to the request or the credentials, and retrying only creates more errors.
For lists, send in series with a random pause between messages instead of firing everything in parallel. The example values (2 to 6 seconds) are your own starting point, not a WhatsApp rule. For bigger campaigns, the server queue (POST /sender/simple, with delayMin and delayMax) takes that control out of your app.
Node.js
const dormir = (ms) => new Promise((ok) => setTimeout(ok, ms));
export async function enviarComRetry(number, text, tentativas = 4) {
for (let i = 0; i < tentativas; i++) {
try {
return await enviarTexto(number, text);
} catch (e) {
const transitorio = !e.status || e.status === 429 || e.status >= 500;
if (!transitorio || i === tentativas - 1) throw e;
await dormir(2 ** i * 1000 + Math.random() * 500);
}
}
}
export async function enviarLista(contatos) {
for (const [numero, texto] of Object.entries(contatos)) {
await enviarComRetry(numero, texto);
await dormir(2000 + Math.random() * 4000);
}
}How to receive replies with an Express webhook
Set the URL in configure webhook and turn on the messages event. The incoming message is in data.message (text, chatid, fromMe). Ignore fromMe true. Reply 200 before processing.
To validate the X-ZapZap-Signature and deduplicate redeliveries, follow the webhooks in production guide. If the event does not arrive, see webhook not arriving.
Node.js (Express)
import express from "express";
const app = express();
app.use(express.json({ limit: "2mb" }));
app.post("/webhooks/zapzap", (req, res) => {
res.sendStatus(200); // confirma primeiro
const evento = req.body;
const msg = evento.data && evento.data.message;
if (evento.event !== "messages" || !msg || msg.fromMe) return;
console.log("mensagem de", msg.chatid, ":", msg.text);
});
app.listen(3000);Checklist before going to production
Credentials out of the code; a timeout on every call; retry only on 429 and 5xx; sending in series with a pause; logging of status and body on failures; a validated webhook signature; and sending only to people who agreed to receive. If you prefer automation without code, the n8n node uses the same endpoints.
Other languages: Python, PHP, Ruby, Go. All endpoints are in the API reference.
Quick summary
- Node 18 or later ships fetch: you do not need axios to send WhatsApp.
- Always use a timeout (AbortSignal.timeout) and retry only 429 and 5xx.
- For lists, send in series with a random pause between messages.
- In Express, reply 200 first and ignore messages with fromMe.
Keep reading
Also read
Frequently asked questions
Do I need axios or a WhatsApp library?
No. Node 18's native fetch is enough. Axios works the same way, with the same headers and the same JSON.
Does it work with TypeScript?
Yes. The code is the same with types on the parameters. The OpenAPI file can generate types for the responses if you want full typing.
How do I avoid sending the same message twice?
Store an idempotency key in your database (for example order plus notice type) before calling the API and check it before resending. Retry should only happen when the call failed without a clear response.
Can using an unofficial API get my number banned?
It can. An unofficial API goes against WhatsApp's terms and the number can be limited or banned. Opt-in, a human pace, warm-up and a clean list reduce the risk, but there is no guarantee. For a sanctioned channel, use Meta's official API.
Run the example on your instance
Create the account, connect a number and send the first message. R$15 per active number per month, no per-message fee.
