API documentation

A credits-based gateway for your own WhatsApp and Telegram accounts. Connect a channel, then send and receive messages over one channel-agnostic API — by HTTP, by webhook, or driven by an AI agent over MCP. JSON in, JSON out, an example per endpoint.

Get started

Three steps to your first message:

  1. 1. Get an API key

    Go to Profile → API keys, create a key with the scopes you need, and copy the wgk_…token — it's shown once. The same key authenticates both the HTTP API and MCP.

  2. 2. Connect a channel

    On Connections, add a WhatsApp or Telegram connection and pair it — scan the QR (WhatsApp) or enter your phone number and the login code (Telegram). This links your own account and is a one-time human step. When the connection reads connected, it can send and receive.

  3. 3. Send your first message

    curl -X POST https://api.fastwa.dev/connections/{connection_id}/messages \
      -H "Authorization: Bearer wgk_…" \
      -H "content-type: application/json" \
      -d '{"to":"5491122334455","text":"hello from the gateway"}'
    
    # → { "command_id": "cmd_…", "status": "queued" }

Authentication

All requests go to https://api.fastwa.dev and authenticate with a bearer API key:

Authorization: Bearer wgk_…

Create and manage keys under Profile → API keys. Each key carries a set of scopes; an endpoint rejects a key that lacks its scope with 403, and an unknown or revoked key with 401.

Scopes

  • connections:read — list and read connections
  • messages:send — send messages
  • audit:read— read a connection's message history
  • balance:read — read your credit balance

Creating, pairing, and deleting connections, and managing keys, are dashboard-only operations — they aren't reachable with an API key.

Connections

A connection is one linked WhatsApp or Telegram account. Create and pair it in the dashboard (the QR / phone-code step is human); read it programmatically.

GET/connectionsconnections:read
GET/connections/{connection_id}connections:read
curl https://api.fastwa.dev/connections \
  -H "Authorization: Bearer wgk_…"

# → [
#   {
#     "id": "conn_…",
#     "channel": "whatsapp",        // or "telegram"
#     "label": "Sales line",
#     "jid": "[email protected]",
#     "status": "connected",        // pending | connected | disconnected | logged_out
#     "webhook_url": "https://…"
#   }
# ]

A connection can send only while its status is connected.

POST/connections/{connection_id}/webhook-secret/rotateconnections:write

Issues a new webhook signing secret for the connection and returns it once — see Webhooks.

Messaging

POST/connections/{connection_id}/messagesmessages:send

Sends a text message, or an image, video, audio or document. Costs 1 credit, is rate-limited per connection, and is recorded in the connection's audit log.

POST https://api.fastwa.dev/connections/{connection_id}/messages
Authorization: Bearer wgk_…
content-type: application/json

{
  "to": "5491122334455",   // WhatsApp: phone digits, country code, no "+"
                           // Telegram: @username, phone, or numeric id
  "text": "hello"
}

# → { "command_id": "cmd_…", "status": "queued" }

# With a file — "text" becomes the caption. We fetch the URL ourselves
# (public http(s) only, up to 16 MB):
{
  "to": "5491122334455",
  "text": "the invoice",
  "media": {
    "url": "https://example.com/invoice.pdf",
    "kind": "document",          // image | video | audio | document
    "filename": "invoice.pdf",   // optional
    "ptt": false                 // audio only: true sends a voice note
  }
}

# Reply to (quote) a message — optional. "message_id" is a message id from a
# webhook or the "command_id" of an earlier send. If it can't be found the
# message is still sent, just not quoted:
{
  "to": "5491122334455",
  "text": "yes, tomorrow",
  "reply_to": { "message_id": "cmd_…" }
}

Delivery is asynchronous: the call returns queued, then the result flips the message's audit entry to sent or failed.

Errors

  • 402 — insufficient credits
  • 404 — connection not found
  • 409 — connection not connected
  • 429 — rate limit exceeded for this connection

Group sends, location, and number-verification are on the roadmap.

Webhooks

Set a Webhook URL on a connection (Connections → 🪝). We POST each inbound message and status change to it as JSON:

POST <your webhook url>
{
  "event_id": "evt_…",
  "channel": "whatsapp",          // or "telegram"
  "connection_id": "conn_…",
  "type": "message",              // message | connection | pairing
  "ts": "2026-…Z",
  "data": {
    "wa_message_id": "…",
    "from": "5491155550001",
    "sender_name": "Alice",       // sender display name
    "chat": "120363…",
    "is_group": true,
    "kind": "text",               // text | image | audio | video | document | unknown
    "text": "hi there",
    "media": null,                // for files: { mime, size, caption?, filename?, ptt?,
                                  //   media_id, url } — url is signed, expires in 7 days
    "reply_to": null,
    "conversation": {
      "id": "120363…",
      "name": "Project group",    // group subject (WA) / chat title (TG) / contact name
      "is_group": true,
      "participant_count": 12     // groups only
    }
  }
}
  • channel tells you how to read from/chat — WhatsApp = phone numbers, Telegram = numeric ids.
  • type: "connection" events carry data.status (connected / disconnected / logged_out).
  • Received files are downloaded for you: fetch data.media.url (no API key needed; it expires), or GET /media/{media_id} with a key that has audit:read. Files are kept 7 days. A caption arrives in text.
  • Signed POSTs carry X-WAGW-Signature: sha256=<hex> — an HMAC-SHA256 of the raw request body. Verify it before trusting the payload.

Signing secrets

  • Each connection has its own signing secret (whsec_…). It's generated the first time a webhook_url is set and returned once, as webhook_secret on that POST /connections or PATCH /connections/{id} response (the dashboard shows it in a dialog). It is never readable again.
  • Lost it, or want a new one? POST /connections/{id}/webhook-secret/rotate (or Connections → 🔑) returns {connection_id, webhook_secret}, once. The old secret stops verifying immediately — update your receiver right away. A 503means signing secrets aren't enabled on this server.
  • Connections whose webhook was set up before per-connection secrets existed (and never rotated) are still signed with the global shared secret. Rotate to move one onto its own.
# verify (Python)
import hmac, hashlib
expected = "sha256=" + hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-WAGW-Signature"])

MCP / Agents

Connect an MCP client (e.g. Claude) over Streamable HTTP at https://api.fastwa.dev/mcp with Authorization: Bearer wgk_…. Each tool is gated by the key's scopes:

list_connections   (connections:read)
read_messages      (audit:read)
get_balance        (balance:read)
send_message       (messages:send)

An agent can read connections, read history, check balance, and send — but onboarding a new account (the QR / phone-code pairing) is still a human step in the dashboard.

Full reference: interactive Swagger at https://api.fastwa.dev/docs, plus a Postman collection + OpenAPI spec under docs/postman/.