Skip to content

Repository files navigation

wapi

WhatsApp over HTTP, on your own box.
A self-hosted REST API for WhatsApp, wire-compatible with WasenderAPI.

Site · Guide · API reference · OpenAPI


Link a number, get an API key, send and receive messages over plain HTTP.

curl -X POST https://api.wapi.crafter.run/api/send-message \
  -H "Authorization: Bearer $KEY" \
  -d '{"to":"+51999888777","text":"hello"}'
{ "success": true, "data": { "msgId": 100024, "jid": "+51999888777", "status": "in_progress" } }

Meta's Cloud API covers business messaging, not the conversations most teams actually run on — group chats, personal threads, the number people already message. Reaching those means driving a real WhatsApp client. wapi does that and puts a stable REST surface in front of it.

Architecture

flowchart TB
    NET["Cloudflare · Traefik"]
    NET --> WEB["web<br/>Next.js 16<br/>dashboard · guide · Clerk"]
    NET --> API["api<br/>Hono on Bun<br/>29 REST routes · stateless"]
    API -- "RPC + Redis pub/sub" --> GW["gateway<br/>Node 22 · Baileys sockets<br/>stateful — one owner per session"]
    GW -- enqueue --> WW["webhook-worker<br/>BullMQ · retry · backoff · DLQ"]
    WW -- "POST + signature" --> APP["your app"]

    subgraph STATE["shared state"]
        direction LR
        PG[("Postgres")]
        RD[("Redis")]
        OBJ[("UploadX / MinIO")]
    end

    API --> STATE
    GW --> STATE
    WW --> STATE
Loading

The gateway is the only stateful piece: a WhatsApp session is a live socket that exactly one process may own. Everything else scales sideways. Credentials live in Postgres rather than on disk, so a redeploy reconnects instead of asking you to scan a QR again.

The dashboard is both halves of that picture: link a number and watch its QR, then browse its contacts, groups and message log, watch webhook deliveries land as they happen, and run a health check that sends one message to the number itself and tells you what actually works.

sequenceDiagram
    autonumber
    participant C as your app
    participant A as api
    participant G as gateway
    participant W as WhatsApp
    participant K as webhook-worker

    C->>A: POST /api/send-message
    A->>A: validate · allocate msgId
    A->>G: internal RPC (deadlined)
    G->>W: Baileys socket
    A-->>C: 200 — msgId, status in_progress
    W-->>G: receipt / inbound message
    G->>K: enqueue event
    K-->>C: POST your webhook_url
Loading

No phone? Use a sandbox

Linking a real number needs a phone, a QR scan, and a number you are willing to have banned. A sandbox session needs none of them — a fake number on a fake WhatsApp that pairs itself, and goes through the same routes and the same code as a real session.

curl -X POST https://api.wapi.crafter.run/api/sandbox/sessions   -H "Authorization: Bearer $PAT" -d '{"name":"my sandbox"}'

It has a small directory, accepts sends, and — the point — can be made to receive messages, so your webhook handler gets a genuine signed delivery to prove itself against. Group mutations work too, which is the one part of the surface nobody should rehearse on a real number.

The dashboard gives a sandbox its own Sandbox tab: the invented contacts, the conversation as it happens, and a box to write a message as one of those contacts — the shortest path from "I have a webhook handler" to "I have watched it run".

Run it

bun install
cp .env.example .env      # Postgres, Redis, Clerk, UploadX
bun run typecheck && bun test
docker compose up

bun test covers unit, contract and SDK-compat suites; the ones needing a running stack or a real number skip themselves rather than fail. The dashboard has its own browser suite (bun run --cwd apps/web e2e) — see apps/web/e2e/README.md for the one-time Chromium and Clerk setup.

The full stack is four services plus Postgres, Redis and object storage — see docker-compose.yaml. Deployment targets a Dokploy VPS from a single root Dockerfile.

Compatibility

29 endpoints reproduced from the WasenderAPI interface, down to the parts nobody would design on purpose: five distinct success envelopes, three failure envelopes, and two unrelated pagination shapes. Their published npm client runs against wapi unmodified — only the base URL changes:

const wa = createWasender(process.env.WAPI_KEY, undefined, "https://api.wapi.crafter.run/api");

That claim is a test suite, not an aspiration: response schemas are checked against the provider's own documented examples, and again against live responses.

Clients

Zero runtime dependencies either side. Types are generated from the OpenAPI document so they cannot drift from the server; method names are hand-written, because generated ones read postApiWhatsappSessionsWhatsappSessionRegenerateKey.

Neither is published to a registry — they live in sdk/, so installation comes from here.

Go — resolves subdirectory modules natively:

go get github.com/crafter-station/wapi/sdk/go@main
client := wapi.New(os.Getenv("WAPI_KEY"))
res, err := client.Messages.Send(ctx, "+51999888777", wapi.Text("hello"))

Python — pip understands git subdirectories:

pip install "git+https://github.com/crafter-station/wapi.git#subdirectory=sdk/python"
client = WapiClient(api_key=os.environ["WAPI_KEY"])
client.messages.send(to="+51999888777", text="hello")

TypeScript — vendored, because npm cannot install a subdirectory of a git repository and this one sits in a monorepo:

npx giget@latest gh:crafter-station/wapi/sdk/typescript/src src/wapi
const wapi = new WapiClient({ apiKey: process.env.WAPI_KEY! });
await wapi.messages.send({ to: "+51999888777", text: "hello" });

Building with an agent

npx skills@latest add crafter-station/wapi --skill=wapi-nextjs

Ships a server-only client, a webhook route handler, and the API's non-obvious behaviour — so your agent writes the integration correctly the first time. Read it first; skills run with your agent's permissions.

Worth knowing

wapi is built on Baileys, which speaks WhatsApp's protocol directly. That is what makes group access possible, and it is against WhatsApp's terms — numbers driven this way can be restricted or banned. Each session can route through its own proxy (http, https or socks5, covering both the socket and media transfers) and an account-protection mode paces sends. Neither is a guarantee.

Use a number you can afford to lose.

Layout

Path
apps/api Hono on Bun — the 29 routes, stateless
apps/gateway Node 22 — Baileys sockets, internal RPC only
apps/webhook-worker BullMQ — delivery with retry and backoff
apps/web Next.js 16 — dashboard, guide, Clerk auth
packages/contracts Zod contracts + the emitted OpenAPI document
packages/core shared logic, WhatsAppEngine and Storage interfaces
packages/db Drizzle schema and migrations
packages/baileys-auth Postgres-backed AuthenticationState
sdk/typescript TypeScript client — generated types, hand-written surface
sdk/python Python client — stdlib only, same surface
sdk/go Go client — net/http only, nested module
compat/ SDK-compatibility, fidelity (sandbox) and live integration suites
apps/web/e2e Playwright — the only thing that renders a page

Design decisions and their reasoning live in PLAN.md; repo conventions and the traps worth knowing before changing anything are in AGENTS.md.

About

WhatsApp REST API — a drop-in-compatible WasenderAPI clone. Baileys engine, Hono API, Next.js dashboard.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages