REST API documentation
Quick start
1. Create a workspace → https://app.whatsmcp.com/console/register
2. Link a WhatsApp number → Console → Pair device (scan the QR with your phone)
3. Create an API key → https://app.whatsmcp.com/console/keys
Then:
curl -H "X-API-Key: $WAMCP_KEY" https://api.whatsmcp.com/v1/accounts
The REST API is the same workspace as the MCP server: the same accounts, the same plan, the same API keys. Use MCP for AI agents; use REST for scripts, backends, CRMs and anything else that speaks HTTP.
Base URL
https://api.whatsmcp.com
Every endpoint is under /v1. Requests and responses are JSON over HTTPS.
The contract is published as an OpenAPI 3.1 document, generated from the code that serves it, so it cannot drift:
| Interactive reference (Swagger UI) | https://api.whatsmcp.com/docs |
| OpenAPI spec | https://api.whatsmcp.com/openapi.json · .yaml |
In the interactive reference, Authorize with your key and every endpoint gets a Try it out button that calls your real workspace.
Authentication
Every /v1 request carries an API key, in either header:
curl -H "Authorization: Bearer $WAMCP_KEY" https://api.whatsmcp.com/v1/accounts
curl -H "X-API-Key: $WAMCP_KEY" https://api.whatsmcp.com/v1/accounts
Create keys at Console → API keys. They look like
wamcp_live_XXXXXXXXXXXX_… and are shown exactly once — we store only a hash, so a lost key
is replaced, never recovered. Mint one key per service or environment and revoke individually.
The key is the workspace. There is no tenant parameter anywhere, and no request can reach another workspace’s accounts or messages.
A missing, unknown or revoked key — or a suspended workspace — is answered 401.
Accounts
GET /v1/accounts
The WhatsApp accounts in your workspace. Every per-account call needs an account_id from
here, and an account can act only while its state is connected.
curl -H "X-API-Key: $WAMCP_KEY" https://api.whatsmcp.com/v1/accounts
{
"accounts": [
{
"account_id": "01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
"phone": "447700900123",
"push_name": "Support",
"state": "connected",
"country": "GB",
"egress": "wireguard",
"egress_configured": "wireguard",
"egress_ip": "203.0.113.24"
}
]
}
| Field | Meaning |
|---|---|
account_id | The id to pass to per-account calls |
phone | The account’s number, E.164 without the + |
push_name | The display name WhatsApp shows for it |
state | pairing, connected, disconnected, locked or unlinked |
reason | Why the account is locked, when it is |
country | The country the number belongs to — not where it connects from |
egress | How the account actually reaches WhatsApp right now: direct, socks5 or wireguard |
egress_configured | The egress it is configured to use. If it differs from egress, a tunnel is set but not carrying traffic |
egress_ip | The public IP its traffic leaves from |
Fields that do not apply are omitted rather than sent empty.
Messages
GET /v1/messages
Messages across all your accounts, oldest first, with a cursor.
| Query parameter | Default | Meaning |
|---|---|---|
after_cursor | 0 | Return only messages after this cursor. Omit or 0 to start from the beginning |
limit | 50 | Page size, 1–200 |
curl -H "X-API-Key: $WAMCP_KEY" \
"https://api.whatsmcp.com/v1/messages?after_cursor=1041&limit=100"
{
"messages": [
{
"cursor": 1042,
"account_id": "01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
"direction": "inbound",
"peer": "447700900456",
"chat": "[email protected]",
"chat_kind": "dm",
"text": "Hi, is my order on its way?",
"kind": "text",
"at": "2026-09-25T09:14:03Z",
"message_id": "3EB0C767D26A1D5F8C21"
}
],
"next_cursor": 1042,
"has_more": false
}
| Field | Meaning |
|---|---|
cursor | This message’s position. Pass the highest one you have seen back as after_cursor |
account_id | Which of your accounts it belongs to |
direction | inbound (received) or outbound (sent by you) |
peer | The other party’s number. In a group or channel this is the individual sender, not the conversation |
chat | The conversation JID — …@g.us for a group, …@newsletter for a channel |
chat_kind | dm, group, channel or broadcast |
kind / text | What the message is (text, image, video, audio, document, sticker, reaction, location, contact, poll, …) and its body. Text, captions and template text carry a body |
at, message_id | RFC 3339 timestamp, and WhatsApp’s id for the message |
send_error | Set on an outbound message that failed to send |
next_cursor | The highest cursor in this page. On an empty page it is your after_cursor, unchanged, so you never lose your place |
has_more | true when the page was full and more may be waiting — fetch again straight away |
Reading incoming messages
The API never pushes: you read on your own schedule, and the cursor is what makes that reliable — no gaps and no duplicates, even for two messages in the same millisecond, which a timestamp cannot promise.
after = 0 # or the cursor you stored last time
repeat:
r = GET /v1/messages?after_cursor={after}&limit=200
for m in r.messages:
if m.direction == "inbound":
handle(m)
after = r.next_cursor # persist it — it is your bookmark
if not r.has_more:
sleep(a few seconds) # caught up; poll again later
Store next_cursor durably and a restart resumes exactly where it stopped.
If you would rather be called than poll, the MCP server’s webhooks deliver each inbound message to your HTTPS endpoint — they are configured per workspace, so they fire for REST users too.
Errors
Errors are RFC 9457 problem documents,
Content-Type: application/problem+json:
{
"status": 403,
"title": "Forbidden",
"detail": "wa_list_messages is not included in your plan …",
"reason": "plan_required"
}
reason is a stable code you can branch on; detail is for humans and may change. When
waiting helps, retry_after_seconds is set and the response carries a Retry-After header —
when it is absent, retrying the same call will not succeed.
| Status | reason | What to do |
|---|---|---|
400 | invalid_request | Fix the request — retrying it unchanged will not help |
401 | — | Missing, unknown or revoked key, or a suspended workspace. Plain-text body |
403 | plan_required | Your plan does not include this. Change the plan, not the request |
404 | — | No such endpoint — see the reference |
409 | account_not_connected, account_locked | The account cannot act right now (locked never recovers) |
422 | — | A parameter failed validation, e.g. limit=500; the body lists which |
429 | quota_exceeded, throttled | Wait Retry-After seconds, then retry |
500 | — | Our fault. The detail carries a ref — quote it to support |
Each endpoint is gated by your plan exactly as its MCP twin is: GET /v1/messages needs what
wa_list_messages needs, and so on.
What’s available
| REST | MCP tool | |
|---|---|---|
GET /v1/accounts | wa_list_accounts | ✅ |
GET /v1/messages | wa_list_messages | ✅ |
| Sending, chats, contacts, groups, channels, calls, webhooks | 36 tools | MCP only, for now |
The REST API is new and grows one endpoint at a time; everything else is available today through the MCP server, with the same key. Each new endpoint appears in the OpenAPI spec the moment it ships.