JavaScript integration
Send WhatsApp messages with Node.js
A complete, runnable Node.js example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — Node.js talks to the API over ordinary HTTP, so you can paste this straight into an existing project.
About 5 minutes to your first delivered message
1. Setup
Create an API key in the dashboard under Accounts → your account → API keys, then make it available to your Node.js code as an environment variable. Never commit the key or ship it in client-side code.
# Node 18+ has global fetch built in, so there is nothing to install.
# Store the key in an environment variable, never in source control.
echo "SPWP_API_KEY=wak_live_your_key_here" >> .env2. Send a text message
One POST with a recipient and a body. The API replies 202 Accepted with a message id in QUEUED state; delivery happens asynchronously and you can track it by id or webhook.
const API_BASE = "https://api.spwp.app/api/v1";
export async function sendWhatsAppText(to, text) {
const res = await fetch(`${API_BASE}/public/messages/text`, {
method: "POST",
headers: {
"X-API-Key": process.env.SPWP_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ to, text }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`SPWP ${err.code}: ${err.message}`);
}
const { data } = await res.json();
return data; // { id, status: "QUEUED", ... }
}
await sendWhatsAppText("9647503505440", "Your verification code is 482310");3. Send an image, document or other media
Media endpoints take the same shape, with a source that is either a public HTTPS URL we fetch server-side or a base64 data URL you upload inline. Swap the path segment for video, audio, document or location.
await fetch(`${API_BASE}/public/messages/image`, {
method: "POST",
headers: {
"X-API-Key": process.env.SPWP_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "9647503505440",
source: "https://example.com/invoice-3214.png",
caption: "Your invoice for May",
}),
});4. Receive replies and delivery receipts
Inbound messages and status changes arrive as signed POSTs to your webhook URL. Recompute the HMAC over the timestamp and the raw request body — re-serialised JSON will not match — and reject anything older than five minutes.
import crypto from "node:crypto";
import express from "express";
const app = express();
// Capture the RAW body — re-serialised JSON will not match the HMAC.
app.post(
"/webhooks/spwp",
express.raw({ type: "application/json" }),
(req, res) => {
const ts = req.header("X-WA-Timestamp");
const v1 = (req.header("X-WA-Signature") ?? "").match(/v1=([a-f0-9]+)/)?.[1];
if (!ts || !v1) return res.status(400).end();
// Replay protection: reject anything older than five minutes.
if (Math.abs(Date.now() - Number(ts)) > 5 * 60_000) return res.status(400).end();
const expected = crypto
.createHmac("sha256", process.env.SPWP_WEBHOOK_SECRET)
.update(`${ts}.${req.body.toString("utf8")}`)
.digest("hex");
if (
v1.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.event, event.data.id);
res.status(200).end(); // Acknowledge fast, process in a background job.
},
);Error handling
Errors carry a non-2xx status and a stable code string you can branch on: VALIDATION_FAILED, INVALID_API_KEY, ACCOUNT_NOT_CONNECTED, RATE_LIMITED, MEDIA_FETCH_FAILED. Retry on 429 while honouring Retry-After; treat 401 as a configuration problem rather than a transient one.
Next steps
The full reference covers every endpoint, field and error code, plus the webhook payload shapes.