Deploying a WATS webhook on Cloudflare Workers
Deploy a WATS webhook on Cloudflare Workers, Deno, and other WinterCG fetch runtimes using createFetchWebhookHandler.
shape-only on Workers — the edge invariant is structurally tested; no full Miniflare integration test yet · reviewed 2026-06-12
This page covers Cloudflare Workers with createFetchWebhookHandler. The
same pattern works on Deno (Deno.serve), Vercel Edge Functions, and any
other runtime that implements the WinterCG fetch handler contract.
Why the fetch adapter works on Workers
@wats/http's createFetchWebhookHandler produces a pure
(request: Request) => Promise<Response> function. The file
packages/http/src/adapters/fetchAdapter.ts contains zero static
node:* imports — an invariant enforced by two structural tests
(packages/testing/tests/workspace-policy.test.ts +
packages/testing/edge/webhook-adapter.test.ts) so regressions can't
sneak in.
Install
npm install @wats/http @wats/core @wats/graph @wats/cryptoWorkers require "nodejs_compat" only for node:buffer-style
Node shims. WATS does not need any of them.
wrangler.toml
name = "wats-webhook"
main = "src/index.ts"
compatibility_date = "2024-10-01"
[vars]
VERIFY_TOKEN = "..."
[[secrets]]
binding = "APP_SECRET"
[[secrets]]
binding = "META_ACCESS_TOKEN"Worker entry
// src/index.ts
import {
createFetchWebhookHandler,
createWebhookAdapter
} from "@wats/http";
import { GraphClient, createFetchTransport } from "@wats/graph";
import { WhatsApp, message } from "@wats/core";
export interface Env {
VERIFY_TOKEN: string;
APP_SECRET: string;
META_ACCESS_TOKEN: string;
WA_PHONE_NUMBER_ID: string;
}
let handler: ((req: Request) => Promise<Response>) | undefined;
function buildHandler(env: Env) {
const graphClient = new GraphClient({
accessToken: env.META_ACCESS_TOKEN,
apiVersion: "v25.0",
transport: createFetchTransport()
});
const wa = new WhatsApp({
graphClient,
phoneNumberId: env.WA_PHONE_NUMBER_ID
});
wa.on(message, async (update) => {
// handler body — fire-and-forget from the Worker's perspective.
console.log("received:", update.message.from);
});
const adapter = createWebhookAdapter({
verifyToken: env.VERIFY_TOKEN,
appSecret: env.APP_SECRET,
whatsapp: wa
});
return createFetchWebhookHandler(adapter);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (handler === undefined) handler = buildHandler(env);
return handler(request);
}
};Deno
import { createFetchWebhookHandler, createWebhookAdapter } from "@wats/http";
// ... build adapter as above ...
const handler = createFetchWebhookHandler(adapter);
Deno.serve(handler);Request body size on Workers
Cloudflare Workers cap request bodies at runtime-plan-specific
limits (100 MiB on paid plans as of 2024). WATS's default
maxBodyBytes is 1 MiB — well within any realistic WhatsApp webhook
payload. Raise it via createWebhookAdapter({ ..., maxBodyBytes })
if your deployment genuinely needs larger envelopes.
mTLS and the HMAC boundary
The Worker handler still verifies Meta webhook POST bodies at the app layer
with HMAC-SHA256 from X-Hub-Signature-256. That verification happens in
@wats/http after the request reaches the Worker and remains required.
Optional Meta webhook mTLS is infrastructure-level client-certificate
validation before the Worker handler runs. Any Cloudflare/edge ingress mTLS
configuration you choose must trust Meta's owned root
meta-outbound-api-ca-2025-12.pem through the platform controls that support
client certificates. WATS does not vendor the CA, embed PEM contents, expose a
Worker-side certificate validator, or configure your infrastructure; obtain
and rotate the CA from Meta's authoritative channel.
Observability on Workers
Use tailed logs or a platform logging binding — logger fires
synchronously and the Worker's execution-context model is compatible
with a non-async logger callback.
Verify it works
curl -i "https://wats-webhook.<your-subdomain>.workers.dev/?hub.mode=subscribe&hub.verify_token=<token>&hub.challenge=ping"A correct deployment returns 200 with body ping; a wrong token returns
401. Only register the URL with Meta once this passes.