How to connect Chatterfy API to your AI
Connecting the API to your AI means giving Claude, Cursor, ChatGPT, or another assistant an instruction and a key so it can send requests to Chatterfy on your behalf. You write 'show me who hasn't bee
Connecting the API to your AI means giving Claude, Cursor, ChatGPT, or another assistant an instruction and a key so it can send requests to Chatterfy on your behalf. You write 'show me who hasn't been replied to since yesterday' — the AI finds the chats and sends a list.
For this, there is a ready-made instruction for AI — a skill. In this article, we cover what the AI will be able to do, where to get the skill, how to connect it to different AIs, and how to securely pass the key.
What the AI will be able to do
Write in plain language, as you would to an employee. For example:
- 'Show me chats where the client hasn't been replied to';
- 'What did Anna write in her last messages?';
- 'Reply to Anna that delivery is tomorrow by 6:00 PM' — the AI will show the text and send it only after your 'yes';
- 'Add the tag Paid to everyone who has a purchase event';
- 'How many new chats were there this week?';
- 'Which operators are answering the slowest in October?';
- 'Export all chats for October to a spreadsheet'.
The AI can do the same as the API: read and search chats, reply, change statuses, tags, and fields, export data, and view analytics. What the API can't do, neither can the AI — more details in the article Chatterfy API: What It Is and How to Get Started.
What you will need
- An API key — How to get an API key. The key's permissions are the AI's permissions. For reading and replying, the Space controller role is sufficient; for analytics, you need Space manager — API Key Permissions: Which Role to Assign.
- An AI that can send requests to the internet: Claude Code, Cursor, Claude with code execution and internet access. ChatGPT in a standard chat does not send requests — more details below.
Copy the skill
A skill is a single text file
SKILL.md containing instructions: how the API is structured, in what order to make requests, how to handle errors, and what cannot be done without your confirmation.Create a folder and a file
Create a folder namedchatterfy-api, and inside it, a text fileSKILL.md.Copy the skill text
Click the copy button in the top right corner of the block, paste the text intoSKILL.md, and save the file.chatterfy-api/SKILL.mdmarkdown--- name: chatterfy-api description: Work with a Chatterfy workspace through the Chatterfy Partners API — find bots, read and search chats and messages, reply to leads, change chat status, tags, assignee and custom fields, export chats, read bot analytics and Tracker campaigns, domains, landings and leads. Use when the user mentions Chatterfy, a Chatterfy bot, chats, leads, operators, or gives an API key that starts with cfy_. --- # Chatterfy Partners API You act on the user's Chatterfy workspace over HTTPS on their behalf. Everything you can do is limited by the rights of the API key the user gave you; the API itself refuses anything else. Answer the user in the language they write in. ## 1. Before the first call 1. You need an API key. It looks like `cfy_prod_<uuid>_<secret>`. Take it from the environment variable `CHATTERFY_API_KEY` if it is set; otherwise ask the user for it once. 2. Never print the key back, never put it into a URL, a file the user did not ask for, or a log line. If you show a command, write `$CHATTERFY_API_KEY` instead of the key. 3. You need a way to send HTTP requests: a terminal (`curl`), Python (`requests`/`urllib`), or another HTTP tool. If you have none, say so and give the user the exact `curl` command to run. 4. Check the key with `POST /companies/v1/search` and an empty body `{}`. A `200` with a non-empty `data` array means the key works. ## 2. Request basics - Base URL: `https://api.chatterfy.ai/public-api` - Auth header: `ApiKey: <key>`. Not `Authorization: Bearer`. - Always send `Content-Type: application/json` with a JSON body. - The version sits after the resource name: `/chats/v1/search`, `/bots/v1/{bot_id}`. - Lists are read with `POST .../search` and a JSON body, not with `GET`. - Ids are UUIDs, except lead ids and campaign source ids, which are short strings. Never invent an id — always take it from a previous response. - Times are RFC 3339 in UTC, e.g. `2026-10-30T12:00:00Z`. - Single objects come back as they are. Lists come back as `{"data": [...], "pagination": {"has_more": true, "next_cursor": "..."}}`. A delete returns `{"id": "...", "deleted": true}`. - `PATCH` bodies are partial: send only the fields you change. An empty `{}` is rejected with `400 no_fields`. - Body limit 64 KB. ```bash curl -s -X POST "https://api.chatterfy.ai/public-api/companies/v1/search" \ -H "ApiKey: $CHATTERFY_API_KEY" -H "Content-Type: application/json" -d '{}' ``` ```python import os, requests BASE = "https://api.chatterfy.ai/public-api" HEADERS = {"ApiKey": os.environ["CHATTERFY_API_KEY"], "Content-Type": "application/json"} def call(method, path, body=None, params=None): response = requests.request(method, BASE + path, headers=HEADERS, json=body, params=params, timeout=30) if response.status_code >= 400: raise RuntimeError(f"{response.status_code} {response.text}") return response.json() companies = call("POST", "/companies/v1/search", {}) ``` ## 3. How to find things: always top-down Company → bot → chat → message. Each step needs the id from the previous one. 1. `POST /companies/v1/search` `{}` → pick `company_id`. Usually there is exactly one. 2. `POST /bots/v1/search` `{"company_id": "...", "limit": 100}` → pick `bot_id` by `name`. `query` filters by name substring. 3. `POST /chats/v1/search` `{"bot_id": "...", "limit": 50}` → chats, newest activity first. 4. `POST /messages/v1/search` `{"chat_id": "...", "limit": 50}` → messages of one chat. When the user names a bot or a person, resolve the name to an id yourself through these lists. If two objects match, ask which one. ## 4. Pagination - `limit` default 25, max 100. - Copy `pagination.next_cursor` into `cursor` of the next request with the same body. Stop when `has_more` is `false`. Never build a cursor yourself. - Chat search stops at 10 000 rows (`400 cursor_out_of_range`). For more, use `POST /chats/v1/export` (section 7). - Chat order changes while you read (a chat with a new message jumps to the top). Deduplicate by `id`. - Messages: `order` is `asc` or `desc`; `type` is `message` (real messages) or `event` (system events such as tag or status changes). ## 5. Rights and why a call fails The key belongs to a service account. Its company role and its role in each space decide what it can do. Companies, spaces and bots return an `access` map such as `{"chat.read": "all", "message.send": "all"}`: a permission missing from the map is not granted. Breadth `own` means only chats assigned to the account, `own_unassigned` adds unassigned chats, and `assigned` depends on the bot's assignment setting (all, own plus unassigned, or own only); narrower lists return fewer rows instead of failing. | Status | Meaning | What you do | | --- | --- | --- | | 400 | bad request: wrong field, wrong enum, broken cursor, invalid filter | read `error.message`, fix the body, retry once | | 401 | key missing, malformed or revoked | stop, ask the user for a valid key | | 403 `access_denied` | the object exists but this key may not do that: the role lacks the permission, the object is in another company or in a space the account is not in, or the chat is not assigned to it | stop, tell the user which action failed; an admin must add the service account to the space or give it a role with that right | | 404 | no object with that id — a typo or an id of the wrong kind | re-check the id, take ids only from API responses; do not retry blindly | | 409 | state conflict, e.g. deleting a space that still has bots | explain, do not force | | 429 | rate limit (120 requests per minute per key by default) | wait `Retry-After` seconds, then continue | | 503 | a dependency is down | wait a few seconds, retry up to 3 times | Errors look like `{"error": {"type": "...", "code": "...", "message": "...", "request_id": "req_..."}}`. Give the user `request_id` when something breaks — support finds the call by it. ## 6. Safety rules - Reading is free. Anything that changes data — sending, editing or deleting a message, changing a chat, deleting a chat, tags, spaces, domains, campaigns — needs the user's explicit confirmation first. Show exactly what you will send and to which chat. - Sending a message reaches a real person in Telegram. There are no idempotency keys: if a send times out, read the chat's last messages before retrying, or the lead gets the text twice. - Deleting a chat removes it with all messages. There is no undo. - Bulk actions (messaging many chats): confirm the list size first, go one by one, stay under the rate limit, stop on the first unexpected error. - Without `chat.contacts.read` names and usernames of leads come back masked. That is intended; do not try to work around it. ## 7. Endpoints Prefix every path with the base URL. `{x}` is an id from an earlier response. ### Companies, spaces, members | Call | Body / query | Needs | | --- | --- | --- | | `POST /companies/v1/search` | `{}` | — | | `GET /companies/v1/{company_id}` | | `company.read` | | `PATCH /companies/v1/{company_id}` | `name` | `company.manage` | | `POST /companies/v1/{company_id}/members/search` | `limit`, `cursor` | company member; full list needs `company.member.read`, otherwise only yourself | | `POST /spaces/v1/search` | `company_id`, `limit`, `cursor` | company member | | `POST /spaces/v1` | `company_id`, `name`, `description` | `space.create` | | `GET` / `PATCH` / `DELETE /spaces/v1/{space_id}` | PATCH: `name`, `description` | `space.read` / `space.manage` / `space.delete` | | `POST /spaces/v1/{space_id}/members/search` | no body | `space.read`; returns `{"data": {"accounts": [...], "service_accounts": [...]}}` | Use the space members list to turn `assign_id` of a chat, `sender` of a message or `operator_id` of analytics into a person's name. ### Bots | Call | Body / query | Needs | | --- | --- | --- | | `POST /bots/v1/search` | `company_id` (required), `status` (`active`/`stopped`), `type`, `query`, `limit`, `cursor` | company member | | `GET /bots/v1/{bot_id}` | | `bot.read` | | `PATCH /bots/v1/{bot_id}` | `name`, `description`, `status`, `timezone`, `image_url` | `bot.manage` | | `GET /bots/v1/{bot_id}/tags` | query `limit`, `cursor` | `bot.read` | | `POST /bots/v1/{bot_id}/tags` | `name`, `color` like `#1A2B3C` | `bot.settings.manage` | | `DELETE /bots/v1/{bot_id}/tags/{tag_id}` | removes the tag from every chat too | `bot.settings.manage` | | `GET /bots/v1/{bot_id}/custom-fields` | query `limit`, `cursor` | `bot.read` | ### Chats | Call | Body | Needs | | --- | --- | --- | | `POST /chats/v1/search` | `bot_id` (required), `filter`, `sort` (`asc`/`desc` by last message time), `limit`, `cursor` | `chat.read` | | `POST /chats/v1/stats` | `bot_id`, `filter` → counts per status and `total` | `chat.read` | | `POST /chats/v1/export` | same as search; no 10 000 limit, no duplicates; the cursor lives 5 minutes, so page without pauses; on `400 cursor_expired` start over without a cursor | `chat.export` | | `GET /chats/v1/{chat_id}` | fresh state straight from the messenger | `chat.read` | | `PATCH /chats/v1/{chat_id}` | `status`, `assign_id`, `tags`, `custom_fields` | `chat.manage` | | `DELETE /chats/v1/{chat_id}` | deletes with all messages | `chat.delete` | `PATCH /chats/v1/{chat_id}` fields: - `status`: `manual` (an operator handles the chat), `ai` (the bot and AI handle it), `blocked`, `start`, `ignore`, `stopped`. - `assign_id`: id of a member of the bot's space (from the space members list). - `tags`: `{"assign": ["<tag id>"], "remove": ["<tag id>"]}` — ids from `GET /bots/v1/{bot_id}/tags`, not names. - `custom_fields`: `{"<field id>": "<value>"}` — the key is the `id` of the field from `GET /bots/v1/{bot_id}/custom-fields`. Every id is checked against the chat's own bot: a foreign tag, field or person is `400 unknown_tag` / `unknown_custom_field` / `unknown_assignee`. Chat fields worth knowing: `status`, `assign_id`, `tags` (tag ids), `custom_fields` (`id`, `key`, `value`), `last_message`, `read_state.incoming_unread_count` (unread messages from the lead), `lead` (traffic source data), `created_at`. ### Messages | Call | Body | Needs | | --- | --- | --- | | `POST /messages/v1/search` | `chat_id` (required), `type` (`message`/`event`), `order` (`asc`/`desc`), `limit`, `cursor` | `message.read` | | `POST /messages/v1/send` | `chat_id`, `content` (≤ 4096 chars), `files`, `reply_message_id`, `meta`, `schedule` | `message.send` | | `GET /messages/v1/{message_id}` | | `message.read` | | `PATCH /messages/v1/{message_id}` | `content` | `message.manage` | | `DELETE /messages/v1/{message_id}` | | `message.delete` | - `content`, `files` or both; an empty message is rejected. - `files`: up to 10 items `{"type": "photo", "url": "https://..."}`; types `photo`, `video`, `audio`, `voice`, `document`, `sticker`, `animation`. Only public URLs — the API does not accept uploads. - `reply_message_id` must be a message from the same chat. - `schedule`: a future RFC 3339 time when the message goes out. - `sender_type` on a message: `incoming` — from the lead, `outcoming` — from our side. `sender` is the operator id, empty when the bot sent it. - Message `status`: `waiting`, `ready`, `failed` (see `error`), `schedule`. ### Analytics (all `GET`, permission `analytics.bot.read`) `/analytics/v1/bots/{bot_id}/chats/new`, `/chats/active`, `/chats/summary`, `/operators/messages`, `/operators/chats`, `/operators/events`, `/operators/messages-per-chat`, `/operators/events-per-chat`, `/operators/reply-time`. Query: `from` and `to` in RFC 3339 (required, at most 366 days apart), `timezone` (IANA, default UTC); operator series also take `group_by` (`day`/`hour`) and `operator_id`. `chats/summary` takes only `timezone` and returns `today`, `yesterday`, `week`, `last_week`, `month`, `last_month`. `reply-time` is the median in seconds between a lead's message and a human reply. ### Tracker (company-wide) | Call | Body | Needs | | --- | --- | --- | | `POST /campaigns/v1/search` | `company_id`, `status`, `target_type`, `query`, `bot_id`, `limit`, `cursor` | `campaign.read` | | `GET /campaigns/v1/{campaign_id}` | with sources, params, bots, buyers | `campaign.read` | | `PATCH /campaigns/v1/{campaign_id}` | `name`, `description`, `status` | `campaign.manage` | | `GET /campaigns/v1/{campaign_id}/sources`, `/params` | query `limit`, `cursor` | `campaign.read` | | `POST /campaigns/v1/{campaign_id}/params` | `name`, `key`, `placeholder` | `campaign.manage` | | `DELETE /campaigns/v1/{campaign_id}/params/{param_id}` | | `campaign.manage` | | `POST /domains/v1/search` | `company_id`, `limit`, `cursor` | `domain.read` | | `POST /domains/v1` | `company_id`, `link` | `domain.manage` | | `GET` / `PATCH` / `DELETE /domains/v1/{domain_id}` | PATCH: `link` | `domain.read` / `domain.manage` | | `POST /landings/v1/search` | `company_id`, `limit`, `cursor` | `landing.read` | | `GET /landings/v1/{landing_id}` | | `landing.read` | | `GET /leads/v1/{lead_id}` | lead with `params` (ip, user agent, UTM) and `events` (`purchase`, …) | `campaign.read` | Campaigns cannot be created through the API. ### Realtime (instead of polling) `POST /chats/v1/realtime` with `bot_id` and `filter` (same as search), or `POST /chats/v1/{chat_id}/realtime` for one chat, returns `ws_url`, `connection_token`, `channel` (and `subscription_token` for one chat). Connect with any Centrifugo client, subscribe to the channel, and call the same endpoint again before `expires_in_seconds` runs out. List events have `kind`: `bump` (new message), `upsert` (changed), `remove`, `stale` (re-read that chat). Use this only when the user runs a long-lived program; for one-off questions, search is enough. ## 8. Chat filter `filter` in chat search, stats, export and realtime is a tree. A condition is `{"field", "operator", "value"}`; a group is `{"logic": "and"|"or", "filters": [...]}`. At most 64 nodes, depth 5, 100 values per condition. An unknown field or operator is `400`, never a silent empty answer. | Field | Operators | Value | | --- | --- | --- | | `query` | none | text; matches name, username, external id of the contact | | `id`, `external_id` | `is`, `is_any`, `is_not` (`external_id` also `is_not_empty`) | chat id / Telegram id | | `status` | `is`, `is_any`, `is_not` | `manual`, `ai`, `blocked`, `start`, `stopped`, `waiting`, `error`, `finished`, `lead`, virtual `ignore` (= `manual` and the last message is from the lead) | | `last_message_sender_type` | `is`, `is_any`, `is_not` | `incoming` / `outcoming` | | `assign_id` | `is`, `is_any`, `is_not`, `is_empty`, `is_not_empty` | member id | | `current_step_id`, `current_flow_id` | same as `assign_id` | step / flow id | | `current_sale_stage_id` | `is`, `is_any`, `is_not`, `is_not_empty` | sale stage id | | `tags` | `has_any`, `has_all`, `has_only`, `has_none`, `is_not_empty` | array of tag ids | | `tracker_events` | `has_any`, `has_all`, `has_none`, `is_not_empty` | e.g. `["purchase"]` | | `created_at`, `updated_at` | `before`, `after`, `between` | RFC 3339 or `2026-10-01`; optional `timezone`; `updated_at` = time of the last message | | `cost`, `incoming_unread_count` | `more_number`, `less_number`, `more_equal_number`, `less_equal_number`, `equal_number`, `not_equal_number` | number | | `custom_fields` | `is`, `is_not`, `is_any` | needs `key` = custom field `id` | | `tracker_fields` | `is`, `is_not`, `is_any` | needs `key` = parameter name, e.g. `utm_source` | ```json { "bot_id": "<bot id>", "limit": 50, "filter": { "logic": "and", "filters": [ { "field": "status", "operator": "is", "value": "manual" }, { "field": "incoming_unread_count", "operator": "more_number", "value": 0 }, { "field": "updated_at", "operator": "after", "value": "2026-10-29T00:00:00Z" } ] } } ``` Search reads an index that lags a second or two behind. For the exact current state of one chat, use `GET /chats/v1/{chat_id}`. ## 9. Recipes - **"Which chats are waiting for an answer?"** — chat search with `status is ignore`, or `last_message_sender_type is incoming` plus `incoming_unread_count more_number 0`. Report name, time of the last message and its text. - **"What did the lead write?"** — find the chat (`query` filter by name or `external_id`), then messages with `order: desc`, `type: message`. - **"Reply to the lead"** — draft the text, show it, wait for "yes", `POST /messages/v1/send`, then confirm by reading the message back (`status` should become `ready`). - **"Hand the chat to an operator"** — space members list → `assign_id`; `PATCH` with `assign_id` and `status: manual`. - **"Tag every chat that …"** — `GET /bots/v1/{bot_id}/tags` for the tag id, search with the filter, show the count, confirm, then `PATCH` each chat with `tags.assign`. - **"How many new chats this week?"** — `GET /analytics/v1/bots/{bot_id}/chats/summary?timezone=Europe/Moscow`. - **"Which operator answers slowest?"** — `operators/reply-time` for the period, resolve `operator_id` through the space members list. - **"Export everything"** — `POST /chats/v1/export` page by page without pauses; get the expected total from `POST /chats/v1/stats` with the same filter. - **"Where did this lead come from?"** — `GET /chats/v1/{chat_id}` → `lead` (or `GET /leads/v1/{lead_id}`) → `params` and `campaign_id` → `GET /campaigns/v1/{campaign_id}`. ## 10. What the API does not do No inviting people, changing roles, billing, creating bots or flows, creating campaigns, uploading files, or webhooks. Those are done in the Chatterfy app. If the user asks for one of them, say it is not available through the API and point them to the app.
The skill is written in English — this helps the AI understand instructions more accurately. The AI will reply in the language you are writing in.
Connect the skill in Claude Code
- Place the
chatterfy-apifolder into~/.claude/skills/— you will get~/.claude/skills/chatterfy-api/SKILL.md.
- Pass the API key via the
CHATTERFY_API_KEYenvironment variable and launch Claude Code in the same Terminal window:
Терминалbash
export CHATTERFY_API_KEY='ВАШ_КЛЮЧ'
claude- Ask anything about Chatterfy — Claude will pick up the skill automatically.
Connect the skill in Cursor
- Place the
chatterfy-apifolder into the.cursor/skills/folder of your project — you will get.cursor/skills/chatterfy-api/SKILL.md.
- Pass the key via the
CHATTERFY_API_KEYenvironment variable in the terminal from which you launch Cursor, or mention the key to the agent in the chat.
- Ask the agent: "Show my bots in Chatterfy". The agent will send requests via the terminal.
Connect the skill in Claude in the browser and Claude Desktop
- Compress the
chatterfy-apifolder into a ZIP. On macOS: right-click the folder → Compress. On Windows: right-click → Send to → Compressed (zipped) folder.
- In Claude, open Settings → Capabilities, find the Skills section, and upload the ZIP archive.
- In the same settings, enable code execution and allow internet access to the
api.chatterfy.aidomain. Without this, Claude will not be able to send requests.
- In a new chat, tell Claude the key and ask a question about Chatterfy.
Connect the skill in ChatGPT
- Create a project in the Projects section and add
SKILL.mdto the project files.
- Ask questions in the chats of this project.
ChatGPT will know how the API is structured and will write the necessary commands and scripts. But it cannot send requests to Chatterfy itself: its code environment does not have internet access. Run the commands yourself — as in the article First API request in 5 minutes.
Connect the skill in another AI
If your AI assistant does not support skills, paste the text of
SKILL.md at the beginning of the chat or into the assistant's system instructions. The AI will be able to send requests only if it has access to the internet or to a terminal.Pass the key securely
- The best way is via the
CHATTERFY_API_KEYenvironment variable: the skill first looks for the key there, and it does not appear in the correspondence.
- If the AI works in a browser, you will have to provide the key in the chat. Create a separate service account for the AI — then in case of a leak, it is enough to delete only that one.
- Grant the AI the most restricted role that suffices for the task. For reading — Space viewer, for responding — Space controller.
- If the AI revealed the key in a response or you sent the key to the wrong place, immediately issue a new one using the Reissue key button.
AI acts on behalf of the key
Everything permitted to the key can also be done by the AI. The skill requires asking for confirmation before sending messages, making changes, and deleting, but always read what the AI intends to do before replying "yes".
Check that everything works
Ask to check the connection
Write to the AI: "Check the connection to Chatterfy and show my bots".Look at the answer
The AI should show a list of bots. Error401— the key did not reach the AI or was copied incorrectly. An empty list or error403— insufficient permissions: check the role according to the article API Key Permissions: Which Role to Assign.
Frequently Asked Questions
Related
Was this article helpful?

