#Серверный API диалогов
Статус: Доступно (чтение, заведение диалога, приём сообщения клиента с ответом ИИ, отправка карточек, операции) / Планируется (ответ оператора, перехват, возврат ИИ, закрытие). Маршруты
operator-messages,takeover,releaseиcloseописаны в проекте и ещё не работают: вызовы вернут404.
Назначение: управлять диалогами с вашего сервера: заводить диалог из своего мессенджера, передавать реплики клиента ИИ-продажнику, получать его ответ, отвечать от имени оператора, перехватывать диалог и возвращать его ИИ, закрывать.
#Содержание
- Когда использовать
- Доступ
- Общие правила
- Объекты
- GET /conversations
- GET /conversations/{id}
- GET /conversations/{id}/messages
- Подключение канала
- POST /conversations
- POST /conversations/{id}/messages
- Причины отсутствия ответа ИИ
- GET /operations/{id}
- POST /conversations/{id}/cards
- POST /conversations/{id}/operator-messages
- Перехват, возврат ИИ и закрытие
- События
- Примеры на четырёх языках
- Заметки
- Открытые вопросы
#Когда использовать
- Ваши клиенты общаются в вашем мессенджере, приложении или стороннем виджете, а отвечать должен ИИ-продажник Формула ИИ. Пошаговое руководство — ИИ-продажник в своём канале.
- Нужно показывать диалоги Формула ИИ в вашей системе и отвечать оператором оттуда.
- Для чата на сайте без вашего сервера используйте виджет; для своего интерфейса в браузере — клиентский Chat API.
#Доступ
| Базовый адрес | https://<домен ячейки>/api/public/v1 — RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id |
| Аутентификация | Authorization: Bearer <ключ>; ключ действует только в своей ячейке (Аутентификация) |
| Только сервер | CORS отсутствует: вызывайте API с вашего сервера, ключ в браузер и в приложение не помещайте |
| Лицензия | признак api_access; иначе feature_not_in_plan |
| Канал | api. Подключается в кабинете: Настройки → API → «Подключить канал чата» (одна интеграция — один канал api, агент выбирается там же). Идентификатор канала возвращает GET /me (channel_id). Пока канал не подключён, маршруты записи отвечают 409 channel_not_configured (Подключение канала) |
Области ключа:
| Область | Что даёт | Платная | Содержит ПДн |
|---|---|---|---|
conversations:read | чтение диалогов и сообщений | нет | да |
conversations:write | заведение диалога, приём сообщения клиента, отправка карточек в диалог; в планах — сообщение оператора, перехват, возврат ИИ, закрытие | нет | да |
ai_seller:invoke | ответ ИИ-продажника на сообщение клиента | да | да |
Права запроса — пересечение областей ключа, роли интеграции, прав владельца и лицензии (как вычисляются). Для приёма сообщения клиента с ответом ИИ нужны обе области: conversations:write и ai_seller:invoke. Приём без ответа ИИ требует только conversations:write.
В ячейке RU для областей с ПДн (
conversations:*) при подключении интеграции обязательны список разрешённых IP, страна размещения принимающей системы и основание передачи. ПДн передаются только получателям в РФ: если страна иная, областьconversations:*не выдаётся (403 pii_transfer_not_allowed, Ячейки и данные).
| Статус | Код | Когда |
|---|---|---|
401 | invalid_api_key, wrong_cell | ключ неверен или из другой ячейки |
403 | scope_missing, feature_not_in_plan, ip_not_allowed | нет области, тарифа или адрес не разрешён |
413 | payload_too_large | тело запроса больше 256 КБ |
#Общие правила
Подробно — Соглашения.
| Правило | Значение |
|---|---|
| Формат | JSON, UTF-8, тело до 256 КБ |
| Ошибки | application/problem+json, ветвление по полю code; каталог — Соглашения |
| Идентификатор запроса | заголовок X-Request-Id в каждом ответе, поле request_id в ошибке |
| Пагинация | limit от 1 до 100 (по умолчанию 25), cursor (живёт 24 часа); ответ {data, next_cursor}; порядок возрастающий: у диалогов (updated_at, id), у сообщений (created_at, id) (пагинация) |
| Идемпотентность | заголовок Idempotency-Key на POST: для заведения диалога, приёма сообщения клиента и отправки карточек — обязателен; срок 24 часа (идемпотентность) |
| Асинхронность | ответ ИИ только асинхронно: 202 и конверт operation; ?wait=N (до 20 с) ждёт то же задание; результат — GET /operations/{id} или событие operation.completed (асинхронные операции) |
| Платные операции | ответ ИИ списывается с кошелька арендатора; возможны spend_limit_reached (402) и wallet_suspended (402, обмен остановлен); стоимость — в operation.usage.cost (платные операции) |
| Пределы частоты | чтение 600/мин, запись 120/мин, платные 30/мин (стартовые значения, действующие — в GET /me). Одновременных платных операций (ответов ИИ) не более 5 на интеграцию и не более 5 на арендатора: сверх предела — 429 concurrency_limit |
| Выбор полей | параметр fields= у чтения: без него приходят все поля, кроме персональных; персональное поле — только если названо явно (Минимизация и выбор полей) |
| Идентификаторы | непрозрачные строки; в примерах cnv_EXAMPLE01, msg_EXAMPLE01, evt_EXAMPLE01 |
#Объекты
Поля объектов диалога и сообщения — фактические (схемы
ConversationиMessageв OpenAPI). Идентификаторы —uuid. Набор значенийstatus,channelиcontent_typeможет расширяться: не считайте списки закрытыми.
#Диалог
| Поле | Тип | Описание |
|---|---|---|
id | строка (uuid) | идентификатор диалога |
channel | строка или null | тип канала: api, telegram, website … |
contact_id | строка или null | идентификатор контакта (клиента) в Формула ИИ |
external_customer_id | строка или null | персональное поле: приходит, только если названо в fields=. Идентификатор клиента в вашей системе; отдаётся только у канала api, у мессенджеров всегда null (внешний идентификатор мессенджера не раскрывается) |
status | строка | состояние: open, handoff_pending, resolved, closed … |
mode | ai или operator | кто ведёт диалог: ai — отвечает ИИ (режимы auto и supervised), operator — человек (copilot и manual) |
language | строка или null | язык диалога (ISO 639-1) |
created_at | строка или null | начало диалога, ISO 8601 UTC |
updated_at | строка | последнее изменение или сообщение, ISO 8601 UTC; по нему идёт обход. Счётчик непрочитанного его не меняет |
_enc | массив строк | только ячейки EN и ID, когда действует формат JWE: имена полей, значения которых заменены строкой JWE (Формат JWE) |
#Сообщение
| Поле | Тип | Описание |
|---|---|---|
id | строка (uuid) | идентификатор сообщения |
conversation_id | строка | диалог |
sender | customer, ai, operator, system | отправитель: клиент, ИИ, оператор, служебное |
content_type | строка | вид содержимого: text, image, file … |
content | строка или null | персональное поле (только по fields=). Текст; null — текста нет (только вложение) или сообщение удалено |
attachments | массив | персональное поле (только по fields=). Вложения, до 5 на сообщение: {type, name, size, url, expires_at}; url — временная подписанная ссылка (около часа), не сохраняйте её, запросите сообщение заново |
cards | массив | карточки товаров сообщения (до трёх), копия на момент отправки; поля нет — карточек нет. Персональных данных не содержат. Форма — Карточки |
deleted | логическое | true — сообщение удалено (приходит только при include_deleted=true, без текста и вложений) |
created_at | строка | время сообщения, ISO 8601 UTC |
_enc | массив строк | как у диалога: только EN и ID при формате JWE |
Сообщения содержат персональные данные клиентов. Не записывайте их в журналы и не передавайте третьим лицам без основания.
Персональные данные и журнал выдач. Каждая выдача записывается в журнал выдач (см. ячейки и данные); в журнал попадают только поля, которые вы назвали в fields=. На ячейке RU нужны узкий список разрешённых IP-адресов у ключа и страна получателя RU, иначе 403 pii_transfer_not_allowed приходит до чтения. Если строку журнала записать не удалось — 503, данные не отдаются. Диалоги личных ящиков и чаты кабинета ученика не отдаются; чужой, несуществующий или не-uuid идентификатор даёт 404 not_found.
Ответы чтения несут Cache-Control: private, no-store.
#GET /conversations
Статус: Доступно. Область
conversations:read.
Список диалогов с фильтрами и курсором. Порядок возрастающий по (updated_at, id): чтобы синхронизироваться, запоминайте next_cursor и возвращайтесь с ним.
| Параметр | Тип | Обязательный | Ограничения | Пример |
|---|---|---|---|---|
channel | строка | нет | фильтр по каналу | api |
status | строка | нет | фильтр по статусу | open |
mode | строка | нет | ai или operator | operator |
updated_after | строка | нет | диалоги, изменённые строго позже момента; ISO 8601 с поясом | 2026-10-11T00:00:00Z |
include_deleted | логическое | нет | принимается для единообразия, у диалогов ничего не добавляет (мягкого удаления нет) | false |
limit | целое | нет | от 1 до 100, по умолчанию 25 | 50 |
cursor | строка | нет | next_cursor предыдущего ответа | eyJ2Ijox… |
fields | строка | нет | через запятую, до 2000 знаков; id приходит всегда. Без параметра — все поля, кроме персональных; персональное поле (external_customer_id) — только если названо (Минимизация и выбор полей) | external_customer_id,status |
Encryption-Key-Id | заголовок | нет | только EN и ID: какой зарегистрированный ключ использовать для формата JWE (Формат JWE) | key-2026-10 |
curl -sS "$API_BASE_URL/conversations?channel=api&mode=operator&limit=50&fields=external_customer_id" \
-H "Authorization: Bearer $API_KEY"
{
"data": [
{
"id": "7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01",
"channel": "api",
"contact_id": "0d94a7c8-5e2f-4b13-8a6c-91f4e7b02d35",
"external_customer_id": "client-4815",
"status": "open",
"mode": "operator",
"language": "ru",
"created_at": "2026-10-11T08:00:00Z",
"updated_at": "2026-10-11T08:15:30Z"
}
],
"next_cursor": null
}
В примере поле external_customer_id названо в fields=; без этого параметра в ответе его не будет. Меняя фильтры, начинайте обход заново без cursor: курсор привязан к фильтрам. Недействительный, просроченный или чужой курсор — 422 validation_failed.
#GET /conversations/{id}
Статус: Доступно. Область
conversations:read.
Один диалог; принимает fields= и Encryption-Key-Id так же, как список. Если диалога нет или он принадлежит другому арендатору — 404 not_found.
curl -sS "$API_BASE_URL/conversations/7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01?fields=external_customer_id" \
-H "Authorization: Bearer $API_KEY"
#GET /conversations/{id}/messages
Статус: Доступно. Область
conversations:read.
Сообщения диалога постранично: limit (по умолчанию 25, максимум 100), cursor, include_deleted, fields, заголовок Encryption-Key-Id (EN и ID). Без fields текст (content) и вложения (attachments) не приходят: это персональные поля, их нужно назвать явно, например fields=content,attachments. Внутренние заметки операторов не отдаются. Если интеграция на EN или ID работает в режиме required и ключ не зарегистрирован — 409 encryption_key_required. Порядок возрастающий по (created_at, id) — от старых к новым.
С include_deleted=true удалённое сообщение приходит как «надгробие»: deleted: true, content: null, без вложений.
curl -sS "$API_BASE_URL/conversations/7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01/messages?fields=sender,content,attachments,cards,created_at" \
-H "Authorization: Bearer $API_KEY"
{
"data": [
{ "id": "1a2b3c4d-0001-4000-8000-000000000001", "sender": "customer", "content": "Сколько стоит курс?", "attachments": [], "created_at": "2026-10-12T08:15:30Z" },
{ "id": "1a2b3c4d-0002-4000-8000-000000000002", "sender": "ai", "content": "Курс стоит 9 900 ₽.", "attachments": [], "created_at": "2026-10-12T08:15:33Z" },
{
"id": "1a2b3c4d-0003-4000-8000-000000000003",
"sender": "customer",
"content": null,
"attachments": [
{ "type": "image", "name": "label.jpg", "size": 182044, "url": "https://example.com/signed/EXAMPLE", "expires_at": "2026-10-12T09:20:00Z" }
],
"created_at": "2026-10-12T08:19:00Z"
}
],
"next_cursor": null
}
#Подключение канала
Все маршруты записи работают в канале api вашей интеграции. Канал подключается один раз в кабинете: Настройки → API → «Подключить канал чата». Там же выбирается агент, который будет отвечать.
Пока канал не подключён, POST /conversations и POST /conversations/{id}/messages возвращают 409 channel_not_configured: подключите канал в кабинете и повторите запрос. Чтение (GET) от подключения не зависит.
#POST /conversations
Статус: Доступно. Область
conversations:write.Idempotency-Keyобязателен.
Найти или завести диалог клиента вашего канала по внешнему идентификатору.
- Есть активный диалог этого клиента — вернётся он же:
200. - Нет — создаётся новый:
201и событиеconversation.created. external_customer_idизолирован по интеграции: один и тот же идентификатор у двух разных интеграций — это разные клиенты, диалоги не смешиваются.- Повтор с тем же
Idempotency-Keyвозвращает тот же диалог.
| Поле | Тип | Обязательный | Ограничения | Пример |
|---|---|---|---|---|
external_customer_id | строка | да | 1–128 знаков; стабильный идентификатор клиента в вашей системе (не телефон и не почта) | client-4815 |
name | строка | нет | до 120 знаков; пишется только в пустое поле контакта | Анна |
email | строка | нет | до 254 знаков; только в пустое поле контакта | anna@example.com |
phone | строка | нет | до 32 знаков, международный формат; только в пустое поле контакта | +79990001122 |
language | строка | нет | ISO 639-1, две буквы; язык нового диалога. Язык ответа ИИ ограничен языками ячейки | ru |
Неизвестные поля дают 422 validation_failed.
curl -sS -X POST "$API_BASE_URL/conversations" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: 6f3c1f3e-5b52-4a52-9e0b-0d8e3a2c9f11" \
-H "Content-Type: application/json" \
-d '{"external_customer_id":"client-4815","name":"Анна","language":"ru"}'
Ответ — объект диалога: 201 для нового, 200 для найденного. Ответ содержит персональные данные, поэтому выдача записывается в журнал выдач (на RU действуют ограничения из раздела Доступ).
| Статус | Код | Когда |
|---|---|---|
409 | channel_not_configured | канал чата не подключён (Подключение канала) |
409 | idempotency_conflict | тот же ключ с другим телом |
409 | operation_in_progress | параллельный повтор того же ключа |
402 | wallet_suspended | обмен остановлен: баланс кошелька отрицателен или кошелёк заблокирован |
422 | validation_failed | нет external_customer_id, неизвестные поля, длина |
429 | rate_limited | предел частоты |
503 | service_unavailable | хранилище пределов недоступно; повторите с тем же ключом |
#POST /conversations/{id}/messages
Статус: Доступно. Области:
conversations:write(приём) иai_seller:invoke(ответ ИИ).Idempotency-Keyобязателен. Платная операция при вызове ИИ.
Передать сообщение клиента. Если запрошен ответ ИИ и у ключа есть область ai_seller:invoke, платформа запускает ИИ-продажника асинхронно. Вложения в этом маршруте не принимаются.
#Запрос
| Поле | Тип | Обязательный | Ограничения | Пример |
|---|---|---|---|---|
content | строка | да | 1–4000 знаков | Сколько стоит курс? |
ai_reply | auto или none | нет, по умолчанию auto | auto — запросить ответ ИИ (без области ai_seller:invoke сообщение только сохраняется); none — только сохранить сообщение | auto |
Неизвестные поля дают 422 validation_failed.
| Параметр адреса | Тип | Описание |
|---|---|---|
wait | целое от 0 до 20 | секунды ожидания ответа ИИ. Ждёт то же задание, новое не создаётся. Не входит в ключ идемпотентности |
| Заголовок | Обязательный | Описание |
|---|---|---|
Idempotency-Key | да | уникальная строка на каждую новую реплику клиента; повтор с тем же ключом не создаёт дубль и не списывает деньги второй раз |
curl -sS -X POST "$API_BASE_URL/conversations/7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01/messages?wait=15" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: 8d4b2a40-1c63-4d4e-8f0a-6c2d3e1b7a90" \
-H "Content-Type: application/json" \
-d '{"content":"Сколько стоит курс?","ai_reply":"auto"}'
#Ответы
Ответ ИИ выполняется только асинхронно: запрос не держит соединение дольше wait. В ответе нет текстов: ни сообщения клиента, ни ответа ИИ — только идентификаторы и итог. Тексты читайте запросом GET /conversations/{id}/messages (там выдача ПДн попадёт в журнал выдач) или получайте событием message.created.
| Статус | Когда | ai_reply |
|---|---|---|
202 | ответ ИИ запущен, к концу wait ещё не готов (или wait не задан) | status: queued, есть operation |
200 | ответ ИИ готов в пределах wait | status: replied, есть reply.id и завершённая operation со стоимостью |
201 | ответ ИИ не запрашивался: ai_reply: none или нет области ai_seller:invoke | status: skipped, reason: not_requested; ИИ не вызывается, ничего не списывается |
200 | ИИ не ответит по причине, не связанной с вашим запросом | status: skipped и причина в reason; операции нет, деньги не списываются |
200 | ответ ИИ стал черновиком, ожидающим оператора (режим с проверкой) | status: queued_for_review |
Принято, ответ ИИ готовится (202):
{
"message_id": "1a2b3c4d-0001-4000-8000-000000000001",
"conversation_id": "7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01",
"ai_reply": { "status": "queued" },
"operation": {
"id": "5e0c9d52-7b1a-4c3d-8e21-0f4a6b7c8d90",
"type": "conversation.ai_reply",
"status": "queued",
"created_at": "2026-10-12T08:15:30Z",
"resource": { "type": "message", "id": "1a2b3c4d-0001-4000-8000-000000000001" }
}
}
Ответ готов в пределах wait (200). Текста ответа нет, только reply.id; в операции стоимость в валюте кошелька:
{
"message_id": "1a2b3c4d-0001-4000-8000-000000000001",
"conversation_id": "7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01",
"ai_reply": { "status": "replied" },
"reply": { "id": "1a2b3c4d-0002-4000-8000-000000000002" },
"operation": {
"id": "5e0c9d52-7b1a-4c3d-8e21-0f4a6b7c8d90",
"type": "conversation.ai_reply",
"status": "succeeded",
"created_at": "2026-10-12T08:15:30Z",
"completed_at": "2026-10-12T08:15:33Z",
"resource": { "type": "message", "id": "1a2b3c4d-0001-4000-8000-000000000001" },
"usage": { "cost": { "amount": "0.42", "currency": "RUB" }, "model": "model-EXAMPLE" }
}
}
ИИ не запрашивался или не отвечал — это успешный ответ, а не ошибка. Операция не создаётся, деньги не списываются:
{
"message_id": "1a2b3c4d-0001-4000-8000-000000000001",
"conversation_id": "7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01",
"ai_reply": { "status": "skipped", "reason": "not_requested" }
}
Не дождались — вернётся 202 с текущим состоянием операции. Дальше выберите способ получения ответа (Асинхронные операции):
- подпишитесь на события
message.createdиoperation.completed(вебхуки) — предпочтительный способ; - опросите
GET /operations/{id}; - прочитайте
GET /conversations/{id}/messages(текст ответа запрашивайте черезfields=content).
Повтор того же запроса с тем же Idempotency-Key во время выполнения возвращает тот же 202, после завершения — итог; сообщение повторно не записывается и не оплачивается.
#Проверки до записи
Платная часть проверяется до записи сообщения: при отказе ничего не записывается, а Idempotency-Key освобождается — повторите запрос после устранения причины с тем же ключом.
- Одновременных ответов ИИ не более 5 на интеграцию и не более 5 на арендатора — иначе
429 concurrency_limit. - Максимум стоимости одной операции и суточный предел интеграции — иначе
402 spend_limit_reached. - Диалог другого канала или арендатора —
404 not_found, закрытый диалог —409 invalid_state.
#Ошибки
| Статус | Код | Когда | Что делать |
|---|---|---|---|
422 | validation_failed | нет текста, длиннее 4000 знаков, неизвестные поля; детали в errors[] | Исправить запрос |
413 | payload_too_large | тело больше 256 КБ | Уменьшить тело |
401 | invalid_api_key, wrong_cell | ключ | Проверить ключ и адрес ячейки |
402 | spend_limit_reached | достигнут предел трат (операции или суток) | Дождаться окна или поднять предел |
402 | wallet_suspended | обмен остановлен: баланс кошелька отрицателен или кошелёк заблокирован; в ответе topup_url и balance | Пополнить кошелёк |
403 | scope_missing, pii_transfer_not_allowed | нет conversations:write; на RU — не выполнены условия выдачи ПДн | Проверить области и настройки интеграции |
404 | not_found | диалога нет, он чужой или другого канала | Проверить идентификатор |
409 | channel_not_configured | канал чата не подключён | Подключить в кабинете |
409 | invalid_state | диалог закрыт | Завести новый диалог |
409 | idempotency_conflict | тот же ключ с другим телом | Новый ключ для новой реплики |
409 | operation_in_progress | параллельный повтор | Подождать Retry-After и повторить |
429 | rate_limited | предел частоты | Подождать Retry-After |
429 | concurrency_limit | уже идут 5 ответов ИИ на интеграцию или на арендатора | Дождаться завершения и повторить с тем же ключом |
503 | upstream_unavailable | недоступна языковая модель | Повторить с тем же ключом |
503 | service_unavailable | недоступно хранилище пределов; есть Retry-After | Повторить с тем же ключом |
Общий разбор классов — Соглашения.
#Причины отсутствия ответа ИИ
Платформа принимает сообщение клиента всегда, когда запрос корректен. ИИ может не ответить — и это не сбой. Состояние ответа ИИ возвращается полем ai_reply в теле успешного ответа на сообщение клиента.
ai_reply — объект {status, reason?}:
status | Смысл |
|---|---|
replied | ИИ ответил: ответ — отдельное сообщение диалога (reply.id) |
queued | ответ готовится; результат — по operation или событием message.created |
queued_for_review | ответ ИИ стал черновиком и ждёт оператора (режим с проверкой): клиенту он не уйдёт, пока оператор не одобрит |
skipped | ИИ не ответит; причина в reason |
reason (при skipped) | Смысл | Что делать вашему серверу |
|---|---|---|
operator_active | диалог ведёт оператор; ИИ не отвечает | Ничего: оператор ответит. Ответ придёт событием message.created |
spam | сообщение признано спамом | Не пересылать клиенту автоответ; при необходимости — журнал |
flow_reply | ответил сценарий (поток), а не ИИ | Взять ответ сценария из диалога |
ai_unavailable_handoff | ИИ недоступен; диалог передан оператору | Сообщить клиенту, что ответит сотрудник |
no_agent | для канала не назначен агент | Назначить агента в кабинете |
daily_limit | исчерпан суточный предел ответов ИИ в канале | Сообщение сохранено, диалог получил состояние handoff_pending, ответ ИИ не запрашивался и не оплачивался. Сообщить клиенту, что ответит сотрудник |
insufficient_funds | на кошельке не хватило средств на ответ ИИ | Сообщение сохранено, ИИ не вызывался. Пополнить кошелёк; клиенту сообщить, что ответит сотрудник |
upstream_unavailable | языковая модель не ответила | Сообщение сохранено; повторять запрос не нужно — ответит оператор или следующая реплика |
quota_exceeded | исчерпана квота ответов ИИ по тарифу | Поднять тариф или дождаться нового периода |
not_requested | вы не просили ответ ИИ: ai_reply: none или у ключа нет ai_seller:invoke | Ничего; при необходимости добавьте область ai_seller:invoke |
Список причин может расти: ветвитесь по известным значениям и обрабатывайте неизвестное как «ответа ИИ не будет».
Для канала api суточный предел работает так же, как у виджета: значение берётся из настройки канала ai_daily_limit (по умолчанию 200 в сутки, максимум 10 000); в событии conversation.handoff_requested диалог попадает к оператору (События).
#GET /operations/{id}
Статус: Доступно. Отдельной области не требует: нужна та же область, что и у создавшего операцию запроса (для ответа ИИ-продажника —
ai_seller:invoke).
Состояние асинхронной операции, созданной вашей интеграцией (например, conversation.ai_reply из ответа 202). Возвращает конверт {operation}: состояние queued, running, succeeded, failed или canceled, ресурс результата (resource) и стоимость (usage) для платных операций.
curl -sS "$API_BASE_URL/operations/5e0c9d52-7b1a-4c3d-8e21-0f4a6b7c8d90" \
-H "Authorization: Bearer $API_KEY"
Операция другой интеграции или другого арендатора — 404 not_found. Вместо частого опроса подпишитесь на событие operation.completed (События). Подробности конверта — Соглашения.
#POST /conversations/{id}/cards
Статус: Доступно. Область
conversations:write.Idempotency-Keyобязателен.
Отправить в диалог одну—три карточки товаров с кнопками: сообщение от имени оператора. Карточки создаются заранее — в кабинете или через API карточек.
| Поле | Тип | Обязательный | Ограничения | Пример |
|---|---|---|---|---|
codes | массив строк | да | коды карточек, от 1 до 3 | ["hydrolat-basics"] |
comment | строка или null | нет | текст над карточками, до 1000 знаков | Подобрал для вас курс |
curl -sS -X POST "$API_BASE_URL/conversations/7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01/cards" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: 2f9a1c77-5d3e-4b8a-9c01-7e6d5a4b3c21" \
-H "Content-Type: application/json" \
-d '{"codes":["hydrolat-basics"],"comment":"Подобрал для вас курс"}'
Ответ 201:
{
"message_id": "1a2b3c4d-0003-4000-8000-000000000003",
"conversation_id": "7c1f0e52-3a4b-4d6e-9f10-2b8a5c3d9e01",
"created_at": "2026-10-12T08:20:00Z",
"cards": [
{
"code": "hydrolat-basics",
"name": "Основы гидролатов",
"description": null,
"image_url": "https://example.com/img/hydrolat.jpg",
"price_text": "9 900 ₽",
"badge": "Хит",
"page_url": "https://example.com/courses/hydrolat-basics",
"buttons": [{ "label": "Подробнее", "url": "https://example.com/courses/hydrolat-basics" }]
}
],
"delivery": "metadata",
"status": "sent"
}
delivery показывает, как карточки доходят до клиента: metadata — виджет сайта и канал интеграции (вы получаете событие message.created); native — Telegram, VK и Max своим форматом; text — прочие каналы строками «название — цена — ссылка». status: sent, delivered или failed.
Карточка в сообщении — копия на момент отправки, адреса развёрнуты до полных. Неизвестный или неактивный код — 422 validation_failed; диалог другого арендатора, кабинета ученика или личного ящика — 404 not_found. Повтор того же Idempotency-Key возвращает то же сообщение.
#API арендатора: виджет и карточки
Настройки виджета и каталог карточек можно вести из вашей системы. Полные описания — в справочнике API.
| Маршрут | Область | Что делает |
|---|---|---|
GET /channels/{id}/widget | channels:read | прочитать настройки виджета канала (название и подзаголовок помощника, очередь, подпись, файлы, цвет) |
PATCH /channels/{id}/widget | channels:write | изменить настройки; null восстанавливает значение по умолчанию; изменения пишутся в аудит |
GET /cards, GET /cards/{code} | cards:read | список и одна карточка |
POST /cards, PATCH /cards/{code}, DELETE /cards/{code} | cards:write | создать, изменить, удалить карточку |
POST /conversations/{id}/cards | conversations:write | отправить карточки в диалог (выше) |
Что означают настройки виджета и как выглядят состояния — Встраивание виджета.
#POST /conversations/{id}/operator-messages
Статус: Планируется. Область
conversations:write.Idempotency-Key— рекомендуется.
Ответ оператора, введённый в вашей системе. Сообщение пишется в диалог от имени оператора и уходит клиенту событием message.created через ваш вебхук.
| Поле | Тип | Обязательный | Ограничения | Пример |
|---|---|---|---|---|
content | строка | да | Здравствуйте! Меня зовут Анна. | |
operator_id | строка | нет | идентификатор сотрудника (предварительно) | usr_EXAMPLE01 |
curl -sS -X POST "$API_BASE_URL/conversations/cnv_EXAMPLE01/operator-messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: 0a1f6f7b-8e0d-47c5-9f53-5a1b27c9d3e4" \
-H "Content-Type: application/json" \
-d '{"content":"Здравствуйте! Меня зовут Анна."}'
#Перехват, возврат ИИ и закрытие
Статус: Планируется. Область
conversations:write.
| Маршрут | Действие | После вызова |
|---|---|---|
POST /conversations/{id}/takeover | Оператор берёт диалог | mode=operator; ИИ перестаёт отвечать |
POST /conversations/{id}/release | Оператор возвращает диалог ИИ | mode=ai; следующие реплики клиента получают ответ ИИ |
POST /conversations/{id}/close | Закрыть диалог | диалог закрыт |
Тела запросов в проекте не описаны. Повтор вызова должен быть безопасным; Idempotency-Key передавайте в любом случае. Действие, недопустимое в текущем состоянии диалога (например, takeover закрытого диалога), возвращает 409 invalid_state.
curl -sS -X POST "$API_BASE_URL/conversations/cnv_EXAMPLE01/takeover" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: c2d6f3a9-4b0e-4a8b-8b7f-2d9a6e1f0c55"
#События
Получайте результат вебхуками вместо опроса (Вебхуки). Конверт события — {id, type, api_version, created_at, livemode, data}.
| Событие | Когда | В data |
|---|---|---|
conversation.created | создан диалог (в том числе вызовом POST /conversations) | conversation_id; полная нагрузка с ПДн — только при include_pii: true |
message.created | в диалоге появилось сообщение: клиента, ИИ или оператора, а также сообщение с карточками | conversation_id, message_id, sender; для сообщения клиента — ai_reply: {status, reason?}. Текстов, вложений и карточек в событии нет |
conversation.handoff_requested | запрошена передача оператору (в том числе по daily_limit и ai_unavailable_handoff) | conversation_id, lead_id (если у диалога есть лид) |
conversation.summary_ready | готова сводка диалога | conversation_id |
operation.completed | завершился ответ ИИ (succeeded, failed, canceled) | конверт operation (идентификаторы, состояние, usage.cost; без текста ответа) |
Событие передачи оператору одно — conversation.handoff_requested; имя handoff.requested относится к вебхукам версии 1 и в v2 не используется.
События message.created и operation.completed «тонкие» всегда: текст сообщения, вложения и карточки в них не входят, даже при include_pii: true. Содержимое получайте запросом GET /conversations/{id}/messages с нужными fields= (Минимизация и выбор полей): так выдача попадёт в журнал выдач. Флаг include_pii подписки добавляет ПДн в conversation.created, conversation.handoff_requested и conversation.summary_ready; для него у интеграции нужны conversations:read и leads:read (Вебхуки).
Событие message.created приходит и на сообщения оператора, и на ответы ИИ. Чтобы не отправить клиенту собственное сообщение дважды, сравнивайте message_id с тем, что вы уже обработали.
#Примеры на четырёх языках
Во всех примерах ключ и адрес берутся из переменных окружения; идентификатор диалога — условный.
#curl
curl -sS -X POST "$API_BASE_URL/conversations/cnv_EXAMPLE01/messages?wait=15" \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"content":"Сколько стоит курс?","ai_reply":"auto"}'
#JavaScript (Node.js 18+)
import { randomUUID } from 'node:crypto';
const BASE = process.env.API_BASE_URL; // https://formula-cream.pro/api/public/v1
const KEY = process.env.API_KEY;
export async function sendCustomerMessage(conversationId, text, idempotencyKey = randomUUID()) {
const res = await fetch(`${BASE}/conversations/${conversationId}/messages?wait=15`, {
method: 'POST',
headers: {
Authorization: `Bearer ${KEY}`,
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content: text, ai_reply: 'auto' })
});
const body = await res.json();
if (!res.ok) {
// application/problem+json: ветвимся по code (concurrency_limit, spend_limit_reached,
// channel_not_configured ...)
const err = new Error(`${body.code}: ${body.detail}`);
err.code = body.code;
err.requestId = body.request_id;
throw err;
}
// 200 — body.ai_reply.status === 'replied', body.reply.id: текст читайте через
// GET /conversations/{id}/messages?fields=content
// 202 — ответ ещё готовится (body.operation): ждите вебхук message.created
// или опрашивайте GET /operations/{id}
// 201 — ИИ не запрашивался (ai_reply.reason === 'not_requested')
return { status: res.status, body, idempotencyKey };
}
#Python (httpx)
import os
import uuid
import httpx
BASE = os.environ["API_BASE_URL"] # https://formula-cream.pro/api/public/v1
KEY = os.environ["API_KEY"]
def send_customer_message(conversation_id: str, text: str, idempotency_key: str | None = None):
key = idempotency_key or str(uuid.uuid4())
res = httpx.post(
f"{BASE}/conversations/{conversation_id}/messages",
params={"wait": 15},
headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": key},
json={"content": text, "ai_reply": "auto"},
timeout=30,
)
body = res.json()
if res.status_code >= 400:
# application/problem+json
raise RuntimeError(f'{body.get("code")}: {body.get("detail")} (request {body.get("request_id")})')
return res.status_code, body, key
#PHP
<?php
function sapport_send_customer_message(string $conversationId, string $text, ?string $idempotencyKey = null): array
{
$base = getenv('API_BASE_URL'); // https://formula-cream.pro/api/public/v1
$key = getenv('API_KEY');
$idempotencyKey ??= bin2hex(random_bytes(16));
$ch = curl_init("$base/conversations/$conversationId/messages?wait=15");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $key",
"Idempotency-Key: $idempotencyKey",
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['content' => $text, 'ai_reply' => 'auto'], JSON_UNESCAPED_UNICODE),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$body = json_decode($raw, true);
if ($status >= 400) {
throw new RuntimeException(($body['code'] ?? 'error') . ': ' . ($body['detail'] ?? ''));
}
return [$status, $body, $idempotencyKey];
}
#Заметки
- Не повторяйте с новым ключом. После сетевого сбоя повторяйте запрос с тем же
Idempotency-Key, иначе клиент получит два ответа, а кошелёк спишет два раза. - Один ключ — одна реплика. Не используйте один
Idempotency-Keyдля разных реплик. - Предел реплики — 4000 знаков; вложения в
POST /conversations/{id}/messagesне принимаются. - Тексты не возвращаются при записи. Ответ на сообщение клиента несёт идентификаторы и итог ответа ИИ; текст ответа читайте отдельным запросом с
fields=content— выдача попадёт в журнал выдач. - Канал
apiизолирован. Для него никогда не используется общий промпт площадки; ИИ отвечает по настройкам вашего агента и базе знаний арендатора. - Вложения и карточки. У сообщений есть
attachments(персональное поле, до 5 файлов, ссылки временные) иcards(без ПДн, до трёх). Карточки в диалог отправляетPOST /conversations/{id}/cards. - ПДн. Содержимое диалогов — персональные данные. В ячейке RU подключение интеграции с областями
conversations:*требует списка IP, страны размещения принимающей системы (только РФ; иначеpii_transfer_not_allowed) и основания передачи; передача выдач фиксируется в журнале выдач, который арендатор видит в кабинете и поGET /disclosures(Ячейки и данные). - Журнал изменений: 90-changelog.
#Открытые вопросы
- Тела и коды
409 invalid_stateдляtakeover,release,closeв закрытом диалоге; имена полейoperator-messages. Техническое. - Вложения клиента в серверном API записи сообщения (сейчас вложения только читаются). Техническое.