whatsapp-cloud-client
A typed WhatsApp Cloud API client for Node: webhook signature verification first, in constant time and fail-closed.
92downloads / month
project · open source
A typed WhatsApp Cloud API client for Node. Webhook signature verification first.
Ships TypeScript sources, verifies webhook signatures before anything else, and keeps the surface small enough to read in one sitting.
A typed WhatsApp Cloud API client for Node: webhook signature verification first, in constant time and fail-closed.
92downloads / month
A typed WhatsApp Cloud API client for Node. Webhook signature verification first, in constant time and fail-closed.
A typed WhatsApp Cloud API client for Node. Webhook signature verification first, in constant time and fail-closed.
npm i whatsapp-cloud-client
It ships TypeScript sources, not compiled JavaScript, so it needs a TypeScript-aware runtime or
bundler — Bun, tsx, Vite, Next, esbuild. That is a deliberate choice for now and not an oversight:
a compiled build is worth adding, and it is not worth pretending to have.
import { verifyMetaSignature, parseMetaWebhook } from "whatsapp-cloud-client/webhook";
// In your webhook handler. The RAW body — not the parsed one: re-serialising changes the bytes and
// the signature stops matching, which is the single most common way this check silently never passes.
export async function handler(rawBody: string, headers: Headers) {
if (!verifyMetaSignature(rawBody, headers.get("x-hub-signature-256"), process.env.META_APP_SECRET ?? "")) {
return new Response("bad signature", { status: 401 });
}
const { inbound, statuses } = parseMetaWebhook(JSON.parse(rawBody));
for (const message of inbound) {
// message.from is E.164 without the '+', message.body is the text,
// message.media is present when it carried an image, document, audio or video.
console.log(message.from, message.body);
}
for (const status of statuses) {
console.log(status.providerMessageId, status.status); // sent | delivered | read | failed
}
return new Response("ok");
}
Most of the code you write against the WhatsApp Cloud API is not sending messages — it is the part around it. Verifying that a webhook is really from Meta. Deciding whether a failure is worth retrying. Discovering that a media download is two calls and that the second one is not JSON. Finding out that a button parameter must be the link suffix, because Meta concatenates it onto the base it stored — and that it does not fail on send: the message is accepted, the row says delivered, and the link only breaks when the customer taps it.
This package is that part, extracted from a system where it runs against live customer conversations.
The signature check is fail-closed and takes the raw body. Every other design leaks: comparing
with === leaks timing, and re-serialising the parsed body changes the bytes so the check silently
never passes — which looks exactly like "it works" until someone forges a request.
Errors are classified by status and code, never by message text. Meta rewrites its English without notice, and a guard that matches on text goes quietly dead at the next rewording. The default for an unknown code is transient, and that is deliberate: a false terminal loses the customer's message forever, while a false transient spends a few API calls and ends up in a dead-letter queue anyway. The two mistakes do not cost the same.
See CONTRIBUTING.md — it says in the first line whether your pull request will be considered.
See SECURITY.md.
Apache-2.0. See LICENSE and AUTHORS.
Built by Vorluno, extracted from niiko — where it runs in production. Unofficial: not affiliated with, endorsed by, or sponsored by Meta.