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:

send.js
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:

send-media.js
// 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

send-otp.js
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:

send-batch.js
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 statusMeaningWhat to do
400Invalid payload (bad number, missing field)Fix the request; log and skip
401 / 403Bad, disabled, or origin-blocked keyCheck the key and its origin allowlist
402Membership expired or monthly quota usedAlert whoever manages billing; renew to resume
429Key's daily limit reachedBack off until midnight or raise the key limit
503WhatsApp number disconnectedRe-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.

with-retry.js
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.