For agents
Circles API and MCP documentation
This page is written for AI agents learning what Circles is and how to use it: the tools, the shape of every answer, how sign-in works, the HTTP API and every limit. People setting Circles up will find the guides at usecircles.app/en/docs.
Circles serves the WhatsApp chats a person opens to their agents through a remote MCP server at https://app.usecircles.app/api/mcp (OAuth 2.1 or a bearer token) and an HTTP API at https://app.usecircles.app/api/v1/agent (OpenAPI 3.1 at https://app.usecircles.app/api/openapi.json). There are 9 tools, every refusal is an answer that names what is open, and all times are UTC. As of October 2026.
Updated
Start here
You act for one person, inside the chats that person opened to agents. Nothing else in their WhatsApp exists for you.
- Know your world.
allowed_chatslists every chat open to you and whether you may send there. A chat missing from it cannot be read or searched. The one way past the list is a new chat: when the number's default is read and send,sendcan start a chat with someone new, by phone number or a saved contact's exact name (GET /recipients?to=over HTTP). - "What did I miss?" Call
activitywith asincetime first (counts only, cheap), thencatch_upfor the chats that moved. Voice notes arrive as text inmedia.transcript. - A refusal is an answer. It comes with
isError: falseand names what IS open or allowed. Pass it on to the person; do not retry it. - Sending speaks as the person. Show the person the exact text and the exact chat, and call
sendwithconfirmed: trueonly after they agree. A send is never retried: if delivery is uncertain, read the chat before deciding anything. - Times are UTC. Circles does not know where the person is; convert times for them yourself.
- Not connected yet? The person links WhatsApp and connects you by following the guides at usecircles.app/en/docs. Circles never asks you for their password or a WhatsApp code.
Endpoints
| What | Where |
|---|---|
| MCP server (HTTP, stateless) | https://app.usecircles.app/api/mcp |
| HTTP API | https://app.usecircles.app/api/v1/agent |
| OpenAPI 3.1 | https://app.usecircles.app/api/openapi.json; a readable reference at https://app.usecircles.app/api |
| OAuth metadata | https://app.usecircles.app/.well-known/oauth-protected-resource, https://app.usecircles.app/.well-known/oauth-authorization-server |
| MCP server card | https://usecircles.app/.well-known/mcp/server-card.json |
| This page as Markdown | https://usecircles.app/en/api-mcp-docs.md; every page of the site is listed in https://usecircles.app/llms.txt |
The nine MCP tools
Tools are listed per caller: send appears only with the chats:send scope and somewhere to send, get_media only with media:read. A tool a caller lacks answers exactly like a tool that does not exist.
allowed_chats: no parameters. The chats open to agents, each with name, kind, access (read only or read and send) and archived where set. This is the whole world: a chat missing here cannot be read, searched or sent to. Also carries connection (not_linked, subscription_ended) and subscription.ends_at when a cancelled plan is running out.
activity: since (ISO 8601, required). Which open chats moved: message count, last_at / last_at_iso, last_from. Counts only, no text. The cheap first call for "what did I miss?".
catch_up: since (required), chat, limit, per_chat, kind. Messages since a time, oldest first, senders by name. With chat: that chat, limit default 50, max 200, newest kept. Without it: every open chat that moved, newest chat first, per_chat default 20, max 50, within a total of 150; a chat with more says more. kind is voice, audio or recording (either). Attachments are announced only. Fetch one with get_media.
search: query (required), chat, days (1 to 36,500; default: all history kept), kind. Case-insensitive text search in one open chat or all of them, matching what was written and what voice notes and audio files say. Each hit names its chat when searching all.
people: since (required), limit (default 60, max 150), min_messages (default 3). Who the person talks with across open chats: direct-chat counts, shared groups, messages per group. It returns names only, with no numbers and no text.
group_members: chat (required). Members of one open group from WhatsApp's member list, including those who never wrote. Saved name, else the name they write under, else their WhatsApp name marked ~. Admins marked. At most 300 names; members with no known name are counted in unnamed. Never phone numbers.
documents: since (required). Every PDF, Word file and sheet sent in open chats: filename, date, the chats it was sent to (one entry per file), ref and state. Names and dates only.
get_media: ref (required). One attachment, base64 in bytes_base64, with mime, filename and size_bytes. Up to 50 MB per file. For an on_request file it asks the phone to re-upload and waits about 25 seconds. One ref per call. The agent reads the file itself. Circles does not run OCR, describe images or extract PDF text.
send: chat, text, confirmed (all required), file (mime, data_base64, filename), from. One message to a chat open for sending: text up to 65,536 characters, or a file up to about 3 MB with the text as its caption (up to 4,096). A JPEG, PNG or WebP goes as a photo, an MP4 or 3GP as a video, anything else as a named document. When the number's default is read and send, chat may be a phone number with its country code or a saved contact's exact name, to start a new chat; from picks the number when more than one could. Capped per day, never retried. confirmed is the agent's statement that the person agreed to this exact text in this exact chat; Circles cannot verify it and does not treat it as permission.
Answers, refusals and media
The envelope. Every tool answers with one text item. For an answer, content[0].text is JSON (pretty-printed). For a refusal, it is a plain sentence. Either way isError is false: read a refusal and pass it on. Do not retry it.
Refusals name what IS allowed. "No chat named X is open to agents. What IS open: A, B." or "X is open for reading only." A hidden chat is never named, so a refusal cannot tell an agent whether a chat exists. "No messages" and "you cannot see that chat" are never confused.
Coverage. When a request reaches further back than anything held, the answer carries coverage saying where the kept history starts and why, ending "Nothing was hidden from you." No coverage means nothing in the window is missing.
Times are UTC: at in milliseconds since 1970 and at_iso in ISO 8601 ending in Z (last_at / last_at_iso likewise). Circles does not know where the person is and converts nothing.
Voice notes. media.kind is voice for a WhatsApp voice note and audio for an audio file. Both carry transcript and transcript_state: pending (still being transcribed), done, empty (nothing was said) or failed; and duration_seconds where WhatsApp gave it. Transcription is automatic, Hebrew and English. A voice note older than about 25 days at linking is transcribed once it is fetched.
Attachment states. Every announced file has a state:
available:get_mediareturns it now.downloading: arriving by itself, usually within a minute. Ask again shortly.on_request: WhatsApp no longer has it;get_mediaasks the person's phone to re-upload it (the phone must be online) and waits about 25 seconds.fetching: the phone has not answered yet. It is NOT gone: ask again with the same ref in about a minute; a repeat joins the request already running.gone: the phone could not re-upload it. Say so; do not retry.too_large: over 50 MB, the limit for one call.failed: could not be downloaded yet. Nothing for the person to do.
Also in messages: reactions (emoji, who, when) once someone reacted, from 2026-10-27 onward. Edits in WhatsApp update the stored text; "delete for everyone" removes the message. Locations, contact cards and polls are kept as empty messages so a chat has no holes. Link previews and quoted-message context are not included.
A shortened example, with invented people:
// An answer: content[0].text is JSON, isError is false
{
"chat": "Oak Street parents",
"coverage": {
"days": 30,
"note": "This account keeps the last 30 days ... Nothing was hidden from you."
},
"messages": [
{
"id": "G7",
"from": "Dana",
"fromMe": false,
"at": 1792995120000,
"at_iso": "2026-10-26T06:12:00.000Z",
"body": "",
"media": {
"kind": "voice",
"duration_seconds": 41,
"transcript_state": "done",
"transcript": "The trip moved to Thursday, buses leave at 8:15."
}
},
{
"id": "G8",
"from": "Yossi",
"fromMe": false,
"at": 1792998180000,
"at_iso": "2026-10-26T07:03:00.000Z",
"body": "Permission slip attached",
"media": {
"kind": "document",
"filename": "trip-permission.pdf",
"mime": "application/pdf",
"size": "212 KB",
"state": "on_request",
"fetch_with": "get_media",
"ref": "r1_..."
}
}
]
}
// A refusal: content[0].text is the sentence itself, isError is still false
No chat named "Book club" is open to agents. What IS open: Oak Street parents, Building committee. Only the person can open another, in Circles.Auth: OAuth 2.1 and tokens
| Value | |
|---|---|
| Protected resource metadata | https://app.usecircles.app/.well-known/oauth-protected-resource |
| Authorization server metadata | https://app.usecircles.app/.well-known/oauth-authorization-server (issuer https://app.usecircles.app) |
| Endpoints | /oauth/authorize, /oauth/token, /oauth/register (dynamic client registration), /oauth/revoke |
| Flow | Authorization code with PKCE (S256 only). No implicit flow, no client secrets (token_endpoint_auth_methods_supported: none). |
| 401 | WWW-Authenticate: Bearer resource_metadata=... pointing at the protected resource metadata |
| Redirect URIs | https anywhere, or http on localhost, 127.0.0.1 or [::1]. Custom schemes are refused. |
| Scopes | chats:read (read the open chats), media:read (open their files), chats:send (send where the person allowed) |
| Consent | On Circles' own page. The client gets only what the person leaves ticked there, whatever it asked for. Every box starts ticked; the person unticks sending to connect a reader. |
| Lifetimes | Access token 1 hour. Refresh token 30 days, rotated on every use, with reuse detection. |
| Minted tokens | Made on the Agents page (New token). Prefix circ_, shown once, only a hash stored. Lifetime: no limit (default), 90 days or 1 year. No refresh. |
| One-time connect code | circ_code_..., exchanged at POST /api/v1/connect/exchange with {"code": "..."} and no Authorization. Single use, 1 hour, opens nothing by itself, hash only, 10 tries a minute per address. |
| Revoking | Disconnect on the Agents page revokes OAuth grants and minted tokens alike, at once |
Scope narrows; it never widens. A token holding every scope still reaches only the chats the person opened, because the chat levels are read again on every call.
HTTP API
The same operations as MCP, as JSON over HTTP, for agents and tools that call an API. OpenAPI 3.1 (version 2.0.0) at https://app.usecircles.app/api/openapi.json; a read-only reference at https://app.usecircles.app/api.
Every request carries Authorization: Bearer <key>: a minted circ_ key or an MCP client's OAuth token. Nothing else is accepted, no cookies.
- Reads:
GET https://app.usecircles.app/api/v1/agent/allowed_chats,/activity,/people,/catch_up,/search,/documents,/group_members, with the same parameters as the tools. - Details:
GET /chats/{chat_ref}andGET /messages/{message_ref}return what is allowed (capabilities, with reasons and notes) and completeactions. - Send:
POST /chats/{chat_ref}/messages(text, or a file up to about 3 MB with an optional caption). - Reply:
POST /messages/{message_ref}/replies, a native WhatsApp quoted reply. A reply that cannot be quoted is refused. - React:
POST /messages/{message_ref}/reactions, an emoji reaction. - New chat:
GET /recipients?to=with a phone number (country code, URL-encoded) or a saved contact's exact name; answers like a chat's details. - Write receipt:
GET /writes/{submission_ref}. - Files:
POST /get_media(may ask the phone for the file),GET /media/{ref}/status,/contentand/view(never ask the phone). - Connect code:
POST https://app.usecircles.app/api/v1/connect/exchange.
Acting. To send, reply or react, GET the chat's or message's details and perform one of its actions as given: a complete request (method, URL, headers, body) to which the agent adds only its Authorization header and the text.
Idempotent writes. Each write carries a server-issued, target-bound submission reference. Reusing it cannot send again or take another slot of the daily cap, for 24 hours. An uncertain delivery returns delivery_unknown and is never retried: read the receipt and the chat before making a new, deliberate submission.
Pagination. List answers carry pagination.next_url and has_more at the top level. Follow next_url until it is null; each page rechecks authorization. catch_up pages run from the newest messages back.
Errors. A 401 means the key no longer works. A 403 scope_required means the person did not grant that permission. An invalid argument names the field and its allowed range.
Not supported: poll voting, removing a reaction, quoting a message that is not text.
# Read: which chats moved since 06:00 UTC
curl -H "Authorization: Bearer circ_..." \
"https://app.usecircles.app/api/v1/agent/activity?since=2026-10-27T06:00:00Z"
# Act: GET a chat's details, then perform one of its actions as given
curl -H "Authorization: Bearer circ_..." "https://app.usecircles.app/api/v1/agent/chats/<chat_ref>"
# -> "actions": [{ "method": "POST", "url": ".../chats/<chat_ref>/messages",
# "headers": {...}, "body": { ... "submission_ref": "..." } }]
# An agent handed a one-time code (Instinct) swaps it for its key, no Authorization
curl -X POST -H "Content-Type: application/json" \
-d '{"code":"circ_code_..."}' https://app.usecircles.app/api/v1/connect/exchangeLimits
| What | Limit |
|---|---|
| Text per message | 65,536 characters (WhatsApp's own limit); a caption 4,096 |
| File to send | About 3 MB |
| File to fetch | 50 MB per file; larger is too_large |
| Sends per day | Default 5 per number across all agents; 5, 10, 20 or no cap; counted per UTC day |
catch_up, one chat | Default 50, max 200 messages |
catch_up, all chats | Default 20, max 50 per chat; 150 in total |
people | Default 60, max 150 people; min_messages default 3 |
group_members | At most 300 names |
search | Up to 36,500 days back, or the account's window |
get_media | One ref per call; waits about 25 seconds for a phone re-upload |
| Background file download | Within about 25 days of the message; older files on request |
| History | Agents see the last month on Free and the last 3 months on Starter; on Pro, full history, or the last 90, 60 or 30 days |
| Chats open to agents | 2 on Free; up to 5 on Starter; every chat you choose on Pro |
| Sending | Pro only |
| Voice-note transcription | Automatic, Hebrew and English |
| OAuth tokens | Access 1 hour, refresh 30 days |
| One-time connect code | Single use, 1 hour, 10 tries a minute per address |
| Write idempotency | 24 hours per submission reference |
| Sign-in link | Single use, 10 minutes |
| WhatsApp numbers | One per account, on every plan |
Changelog
Recent changes that an agent or integrator would notice.
- 2026-10-27: Instinct connects with a one-time code sent from WhatsApp, with no key shown or pasted.
- 2026-10-27: A message can be as long as WhatsApp allows: 65,536 characters.
- 2026-10-27: Reactions are kept on their message and shown in every read.
- 2026-10-27: Direct chats open for reading by default on a new number.
- 2026-10-27: HTTP API 2.0.0: browser agents removed; the agent API accepts a Bearer key only.
- 2026-10-26: A cancelled subscription keeps working to the end of the paid period, and
allowed_chatstells the agent the date. - 2026-10-23: Agents can message someone with no chat yet, by phone number or saved contact name.
- 2026-10-19: Agents can send a file of any type: a PDF, a document, a video or an image.
- 2026-10-19: Recordings:
kind=voice|audio|recordingfilters andduration_seconds; every time is stated as UTC. - 2026-10-19: Claude and Codex connect by address, with consent on Circles' page.
- 2026-10-18: Voice notes are transcribed automatically and agents read the transcript on the message.
- 2026-10-18: Full history is the default, for new and existing numbers.
- 2026-10-16: HTTP API with native replies and reactions; edits and deletions made in WhatsApp are applied.
Questions and answers
What is the Circles MCP server URL?
https://app.usecircles.app/api/mcp. It is a remote MCP server over HTTP that accepts OAuth 2.1 (with discovery and dynamic registration) or a bearer token you create in Circles.
Does Circles support streaming or SSE?
Circles answers each MCP request with one JSON response over HTTP POST, statelessly. There is no event stream to keep open, so any client that speaks MCP over HTTP can use it.
Why did my agent get a refusal instead of an error?
Circles returns refusals as answers (isError: false) so agents read them and do not retry. Each one names what IS open or allowed.
Why is a file "fetching"?
Circles asked the person's phone to re-upload a file WhatsApp no longer has. It is not gone: ask again with the same ref in about a minute, with the phone online.
Can I reply to or react to a message?
Yes, through the Circles HTTP API: native quoted replies and emoji reactions. The MCP tools send plain messages and files only.