wats.sh

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/graph

Runtime

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.ts

Expected output:

replied to 15550001111 with wamid.DEMO_REPLY
graph requests captured by mock: 1

Go 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.

On this page