All integration guides

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" >> .env

2. 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.

Other languages and frameworks