Quickstart
Run a WATS bot offline — no credentials, no Meta app, no webhook tunnel.
You get a bot that answers an inbound WhatsApp message — with zero
credentials. The Graph client runs against a mock transport, so the demo is
fully offline: no Meta app, no token, no webhook tunnel. It exercises the
same createWhatsApp / onMessage / sendText facade you will use live;
only the transport is swapped.
Install
bun add @wats/core @wats/graphRuntime
The @wats/* packages run on Bun, Node 20+, Cloudflare Workers, and Deno —
they use portable Web platform APIs only. The @wats/cli serve and upgrade
commands require Bun (Bun.serve and bun update). On Node, Workers, or Deno,
consume the packages directly with your own server; see
deploy on Node for a node:http example.
The bot
import { createWhatsApp, normalizeWebhookEnvelope } from "@wats/core";
import { createMockTransport } from "@wats/graph/testing";
// Mock transport: Graph calls are captured locally. Nothing leaves the machine.
const mock = createMockTransport({
defaultResponse: {
status: 200,
headers: { "content-type": "application/json" },
body: { messaging_product: "whatsapp", messages: [{ id: "wamid.DEMO_REPLY" }] },
},
});
// createWhatsApp wires the Graph client, typed router, and listener registry
// behind one facade. accessToken is required but never sent anywhere — the
// mock transport intercepts every request.
const wa = createWhatsApp({
accessToken: "demo-token",
phoneNumberId: "15550000000",
apiVersion: "v25.0",
transport: mock.transport,
});
// onMessage registers a handler fired for every inbound message update.
wa.onMessage(async (ctx) => {
const message = ctx.update.message;
if (message.type !== "text") return;
await wa.sendText({ to: message.from, text: `pong: ${message.text.body}` });
});
// A synthetic inbound webhook, shaped exactly as Meta delivers it.
const envelope = {
object: "whatsapp_business_account",
entry: [{ id: "demo-waba", changes: [{ field: "messages", value: {
messaging_product: "whatsapp",
metadata: { display_phone_number: "15550000000", phone_number_id: "15550000000" },
messages: [{ from: "15550001111", id: "wamid.INBOUND", timestamp: "1713697100",
type: "text", text: { body: "ping" } }],
} }] }],
};
for (const update of normalizeWebhookEnvelope(envelope).updates) {
await wa.dispatch(update);
}
console.log(`replied to 15550001111 with wamid.DEMO_REPLY`);
console.log(`graph requests captured by mock: ${mock.requests.length}`);
Run it
bun run site/snippets/quickstart.tsExpected output:
replied to 15550001111 with wamid.DEMO_REPLY
graph requests captured by mock: 1Go live
Swap the mock transport for the default fetch transport and a real access token. The end-to-end tutorial walks the credentialed path; this site never asks for or handles tokens. The guide covers the full getting-started walkthrough and the playground explores the API in the browser, offline.