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. 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. 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. 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 connectionsmessages:send— send messagesaudit:read— read a connection's message historybalance: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.
/connectionsconnections:read/connections/{connection_id}connections:readcurl 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.
/connections/{connection_id}/webhook-secret/rotateconnections:writeIssues a new webhook signing secret for the connection and returns it once — see Webhooks.
Messaging
/connections/{connection_id}/messagesmessages:sendSends 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 credits404— connection not found409— connection not connected429— 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
}
}
}channeltells you how to readfrom/chat— WhatsApp = phone numbers, Telegram = numeric ids.type: "connection"events carrydata.status(connected / disconnected / logged_out).- Received files are downloaded for you: fetch
data.media.url(no API key needed; it expires), orGET /media/{media_id}with a key that hasaudit:read. Files are kept 7 days. A caption arrives intext. - 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 awebhook_urlis set and returned once, aswebhook_secreton thatPOST /connectionsorPATCH /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. A503means 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/.