All integration guides

TypeScript integration

Send WhatsApp messages with TypeScript

A complete, runnable TypeScript example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — TypeScript 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 TypeScript code as an environment variable. Never commit the key or ship it in client-side code.

# No dependencies needed on Node 18+ / Bun / Deno.
npm i -D typescript @types/node

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";

interface SpwpEnvelope<T> {
  data: T;
  meta: { requestId?: string; timestamp: string };
}

interface SpwpError {
  statusCode: number;
  code: string;
  message: string;
}

export interface WhatsAppMessage {
  id: string;
  status: "QUEUED" | "SENDING" | "SENT" | "DELIVERED" | "READ" | "FAILED";
  remoteJid: string;
  createdAt: string;
}

export async function sendWhatsAppText(
  to: string,
  text: string,
): Promise<WhatsAppMessage> {
  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()) as SpwpError;
    throw new Error(`SPWP ${err.code}: ${err.message}`);
  }

  const { data } = (await res.json()) as SpwpEnvelope<WhatsAppMessage>;
  return data;
}

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.

type MediaKind = "image" | "video" | "audio" | "document";

export async function sendWhatsAppMedia(
  kind: MediaKind,
  to: string,
  source: string,
  caption?: string,
): Promise<WhatsAppMessage> {
  const res = await fetch(`${API_BASE}/public/messages/${kind}`, {
    method: "POST",
    headers: {
      "X-API-Key": process.env.SPWP_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ to, source, caption }),
  });

  const { data } = (await res.json()) as SpwpEnvelope<WhatsAppMessage>;
  return data;
}

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";

export function isValidSpwpSignature(
  rawBody: string,
  timestamp: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const v1 = signatureHeader.match(/v1=([a-f0-9]+)/)?.[1];
  if (!v1) return false;

  // Replay protection.
  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60_000) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  return (
    v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
  );
}

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