---
title: "WhatsMCP REST API — documentation"
description: "Call a real WhatsApp account over plain HTTP — base URL, API-key auth, the accounts and messages endpoints, cursor polling, errors, and the OpenAPI 3.1 reference."
url: "https://whatsmcp.com/docs/rest"
---

# 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:

```sh
curl -H "X-API-Key: $WAMCP_KEY" https://api.whatsmcp.com/v1/accounts
```

The REST API is the same workspace as the [MCP server](https://whatsmcp.com/docs/mcp): 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](https://api.whatsmcp.com/docs) |
| **OpenAPI spec** | [https://api.whatsmcp.com/openapi.json](https://api.whatsmcp.com/openapi.json) · [`.yaml`](https://api.whatsmcp.com/openapi.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:

```sh
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](https://app.whatsmcp.com/console/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`.

```sh
curl -H "X-API-Key: $WAMCP_KEY" https://api.whatsmcp.com/v1/accounts
```

```json
{
  "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` |

```sh
curl -H "X-API-Key: $WAMCP_KEY" \
  "https://api.whatsmcp.com/v1/messages?after_cursor=1041&limit=100"
```

```json
{
  "messages": [
    {
      "cursor": 1042,
      "account_id": "01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
      "direction": "inbound",
      "peer": "447700900456",
      "chat": "447700900456@s.whatsapp.net",
      "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](https://whatsmcp.com/docs/mcp#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](https://www.rfc-editor.org/rfc/rfc9457) problem documents, `Content-Type: application/problem+json`:

```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](https://api.whatsmcp.com/docs) |
| `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](https://whatsmcp.com/docs/mcp#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](https://whatsmcp.com/docs/mcp), with the same key. Each new endpoint appears in the [OpenAPI spec](https://api.whatsmcp.com/openapi.json) the moment it ships.
