Enviar WhatsApp con Node.js: ejemplo de API REST con fetch
Para enviar WhatsApp con Node.js haces un POST con fetch a /api/v1/{instanceId}/send/text, con los encabezados x-api-key y x-api-secret y un JSON con number y text. fetch es nativo desde Node 18, así que no necesitas axios ni un paquete de WhatsApp. Abajo: envío de texto y de archivo, retry, un lote con intervalo y un webhook en Express.
Publicado el · 3 min de lectura
Este artículo también está disponible en: Português · English
Qué necesitas antes de empezar
Cuenta en ZapZap, instancia conectada por código QR, clave y secreto de la API y el ID de la instancia. La autenticación es por dos encabezados. Usa Node 18 o superior y guarda las credenciales en variables de entorno.
Valida la instancia con cURL antes de programar, para separar problemas de código de problemas de cuenta.
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."}'Cómo enviar un mensaje de texto con fetch
La función de abajo arma la solicitud, define timeout con AbortSignal.timeout y trata el error HTTP. Sin timeout, una llamada trabada retiene tu proceso.
Un módulo ES (archivo .mjs o "type": "module") permite usar await en el nivel superior. En CommonJS, envuelve la llamada en una función asíncrona.
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."));Cómo enviar imagen, PDF y audio con Node.js
La ruta es /send/media. El campo type elige el tipo (image, video, audio, ptt, document, sticker y otros), file es la URL pública, text es el pie y docName el nombre del documento. Para PDF usa type document.
El archivo debe estar en una URL pública. Si está en tu disco o en memoria, publícalo primero en un almacenamiento con enlace temporal.
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();
}Cómo hacer retry y enviar una lista con intervalo
Repite solo 429 y 5xx, con espera creciente. 400, 401 y 403 apuntan a la solicitud o a las credenciales, y repetir solo genera más errores.
Para listas, envía en serie con una pausa aleatoria entre mensajes, en lugar de disparar todo en paralelo. Los valores del ejemplo (2 a 6 segundos) son un punto de partida tuyo, no una regla de WhatsApp. Para campañas mayores, la cola del servidor (POST /sender/simple, con delayMin y delayMax) saca ese control de tu aplicación.
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);
}
}Cómo recibir las respuestas con un webhook en Express
Configura la URL en configurar webhook y activa el evento messages. El mensaje recibido está en data.message (text, chatid, fromMe). Ignora fromMe verdadero. Responde 200 antes de procesar.
Para validar la firma X-ZapZap-Signature y deduplicar reentregas, sigue la guía de webhook en producción. Si el evento no llega, mira webhook que no llega.
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);Lista de verificación antes de producción
Credenciales fuera del código; timeout en cada llamada; retry solo en 429 y 5xx; envío en serie con intervalo; log del estado y del cuerpo en los fallos; firma del webhook validada; y enviar solo a quien aceptó recibir. Si prefieres automatizar sin código, el nodo de n8n usa los mismos endpoints.
Otros lenguajes: Python, PHP, Ruby, Go. Todos los endpoints están en la referencia de la API.
Resumen rápido
- Node 18 o superior trae fetch: no necesitas axios para enviar WhatsApp.
- Usa siempre timeout (AbortSignal.timeout) y repite solo 429 y 5xx.
- Para listas, envía en serie con pausa aleatoria entre los mensajes.
- En Express, responde 200 primero e ignora los mensajes con fromMe.
Sigue leyendo
Lee también
Preguntas frecuentes
¿Necesito axios o alguna biblioteca de WhatsApp?
No. El fetch nativo de Node 18 basta. Axios funciona igual, con los mismos encabezados y el mismo JSON.
¿Funciona con TypeScript?
Sí. El código es el mismo con tipos en los parámetros. El archivo OpenAPI puede generar tipos para las respuestas si quieres tipado completo.
¿Cómo evito enviar el mismo mensaje dos veces?
Guarda una clave de idempotencia en tu base (por ejemplo, pedido más tipo de aviso) antes de llamar a la API y consúltala antes de reenviar. El retry solo debe ocurrir cuando la llamada falló sin una respuesta clara.
¿Usar una API no oficial puede llevar al bloqueo del número?
Puede. Una API no oficial va contra los términos de WhatsApp y el número puede ser limitado o bloqueado. Opt-in, ritmo humano, calentamiento y lista limpia reducen el riesgo, pero no hay garantía. Para un canal homologado, usa la API oficial de Meta.
Ejecuta el ejemplo en tu instancia
Crea la cuenta, conecta un número y envía el primer mensaje. R$15 por número activo al mes, sin cobro por mensaje.
