# Chesly API docs

> The Chesly API: the CLI, the MCP server, REST, events, keys, limits and billing. Start a trial and make keys at https://web.chesly.app/api. Agent setup: https://agents.chesly.app/agents.md

## Overview

The Chesly API gives your code and your agents the chats you've connected in the Chesly app: WhatsApp, iMessage, Instagram, Messenger, Telegram, Signal, Discord, Slack, LinkedIn, X, Google Messages, Google Chat, Reddit and email. It's hosted and always on, so there's nothing to run on your side.

There are four ways in. All of them use the same key and see the same chats.

| Way in | Where | Best for |
|---|---|---|
| CLI | `npm install -g chesly` | Agents that run commands: Claude Code, Codex, Cursor, OpenClaw, Grok Bot, OpenAI Dots |
| MCP | `https://api.chesly.app/mcp` | Clients that speak MCP: the OpenAI API, n8n, Cursor, Claude Code |
| REST | `https://api.chesly.app/v1` | Your own code |
| Events | Webhooks, a live stream and a feed | Acting on new messages as they arrive |

What the API doesn't do:

- **Start new conversations.** Sends go to chats you already have.
- **Answer message requests.** You can read them, but a reply would accept the request, so the API won't send one.
- **Connect accounts.** You do that once, in the Chesly app on iPhone, Android or Mac.

## Quickstart

1. Connect your chats in the [Chesly app](https://chesly.app/download).
2. Start the 7-day trial at [web.chesly.app/api](https://web.chesly.app/api).
3. Install the CLI and sign in. `chesly login` prints a link, and opens it in your browser when it can. Approve it, and the key goes to your keychain.

```bash
npm install -g chesly
chesly login
```

4. See what's new across your chats:

```bash
chesly catchup
```

Or call the API yourself, with a key from the dashboard:

```bash
curl https://api.chesly.app/v1/catch_up \
  -H "Authorization: Bearer $CHESLY_API_KEY"
```

To hand all of this to an agent, copy its prompt from the [agent setup page](https://agents.chesly.app/agents).

## Keys

- A key is `ck_live_` followed by 48 characters. You see it once, when you make it.
- **Full access** keys can read and send. **Read-only** keys can read, search and watch, and can't send or change anything.
- Make keys in the dashboard at [web.chesly.app/api](https://web.chesly.app/api), or with `chesly login`. You can have up to 100, on either plan. `chesly keys list` lists them, and `chesly keys revoke` revokes one.
- Send a key as a bearer token: `Authorization: Bearer ck_live_...`. The CLI also reads `CHESLY_API_KEY`.
- A revoked key stops working on its next request. The dashboard shows when each key was last used.

## CLI

The CLI is built for agents first. Every command takes `--json` (JSON Lines for streams), nothing waits for input when there's no terminal, and exit codes are stable. `chesly --help` and each command's help are written so an agent can learn the tool from them.

You can name chats instead of passing IDs: `chesly read "Mom"`. When a name fits more than one chat, the CLI lists the matches. With `--json`, that's an error that carries the candidates.

| Command | What it does |
|---|---|
| `chesly login` | Sign in. It prints a link, opens it in your browser when it can, and waits while you approve. An agent whose tool shows a command's output only once it ends signs in in two steps: `--no-wait` prints the link and the code, then `--wait` waits for the approval. `--key -` stores a key you already have, read from standard input; never put a key on the command line. On servers and in CI, set `CHESLY_API_KEY` instead. |
| `chesly logout` | Revokes the key it stored, so it stops working everywhere, and removes it from this computer. When it can't revoke it, it still signs out, and names the key that's still active. |
| `chesly whoami` | The key in use: its name, scope, API version, limits and usage |
| `chesly key` | The key in use, masked. `--reveal` prints all of it, for MCP clients. |
| `chesly accounts` | Your connected accounts and their status |
| `chesly chats` | Your chats, most recent first. `--unread` for unread ones, `--account` for one account. |
| `chesly read <chat>` | A chat's latest messages |
| `chesly search <query>` | Search your chats, or one with `--chat` |
| `chesly catchup` | Unread chats with their latest messages, in one call |
| `chesly send <chat> <text>` | Send a message in a chat you already have, its text on standard input (below). `--reply-to` answers one message, `--file` attaches a file, and `--wait` waits for its delivery status. |
| `chesly watch` | Stream new events as they happen. `--chat`, `--account`, `--types` and `--networks` narrow it; `--after` resumes after an event. |
| `chesly webhooks` | `list` your webhooks, `create <url>` one, `delete <id>`, `rotate <id>` its secret, `test <id>` it, and see its `deliveries <id>` |
| `chesly keys` | `list` your keys, or `revoke <key_id>` one. New keys come from `chesly login` or the dashboard. |
| `chesly usage` | Your plan, this period's calls and transcription minutes, and each account's send limits |

Pass a message's text as `-` and a quoted heredoc. Inside double quotes the shell still expands `$` and backticks, so "It costs $5" would arrive as "It costs "; the quotes around `EOF` keep the text exactly as written:

```bash
chesly send "Mom" - <<'EOF'
Landed safe! Calling you tonight
EOF
```

`chats`, `catchup` and `search` take `--network <name>` to keep to one network, and `read` and `send` take it to look a chat name up on that network. Email isn't in the default lists: add `--network email`, as in `chesly catchup --network email`.

| Exit code | Meaning |
|---|---|
| `0` | Done |
| `1` | An error |
| `2` | A usage error: a missing argument or an unknown option, or a request the API refused as `invalid_request` (400) |
| `3` | Not signed in: no key, a malformed one, or one the API refused (401) |
| `4` | Not found (404), or a chat name that fits more than one chat |
| `5` | Not final, come back to it: a limit (429), the API or a gateway briefly away (502, 503 or 504), a first attempt still running (409 `idempotency_in_progress`), or a send the network didn't confirm (`delivery_unconfirmed`: check the chat before you send it again). `retry_after`, when there is one, says when. |
| `6` | Payment required (402): no API plan, or one of its limits reached |

In JSON mode, errors go to standard error as one line of problem details, with the same `type` links as the API's [errors](https://agents.chesly.app/docs/errors).

## MCP

The MCP server is at `https://api.chesly.app/mcp` and speaks Streamable HTTP. Authenticate with your key as a bearer token. It works with Claude Code, the Claude Agent SDK, Codex, Cursor, OpenClaw, n8n and OpenAI's Responses and Agents APIs. Setup for each is on the [agent setup page](https://agents.chesly.app/agents).

| Tool | What it does |
|---|---|
| `list_accounts` | Your connected accounts and their status |
| `list_chats` | Your chats, filtered by unread, a name or a network |
| `catch_up` | Unread chats with their latest messages, on every network or one |
| `read_chat` | The messages in a chat |
| `search_messages` | Search every chat, one chat or one network |
| `get_message` | One message, with its delivery status |
| `send_message` | Send a message in a chat you already have, once per `idempotency_key` |
| `mark_read` | Mark a chat as read |
| `find_contact` | Find the people of your direct chats by name |
| `transcribe_voice_note` | Turn a voice note into text |

A read-only key sees 7 of them: no `send_message`, `mark_read` or `transcribe_voice_note`.

Tools answer with the REST API's JSON, with chat and sender names filled in. What people wrote stays inside each message's `text`, so your agent can tell a message apart from the tool's own words. Email stays out of the lists unless a tool's `network` is `email`. A refused call is a tool error whose text starts with its code, such as `chat_not_found: …`; the codes are on the [errors page](https://agents.chesly.app/docs/errors).

During each Chesly deploy, MCP calls can answer 502 for about 15 to 25 seconds. Retry a call that fails that way after a few seconds.

## REST API

The base URL is `https://api.chesly.app/v1`. The OpenAPI 3.1 document is at [api.chesly.app/v1/openapi.json](https://api.chesly.app/v1/openapi.json), generated from the code that runs the API, and the [REST reference](https://agents.chesly.app/docs/api) lists every operation from it, with its parameters, answers and errors.

| Resource | Endpoints |
|---|---|
| You | `GET /v1/me`: your key, plan, usage this period and limits |
| Accounts | `GET /v1/accounts` |
| Chats | `GET /v1/chats`, `GET /v1/chats/{chat_id}`, `POST /v1/chats/{chat_id}/read` |
| Messages | `GET /v1/chats/{chat_id}/messages`, `GET /v1/messages/{message_id}`, `GET /v1/messages/search?q=`, `GET /v1/messages?since=` |
| Catch-up | `GET /v1/catch_up` |
| Sending | `POST /v1/chats/{chat_id}/messages`, and `POST /v1/media` for a file |
| Media | `GET /v1/media/{media_id}` |
| Contacts | `GET /v1/contacts?q=`: the people of your direct chats |
| Voice notes | `POST /v1/messages/{message_id}/transcript` |
| Events | `GET /v1/events?after=`, `GET /v1/events/stream` |
| Webhooks | `GET /v1/webhooks`, `POST /v1/webhooks`, `GET /v1/webhooks/{id}`, `PATCH /v1/webhooks/{id}`, `DELETE /v1/webhooks/{id}`, `POST /v1/webhooks/{id}/rotate_secret`, `POST /v1/webhooks/{id}/test`, `GET /v1/webhooks/{id}/deliveries` |
| Keys | `GET /v1/keys`, `DELETE /v1/keys/{key_id}` |

How it behaves:

- **IDs** are opaque and prefixed: `acct_`, `chat_`, `msg_`, `media_`, `evt_`, `wh_`, `dlv_` and `contact_`. Use them as they come: a message ID is long, because it carries its chat.
- **Lists** page with cursors. Pass `limit` (1 to 100, 50 by default) and the `next_cursor` of the page before. `GET /v1/messages?since=` takes up to 200 a page.
- **Filters.** `GET /v1/chats` takes `unread`, `archived`, `account_id`, `network`, `q`, `sort` and `updated_since`, and a chat's messages take `before` and `after`. Times are ISO 8601 with an offset, such as `2026-10-27T09:30:00Z`. Unknown query parameters are refused, so a misspelt filter can't widen the answer.
- **Networks** are named `whatsapp`, `imessage`, `instagram`, `messenger`, `telegram`, `signal`, `discord`, `slack`, `linkedin`, `twitter` (X), `gmessages` (Google Messages), `googlechat`, `reddit` and `email`.
- **Email** stays out of chat lists, search, the feed, catch-up and events unless you ask for it with `network=email` (for events, `networks` with `email` in it), or name an email account's `account_id`. An email chat or message is always reachable by its ID. An email reaches the since feed (`GET /v1/messages?since=`) up to 10 seconds after it arrives.
- **Idempotency.** Every POST takes an `Idempotency-Key` header: 1 to 255 characters from `A-Za-z0-9._:-`, such as a UUID (the quoted form `"…"` works too), kept for 24 hours. `POST /v1/media` ignores it, as an upload changes nothing on its own. Repeat a request with the same key and you get the first answer back, with `Idempotent-Replayed: true`. The same key with a different request is a `422`, and while the first request is still running it's a `409` with `Retry-After: 1`. An answer of `429` or `5xx` isn't kept, so a retry with the same key runs again, and a send still goes out only once.
- **Versions.** Every answer carries `Chesly-Version`, the API version your key was made on: `2026-11-01`. Changes within a version only add fields and endpoints.
- **Rate limits** come back in the `RateLimit` and `RateLimit-Policy` headers.

### Sending

A send goes to a chat you already have, with `text`, a `reply_to` message ID from the same chat, a `media_id` from `POST /v1/media`, or text and a file together. A message takes one file, up to 50 MB (50,000,000 bytes). An upload has 60 seconds, and must keep up 50 KB a second once its first 10 are over; a slower one is `408` [`upload_timeout`](https://agents.chesly.app/docs/errors#upload_timeout). In a Google Messages RCS chat, which takes no captions, the text follows the file as a message of its own. It has no side effects beyond the message: no typing, no presence and no read receipt.

```bash
curl https://api.chesly.app/v1/chats/chat_x1R7KTsNtH88aWCJvUyo/messages \
  -H "Authorization: Bearer $CHESLY_API_KEY" \
  -H "Idempotency-Key: 8d4faeef-90bb-4b3a-8760-de2fde14c267" \
  -H "Content-Type: application/json" \
  -d '{"text": "Landed safe! Calling you tonight"}'
```

The answer, `201`, is the message, with its delivery status `queued`:

```json
{
  "id": "msg_x1R7KTsNtH88aWCJvUyo.JFp5TUlnekJnT3RVZ2RMdHgtbTZwLWNWWGtyazNSamdfRXVPOUNuR3dJdDg",
  "chat_id": "chat_x1R7KTsNtH88aWCJvUyo",
  "sender": { "name": "Sam Reyes", "is_me": true },
  "text": "Landed safe! Calling you tonight",
  "truncated": false,
  "type": "text",
  "media": null,
  "sent_at": "2026-10-27T09:52:31.204Z",
  "edited": false,
  "deleted": false,
  "reply_to": null,
  "delivery": {
    "status": "queued",
    "error": null,
    "updated_at": "2026-10-27T09:52:31.236Z"
  }
}
```

`delivery.status` moves on to `sent`, and to `delivered` only where the network reports it (iMessage does; elsewhere a send ends at `sent`), or to `failed` with a `delivery.error` whose `code` says why. `network_rejected`, `not_logged_in`, `rate_limited_by_network` and `unsupported` are certain (`certain: true`): the message didn't go out. `timeout` and `unknown` aren't (`certain: false`): the message may have gone out anyway, so check the chat before you send it again. If such a failure turns out certain later, a second `message.delivery_failed` for the same message follows, with `certain: true`. `GET /v1/messages/{message_id}` shows where a message is, and each failure is also a `message.delivery_failed` event. Messages you sent through the API in the last 30 days carry their `delivery`; every other message has `delivery: null`.

## Events

Every change lands in one log, kept for 7 days on Personal and 30 on Pro, and you can take it three ways:

- **Webhooks**: Chesly posts each event to your URL, signed under the Standard Webhooks spec and retried for about 28 hours. See [Webhooks](https://agents.chesly.app/docs/webhooks).
- **A live stream** at `GET /v1/events/stream`, as server-sent events. Resume with `Last-Event-ID`; a ping every 15 seconds keeps it open. You can keep 10 open at once on Personal and 25 on Pro. `chesly watch` reads it.
- **A feed** at `GET /v1/events?after=`, oldest first, for polling.

None of them count as calls. An event is `{ id, type, timestamp, data }`, and the same event can arrive twice, so skip an `id` you've seen. Email events come only when you ask for them, with `networks` including `email`. Each type's `data` is on the [events page](https://agents.chesly.app/docs/events).

A cursor older than your plan keeps events (`after`, or the stream's `Last-Event-ID`) answers `410` [`events_expired`](https://agents.chesly.app/docs/errors#events_expired): start again without one, and catch up with `GET /v1/catch_up`. `chesly watch` does that by itself, and starts from new events with a warning.

| Event | When |
|---|---|
| `message.received` | Someone sends you a message |
| `message.sent` | You send a message, from anywhere |
| `message.edited` | A message is edited |
| `message.deleted` | A message is deleted |
| `message.delivery_failed` | A message you sent through the API didn't go through |
| `chat.created`, `chat.updated`, `chat.read` | A chat starts, changes or is read |
| `account.connected`, `account.disconnected`, `account.needs_attention` | An account changes state |
| `reaction.added`, `reaction.removed` | Someone reacts to a message |

## Limits

Each plan has its own:

| Limit | Personal | Pro |
|---|---|---|
| Requests, per key and for all your keys together | 15 a second, 900 a minute | 30 a second, 1,800 a minute |
| Webhooks | 25 | 100 |
| Transcription | 600 minutes a month | 3,000 minutes a month |
| Live event streams | 10 open at once | 25 open at once |
| Event history | 7 days | 30 days |

Both plans share the rest:

| Limit | Value |
|---|---|
| API keys | Up to 100 |
| Searches, catch-ups and since-feed pages | 30 a minute each, per user |
| Sends on WhatsApp | 60 an hour and 250 a day, per account |
| Sends on every other network, email included | 30 an hour and 150 a day, per account |
| Text in one message | Instagram 1,000, Discord 2,000, Telegram 4,096, email 100,000 and every other network 10,000 UTF-16 code units |
| Files | One per message, up to 50 MB (50,000,000 bytes) |
| Transcripts | 30 a minute, one at a time per user |

Until billing starts, every key has Personal's limits, and transcription is capped at 60 minutes of audio a day per user instead, until midnight UTC.

Past a limit, you get a `429` with a `Retry-After` header. `GET /v1/me` shows where you stand, account by account. More keys keep agents apart; they never add requests.

Send limits exist because messages leave through shared servers: one script's burst can get a network to flag those servers, and every account on them. Automation can also get a LinkedIn account restricted. The limits sit above what a busy person sends by hand. If you need more, [tell us](https://chesly.app/contact).

## Billing

- **Personal**, $49 a month: 600 transcription minutes a month, 15 requests a second and 900 a minute, 25 webhooks, 10 live event streams, 7 days of event history and email support.
- **Pro**, $199 a month: 3,000 transcription minutes a month, 30 requests a second and 1,800 a minute, 100 webhooks, 25 live event streams, 30 days of event history and priority email support.
- **Both** have everything else: reads, sends, events, the live stream, webhooks, the CLI, MCP, every account you've connected and up to 100 API keys.
- **No meter.** Calls are counted for the dashboard and `GET /v1/me`, never billed.
- **Transcription** uses the plan's minutes. Past them, transcripts answer `402` [`transcription_limit_reached`](https://agents.chesly.app/docs/errors#transcription_limit_reached) until the next month, and everything else keeps working. A note is checked before it's transcribed, so one note can run past the line.
- **The trial** lasts 7 days on the plan you choose, with its limits and minutes, and needs a card. There's one trial.
- **Switching** between Personal and Pro happens in the dashboard. An upgrade applies at once, prorated; a downgrade applies at the end of your billing month, with no credit. Until then, `GET /v1/me` shows it as `plan.scheduled_change`, and you can call it off in the dashboard. Switching down deletes nothing: past the new plan's limit, new webhooks are refused.
- **Every plan includes Chesly Plus**, in the apps too: Personal with 10 connected accounts, up to 3 per network, and Pro with 50, up to 10 per network.
- You buy a plan on the web at [web.chesly.app/api](https://web.chesly.app/api), never in the Chesly apps.

## Errors

Every error is an RFC 9457 problem, with a stable `code` you can branch on:

```json
{
  "type": "https://agents.chesly.app/docs/errors#rate_limited",
  "title": "Too many requests.",
  "status": 429,
  "detail": "This API key made more than 600 requests in one minute. Retry in 4 s.",
  "code": "rate_limited",
  "request_id": "571d003e-ad60-4a08-858a-b7b5353f39db",
  "retry_after": 4
}
```

`type` links to the code on the [errors page](https://agents.chesly.app/docs/errors), which lists every code the API and the CLI answer with, and what to do about each. Include the `request_id` when you write to us.
