Chesly

Read, search and answer your messages on WhatsApp, iMessage, Slack, Telegram and 14 more networks.

Hosted MCP Server

npx add-mcp 'https://mcp.chesly.app/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

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, LINE, Discord, Slack, LinkedIn, X, Reddit, Google Messages, Google Chat, Grok Bot, OpenAI Dots, Muse 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 inWhereBest for
CLInpm install -g cheslyAgents that run commands: Claude Code, Codex, Cursor, OpenClaw, Grok Bot, OpenAI Dots
MCPhttps://mcp.chesly.app/mcpClients that speak MCP: Claude, ChatGPT, Claude Code, Cursor, n8n, the OpenAI API
RESThttps://api.chesly.app/v1Your own code
EventsWebhooks, a live stream and a feedActing 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.
  2. Start the 7-day trial at 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.
npm install -g chesly
chesly login
  1. See what's new across your chats:
chesly catchup

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

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.

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, 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.

CommandWhat it does
chesly loginSign 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 logoutRevokes 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 whoamiThe key in use: its name, scope, API version, limits and usage
chesly keyThe key in use, masked. --reveal prints all of it, for MCP clients.
chesly accountsYour connected accounts and their status
chesly chatsYour 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 catchupUnread 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 watchStream new events as they happen. --chat, --account, --types and --networks narrow it; --after resumes after an event.
chesly webhookslist your webhooks, create <url> one, delete <id>, rotate <id> its secret, test <id> it, and see its deliveries <id>
chesly keyslist your keys, or revoke <key_id> one. New keys come from chesly login or the dashboard.
chesly usageYour 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:

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 codeMeaning
0Done
1An error
2A usage error: a missing argument or an unknown option, or a request the API refused as invalid_request (400)
3Not signed in: no key, a malformed one, or one the API refused (401)
4Not found (404), or a chat name that fits more than one chat
5Not 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.
6Payment 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.

MCP

The MCP server is at https://mcp.chesly.app/mcp and speaks Streamable HTTP. Chesly hosts it and it's always on: there's nothing to install or keep running on your computer, and it works from cloud agents as well as your own machine. It works with Claude, ChatGPT, 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.

There are two ways in:

  • Sign in with Chesly. Claude, ChatGPT, Claude Code and other clients that support MCP's OAuth sign in with your Chesly account, with nothing to paste. In Claude it's one click; in ChatGPT you add it as a custom MCP server (steps). The sign-in page shows who is asking and for what, and offers a plan if you don't have one. Apps you've signed in to are listed under Connected apps on the Keys page of your dashboard, where Disconnect signs them out right away. They use your plan and limits, and count toward your 100 keys.
  • A key. Anything else sends your key as a bearer token.

Its earlier address, https://api.chesly.app/mcp, keeps working, so a setup that uses it needs no change.

ToolWhat it does
list_accountsYour connected accounts and their status
list_chatsYour chats, filtered by unread, a name or a network
catch_upUnread chats with their latest messages, on every network or one
read_chatThe messages in a chat
search_messagesSearch every chat, one chat or one network
get_messageOne message, with its delivery status
send_messageSend a message in a chat you already have, once per idempotency_key
mark_readMark a chat as read
find_contactFind the people of your direct chats by name
transcribe_voice_noteTurn 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.

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, generated from the code that runs the API, and the REST reference lists every operation from it, with its parameters, answers and errors.

ResourceEndpoints
YouGET /v1/me: your key, plan, usage this period and limits
AccountsGET /v1/accounts
ChatsGET /v1/chats, GET /v1/chats/{chat_id}, POST /v1/chats/{chat_id}/read
MessagesGET /v1/chats/{chat_id}/messages, GET /v1/messages/{message_id}, GET /v1/messages/search?q=, GET /v1/messages?since=
Catch-upGET /v1/catch_up
SendingPOST /v1/chats/{chat_id}/messages, and POST /v1/media for a file
MediaGET /v1/media/{media_id}
ContactsGET /v1/contacts?q=: the people of your direct chats
Voice notesPOST /v1/messages/{message_id}/transcript
EventsGET /v1/events?after=, GET /v1/events/stream
WebhooksGET /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
KeysGET /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, line, discord, slack, linkedin, twitter (X), reddit, gmessages (Google Messages), googlechat, grokbot (Grok Bot), chatgptdots (OpenAI Dots), muse and email. tiktok, wechat and Snapchat are coming soon: only accounts that can connect them have chats there.
  • 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. 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.

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:

{
  "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.
  • 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.

A cursor older than your plan keeps events (after, or the stream's Last-Event-ID) answers 410 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.

EventWhen
message.receivedSomeone sends you a message
message.sentYou send a message, from anywhere
message.editedA message is edited
message.deletedA message is deleted
message.delivery_failedA message you sent through the API didn't go through
chat.created, chat.updated, chat.readA chat starts, changes or is read
account.connected, account.disconnected, account.needs_attentionAn account changes state
reaction.added, reaction.removedSomeone reacts to a message

Limits

Each plan has its own:

LimitPersonalPro
Requests, per key and for all your keys together15 a second, 900 a minute30 a second, 1,800 a minute
Webhooks25100
Transcription600 minutes a month3,000 minutes a month
Live event streams10 open at once25 open at once
Event history7 days30 days

Both plans share the rest:

LimitValue
API keysUp to 100
Searches, catch-ups and since-feed pages30 a minute each, per user
Sends on WhatsApp60 an hour and 250 a day, per account
Sends on every other network, email included30 an hour and 150 a day, per account
Text in one messageInstagram 1,000, Discord 2,000, Telegram 4,096, email 100,000 and every other network 10,000 UTF-16 code units
FilesOne per message, up to 50 MB (50,000,000 bytes)
Transcripts30 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.

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 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.
  • Personal includes Chesly Plus, with 10 connected accounts, up to 3 per network, and Pro includes Chesly Max, with 50, up to 10 per network, in the Chesly apps too.
  • You buy a plan on the web at 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:

{
  "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, 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.