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 spechttps://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"
    }
  ]
}
FieldMeaning
account_idThe id to pass to per-account calls
phoneThe account’s number, E.164 without the +
push_nameThe display name WhatsApp shows for it
statepairing, connected, disconnected, locked or unlinked
reasonWhy the account is locked, when it is
countryThe country the number belongs to — not where it connects from
egressHow the account actually reaches WhatsApp right now: direct, socks5 or wireguard
egress_configuredThe egress it is configured to use. If it differs from egress, a tunnel is set but not carrying traffic
egress_ipThe 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 parameterDefaultMeaning
after_cursor0Return only messages after this cursor. Omit or 0 to start from the beginning
limit50Page 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
}
FieldMeaning
cursorThis message’s position. Pass the highest one you have seen back as after_cursor
account_idWhich of your accounts it belongs to
directioninbound (received) or outbound (sent by you)
peerThe other party’s number. In a group or channel this is the individual sender, not the conversation
chatThe conversation JID — …@g.us for a group, …@newsletter for a channel
chat_kinddm, group, channel or broadcast
kind / textWhat 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_idRFC 3339 timestamp, and WhatsApp’s id for the message
send_errorSet on an outbound message that failed to send
next_cursorThe highest cursor in this page. On an empty page it is your after_cursor, unchanged, so you never lose your place
has_moretrue 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.

StatusreasonWhat to do
400invalid_requestFix the request — retrying it unchanged will not help
401—Missing, unknown or revoked key, or a suspended workspace. Plain-text body
403plan_requiredYour plan does not include this. Change the plan, not the request
404—No such endpoint — see the reference
409account_not_connected, account_lockedThe account cannot act right now (locked never recovers)
422—A parameter failed validation, e.g. limit=500; the body lists which
429quota_exceeded, throttledWait 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

RESTMCP tool
GET /v1/accountswa_list_accounts✅
GET /v1/messageswa_list_messages✅
Sending, chats, contacts, groups, channels, calls, webhooks36 toolsMCP 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.