Skip to main content
Level: Advanced · Tags: WhatsApp, Cloud API, Webhooks, Security, Templates Features shown: WhatsApp Cloud API provider (@botmesh/meta: ingest, verifyChallenge, appSecret) · 24-hour window and templates (OutsideSessionWindowError, kind: 'template') · Sending outside a handler (bot.send) · Flows (.ask with validate) · Security · Graceful shutdown · Terminal simulator

Prerequisites

  • Node.js 22.12 or newer and an ESM project ("type": "module"), as in Getting started.
  • A Meta app with the WhatsApp product: an access token, the phone number id, the app secret, and a verify token you choose. An approved template named order_shipped with one body parameter.
  • A public HTTPS URL that reaches port 3000 (a reverse proxy, or a tunnel while you develop).

The complete server

How it works

1. The bot is a factory

The conversation lives in createBot, which receives its provider. npx botmesh dev server.js imports the factory and injects the terminal simulator, so you iterate on the replies without Meta, tokens or a public URL. The Cloud API wiring only runs under isMainModule (step 5).

2. Raw bytes, never re-serialized JSON

Meta signs the exact bytes it sends with your app secret (X-Hub-Signature-256). readRawBody collects them as a Buffer, with a 1 MiB limit, and provider.ingest(raw, req.headers) checks the signature before parsing. Parsing the body and serializing it again (what a JSON body parser does) changes the bytes, so the signature no longer matches: do not put express.json() or similar in front of the webhook.

3. The 24-hour window

On the Cloud API, free-form messages only reach a user within 24 hours of their last message. Outside it, Meta rejects the send with error 131047, which the provider turns into OutsideSessionWindowError; the server then sends the approved order_shipped template, which is always allowed. To have the provider track the window, queue the message and retry it when the user replies, configure its sessionWindow option instead.

4. The routes

ingest() returns right after dispatching the events, so Meta gets its 200 at once while the handlers run. The admin token is compared in constant time, and the route only accepts 4 KiB.

5. Running it

env() stops the process at startup with one clear line when a variable is missing, instead of a provider error later. appSecret turns the signature check on: never deploy without it (and never set allowUnsignedWebhooks). On SIGTERM the server stops accepting webhooks (Meta retries what it could not deliver) and the bot drains the replies in flight. @botmesh/server packages the same ingress with concurrency limits, timeouts and adapters for Express, Fastify and Hono: see Webhook server.

Sample transcript

The conversation, captured with npx botmesh dev server.js:
On the Cloud API, the same replies go out through the Graph API.

Run it on WhatsApp

In the Meta app dashboard, set the callback URL to https://<your-domain>/webhook with the same verify token and subscribe to the messages field. Then message the business number from WhatsApp, and call the notify route from your backend:

Next steps