Send WhatsApp messages from Node.js and Python
You do not need a library to send WhatsApp messages from Node.js or Python. The Qwela API is plain JSON over HTTPS, so the built-in fetch in Node 18 and later, or requests in Python, is enough. This guide covers sending, checking a message's status, handling errors, and receiving replies.
Setup
Link a WhatsApp number by scanning the QR code in the Qwela dashboard, then create an API key and put it in an environment variable called QWELA_KEY. Numbers should include the country code, such as 254712345678.
Send a message in Node.js
const res = await fetch("https://qwela.jpl365.com/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QWELA_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ to: "254712345678", text: "Your order is on its way." }),
});
const data = await res.json();
if (res.status !== 202) throw new Error(`Qwela ${res.status}: ${data.error}`);
console.log(data.id, data.status); // "queued"
Send a message in Python
import os
import requests
def send_whatsapp(to: str, text: str) -> dict:
r = requests.post(
"https://qwela.jpl365.com/v1/messages",
headers={"Authorization": "Bearer " + os.environ["QWELA_KEY"]},
json={"to": to, "text": text},
timeout=15,
)
if r.status_code != 202:
raise RuntimeError(f"Qwela {r.status_code}: {r.json().get('error')}")
return r.json()
print(send_whatsapp("254712345678", "Your order is on its way."))
Wrap it once, reuse it everywhere
Put sending in a single function and call that function from the rest of your code. Here it is in Node.js with a simple retry for temporary failures:
export async function sendWhatsApp(to, text, attempts = 3) {
for (let i = 1; i <= attempts; i++) {
const res = await fetch("https://qwela.jpl365.com/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QWELA_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ to, text }),
});
if (res.status === 202) return res.json();
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || i === attempts) {
const body = await res.json().catch(() => ({}));
throw new Error(`Qwela ${res.status}: ${body.error ?? "unknown error"}`);
}
await new Promise((r) => setTimeout(r, 2000 * i)); // back off
}
}
A 429 can mean you sent too many requests too quickly, which a short wait fixes, or that you hit the daily message limit, which only resets the next day. Read the error field to tell them apart, and do not keep retrying a daily limit.
Check a message's status
const r = await fetch(`https://qwela.jpl365.com/v1/messages/${id}`, {
headers: { Authorization: `Bearer ${process.env.QWELA_KEY}` },
});
console.log(await r.json());
// { id, to, text, status: "queued" | "sending" | "sent" | "failed", error?, created_at }
Statuses move from queued to sending to sent, or to failed with an error such as "number is not on WhatsApp". sent means WhatsApp accepted the message. It does not mean the person has read it.
Receive replies in Python (Flask)
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["QWELA_WEBHOOK_SECRET"]
def verify(raw: bytes, headers) -> bool:
ts = headers.get("X-Qwela-Timestamp", "")
sig = headers.get("X-Qwela-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
expected = "sha256=" + hmac.new(
SECRET.encode(), ts.encode() + b"." + raw, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)
@app.post("/webhooks/qwela")
def webhook():
raw = request.get_data() # raw bytes, before any JSON parsing
if not verify(raw, request.headers):
abort(401)
event = request.get_json()
if event["event"] == "message.received":
print(event["data"]["from"], event["data"]["text"])
return "", 200
The same check for Node.js and Go, and the mistakes that break it, are in our guide to verifying webhook signatures.
Useful patterns
- Send from a background job, not inside a web request, so a slow response never delays your user.
- Check the session with
GET /v1/sessionif sends start failing. It tells you whether your number is still connected. - Deduplicate replies using the event
id, because a retry can deliver the same event twice.
Using PHP instead? See the PHP and Laravel version. Want a payment-receipt example? See M-Pesa confirmations on WhatsApp.