Guides / Developer tutorial
How to Send WhatsApp Messages Programmatically in Node.js
You don't need Meta's approval process or a heavyweight SDK to send WhatsApp messages from Node.js. With a QR-linked QaziHost WhatsApp API gateway, it's one HTTP POST. This guide covers setup, text and media messages, OTP codes, batch sends, and production-grade error handling.
1. Get an API key (2 minutes)
- Create a free account and connect your WhatsApp number by scanning a QR code - the same pairing flow as WhatsApp Web.
- In Dashboard → API Keys, create a key. Optionally restrict it to your server's domain and set a daily send cap.
- Store the key in an environment variable (WA_API_KEY) - never commit it.
2. Send your first message
Node 18+ ships with fetch built in, so there is nothing to install:
const res = await fetch("https://your-platform-domain.com/api/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.WA_API_KEY}`,
},
body: JSON.stringify({
to: "923001234567", // international format, digits only
type: "text",
message: "Hello from Node.js! 👋",
}),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
console.log("Sent:", data.messageId);3. Send images and documents
Every message type uses the same endpoint - change the type field and pass a public URL for the media:
// Image with a caption
await sendWhatsApp({
to: "923001234567",
type: "image",
url: "https://example.com/receipt.png",
caption: "Your receipt 🧾",
});
// PDF document
await sendWhatsApp({
to: "923001234567",
type: "document",
url: "https://example.com/invoice-1042.pdf",
fileName: "invoice-1042.pdf",
mimetype: "application/pdf",
});
async function sendWhatsApp(payload) {
const res = await fetch("https://your-platform-domain.com/api/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.WA_API_KEY}`,
},
body: JSON.stringify(payload),
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
return data;
}4. OTP verification codes
import crypto from "node:crypto";
export async function sendOtp(phone) {
const code = crypto.randomInt(100000, 999999);
await sendWhatsApp({
to: phone,
type: "text",
message: `Your verification code is ${code}. It expires in 5 minutes.`,
});
return code; // hash + store with a 5-minute TTL, compare on submit
}💡 Keep OTPs short-lived and single-use, and set a daily limit on the API key you use for OTP traffic - a leaked key then can't drain your quota.
5. Batch sending
For up to 100 messages per request, use the batch endpoint - it returns a per-recipient result array so one bad number never aborts the rest:
const res = await fetch("https://your-platform-domain.com/api/v1/send-batch", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.WA_API_KEY}`,
},
body: JSON.stringify({
messages: customers.map((c) => ({
to: c.phone,
type: "text",
message: `Hi ${c.name}, your order #${c.orderId} has shipped!`,
})),
}),
});
const { sent, failed, results } = await res.json();
console.log(`${sent} delivered to queue, ${failed} failed`);
for (const r of results.filter((r) => !r.success)) {
console.warn(`${r.to}: ${r.error}`);
}6. Handle errors like production code
| HTTP status | Meaning | What to do |
|---|---|---|
| 400 | Invalid payload (bad number, missing field) | Fix the request; log and skip |
| 401 / 403 | Bad, disabled, or origin-blocked key | Check the key and its origin allowlist |
| 402 | Membership expired or monthly quota used | Alert whoever manages billing; renew to resume |
| 429 | Key's daily limit reached | Back off until midnight or raise the key limit |
| 503 | WhatsApp number disconnected | Re-scan the QR in the dashboard, then retry |
A simple retry wrapper that respects those semantics: retry 503 a few times with backoff, never retry 4xx blindly.
async function sendWithRetry(payload, tries = 3) {
for (let i = 0; i < tries; i++) {
const res = await fetch("https://your-platform-domain.com/api/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.WA_API_KEY}`,
},
body: JSON.stringify(payload),
});
if (res.status === 503 && i < tries - 1) {
await new Promise((r) => setTimeout(r, 2 ** i * 5000));
continue; // number reconnecting - wait and retry
}
const data = await res.json();
if (!data.success) throw new Error(`${res.status}: ${data.error}`);
return data;
}
}Try it with your own number
Free account, QR-code setup, plans from Rs 1,500/month. Everything in this guide works out of the box.