Deploying a WATS webhook on Bun
Deploy a WATS webhook server under the Bun runtime using createBunWebhookServer.
reviewed 2026-06-12
This page covers the Bun runtime with createBunWebhookServer. For Node,
see deploy on Node. For Cloudflare Workers, Deno,
and other edge runtimes, see
deploy on Cloudflare Workers.
Install
bun add @wats/http @wats/core @wats/graph @wats/cryptoMinimal server
// server.ts
import { createBunWebhookServer, createWebhookAdapter } from "@wats/http";
import { createWhatsApp } from "@wats/core";
const wa = createWhatsApp({
accessToken: process.env.WATS_ACCESS_TOKEN!,
phoneNumberId: process.env.WATS_PHONE_NUMBER_ID!,
apiVersion: "v25.0",
});
wa.onMessage(async (ctx) => {
const message = ctx.update.message;
if (message.type !== "text") return;
await wa.sendText({ to: message.from, text: message.text.body });
});
const adapter = createWebhookAdapter({
verifyToken: process.env.WATS_VERIFY_TOKEN!,
appSecret: process.env.WATS_APP_SECRET!,
whatsapp: wa,
logger: (event) => console.log("wats-http", event.type, event)
});
const server = createBunWebhookServer(adapter, {
port: Number(process.env.PORT ?? 8787),
hostname: "0.0.0.0"
});
console.log(`WATS webhook listening on http://${server.hostname}:${server.port}`);Run it:
bun run server.tsBun.serve already speaks WinterCG Request → Response, so the Bun adapter
is a thin wrapper over createFetchWebhookHandler. Every request goes through
the same runtime-neutral core as the fetch and Node adapters, so the
status-code taxonomy is identical across runtimes.
mTLS and the HMAC boundary
createBunWebhookServer verifies Meta webhook POST bodies at the app layer
with HMAC-SHA256 from X-Hub-Signature-256. Keep that verification enabled;
it is independent of TLS termination.
If your production ingress opts into Meta outbound webhook mTLS, configure
client-certificate validation at the TLS terminator in front of Bun — reverse
proxy, load balancer, CDN, or platform. Meta's transition names the Meta-owned
root meta-outbound-api-ca-2025-12.pem; WATS does not vendor that CA, embed
PEM contents, or configure your infrastructure. Obtain and rotate the CA
through Meta's authoritative channel, then pass only already-accepted HTTP
requests to the Bun handler.
Graceful shutdown
process.on("SIGINT", () => {
server.stop(true);
process.exit(0);
});server.stop(true) terminates active connections. server.stop(false)
stops accepting new connections but lets in-flight requests finish.
Observability
The logger hook fires per lifecycle stage (request_received,
signature_verified, body_normalized, dispatched, response_sent,
error). Pipe it into any observability sink:
logger: (event) => {
metrics.increment(`wats.webhook.${event.type}`);
if (event.type === "error") {
errorTracker.capture(event.error, { stage: event.stage });
}
}Verify it works
Hit the GET verify endpoint the way Meta will:
curl -i "http://127.0.0.1:8787/?hub.mode=subscribe&hub.verify_token=$WATS_VERIFY_TOKEN&hub.challenge=ping"A correct setup returns 200 with body ping. A wrong token returns 401.
Only point Meta at the URL once this passes.