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/node2. 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.