#Соглашения API
Статус: Доступно. Общие правила, одинаковые для всех конечных точек серверного API.
Назначение: формат запросов и ответов, ошибки и полный каталог кодов, пагинация, идемпотентность, асинхронные операции, пределы частоты, версионирование и платные операции.
Примеры JSON ниже иллюстративны: имена необязательных полей уточнятся в спецификации OpenAPI 3.1.
#Содержание
- Формат запросов и ответов
- Ошибки
- Каталог кодов ошибок
- Пагинация
- Идемпотентность
- Асинхронные операции
- Пределы частоты
- Версионирование
- Платные операции
- Кошелёк и остановка обмена
#Формат запросов и ответов
| Правило | Значение |
|---|---|
| Базовый адрес | https://<домен ячейки>/api/public/v1 (Ячейки и данные) |
| Транспорт | HTTPS (TLS) |
| Кодировка и формат | JSON, UTF-8. Для запросов с телом — Content-Type: application/json |
| Авторизация | Authorization: Bearer <ключ> (Аутентификация) |
| Размер тела запроса | не более 256 КБ |
| Даты и время | ISO 8601 в UTC с суффиксом Z: 2026-10-11T08:15:30Z. Для дневных агрегатов — YYYY-MM-DD |
| Деньги | объект {"amount": "123.45", "currency": "RUB"}; сумма — строкой, никогда не числом с плавающей точкой |
| Валюта | валюта кошелька арендатора: RUB для RU, GBP для EN, IDR для ID |
| Идентификаторы | непрозрачные строки; не разбирайте и не сравнивайте их по формату |
| Неизвестные поля | в ответах могут появляться новые поля — клиент их игнорирует (обратная совместимость в v1) |
| CORS | отсутствует: API предназначен только для вызовов «сервер–сервер» |
#Заголовки ответа
| Заголовок | Когда | Смысл |
|---|---|---|
X-Request-Id | всегда | Идентификатор запроса. Совпадает с request_id в теле ошибки. Указывайте при обращении в поддержку |
RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset | всегда | Состояние предела частоты (ниже) |
Retry-After | 429, 503 | Через сколько секунд повторить |
Deprecation, Sunset | если конечная точка устаревает | См. Версионирование |
#Заголовки запроса
| Заголовок | Обязательно | Назначение |
|---|---|---|
Authorization | да | Ключ |
Content-Type | если есть тело | application/json |
Idempotency-Key | для POST и PATCH | Защита от дублей, см. Идемпотентность. Для приёма сообщения клиента обязателен. Для PUT и DELETE принимается и игнорируется |
If-Match | для PATCH /leads/{id} | Версия лида: оптимистичная блокировка |
#Ошибки
Все ошибки — application/problem+json по RFC 9457. Текст ошибки не содержит трассировок стека и сведений о базе данных.
#Поля
| Поле | Тип | Всегда | Описание |
|---|---|---|---|
type | URI | да | Ссылка на описание кода в документации: https://<домен ячейки>/developers/errors#<code> |
title | строка | да | Краткое название, одинаковое для кода |
status | число | да | HTTP-статус |
code | строка | да | Стабильный машинный код (каталог). Ветвите логику по code, а не по тексту |
detail | строка | да | Пояснение к конкретному случаю. Текст для человека — не парсите |
request_id | строка | да | Идентификатор запроса |
errors | массив | нет | Ошибки проверки по полям (только для validation_failed): объекты {field, code, message} |
correct_base_url | строка | нет | Только для wrong_cell: базовый адрес ячейки, в которой выпущен ключ |
Элемент errors[]:
| Поле | Тип | Описание |
|---|---|---|
field | строка | Путь к полю запроса в формате JSON Pointer (RFC 6901): /limit, /events/0. Для параметров адреса — /<имя параметра> |
code | строка | Машинный код причины, например out_of_range, required, invalid_format |
message | строка | Пояснение для человека |
#Пример
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
X-Request-Id: req_EXAMPLE0003
{
"type": "https://formula-cream.pro/developers/errors#validation_failed",
"title": "Запрос не прошёл проверку",
"status": 422,
"code": "validation_failed",
"detail": "Один или несколько параметров некорректны.",
"request_id": "req_EXAMPLE0003",
"errors": [
{ "field": "/limit", "code": "out_of_range", "message": "Должно быть от 1 до 100." }
]
}
#Как обрабатывать
| Класс | Что делать |
|---|---|
400, 413, 422 | Исправить запрос. Повтор без изменений бессмыслен |
401 | Проверить ключ и адрес ячейки. Автоматически не повторять |
402 | wallet_suspended — пополнить кошелёк по topup_url из ответа, обмен возобновится сам; insufficient_funds — пополнить кошелёк; spend_limit_reached — дождаться сброса предела трат |
403 | Проверить область, права, IP, тариф |
404 | Ресурса нет или он принадлежит другому арендатору (это одно и то же для клиента) |
409, 412, 428 | Конфликт состояния: перечитайте ресурс и повторите с актуальной версией (для operation_in_progress — подождите) |
429 | Подождать Retry-After секунд, затем повторить с тем же Idempotency-Key |
5xx | Повторять с экспоненциальной паузой и тем же Idempotency-Key (в том числе 503 service_unavailable и 503 upstream_unavailable) |
#Каталог кодов ошибок
HTTP-статусы кодов зафиксированы. Описание каждого кода доступно по адресу из поля type: https://<домен ячейки>/developers/errors#<code>.
#Аутентификация и доступ
| Код | HTTP | Когда | Что делать |
|---|---|---|---|
invalid_api_key | 401 | Заголовка нет, ключ не найден, отозван или истёк. Ответ намеренно одинаков для всех случаев: платформа не подтверждает, что такой ключ существовал | Проверить ключ; срок действия виден в GET /me (key.expires_at); при необходимости выпустить новый |
wrong_cell | 401 | Ключ выпущен в другой ячейке. В ответе — поле correct_base_url | Использовать базовый адрес своей ячейки (correct_base_url) |
signature_invalid | 401 | Зарезервирован для усиленного режима подписи запроса. В v1 не возвращается | — |
scope_missing | 403 | Нет нужной области, либо права срезаны ролью интеграции, правами владельца или тарифом | Проверить права запроса |
ip_not_allowed | 403 | Запрос с адреса вне списка ключа | Добавить адрес в список или вызывать с разрешённого |
feature_not_in_plan | 403 | Лицензия или тариф не включают функцию (api_access или конкретную возможность) | Сменить тариф или обратиться в поддержку |
pii_transfer_not_allowed | 403 | Ячейка RU: интеграция запрашивает область с ПДн (conversations:*, leads:*) или include_pii, а страна размещения принимающей системы не РФ | Разместить принимающую систему в РФ или не запрашивать области с ПДн и include_pii |
#Запрос и состояние ресурса
| Код | HTTP | Когда | Что делать |
|---|---|---|---|
validation_failed | 422 | Параметры не прошли проверку; детали в errors[] | Исправить запрос |
not_found | 404 | Ресурса нет либо он принадлежит другому арендатору | Проверить идентификатор |
payload_too_large | 413 | Тело запроса больше 256 КБ | Уменьшить тело |
idempotency_conflict | 409 | Idempotency-Key уже использован с другим телом запроса | Использовать новый ключ для нового запроса либо повторить тот же запрос |
operation_in_progress | 409 | Параллельный повтор: операция с этим Idempotency-Key ещё выполняется | Подождать и повторить через Retry-After либо опросить операцию |
invalid_state | 409 | Действие недопустимо в текущем состоянии ресурса (например, отклонить тему стратега не из состояния proposed) | Перечитать ресурс и проверить его состояние |
version_conflict | 412 | If-Match не совпал с текущей версией ресурса (его изменили) | Перечитать ресурс, применить правку к актуальной версии |
precondition_required | 428 | Для изменения не передан If-Match | Передать версию из последнего чтения |
editorial_profile_required | 409 | Для генерации статей не заполнен профиль редакции арендатора | Заполнить профиль в кабинете |
#Деньги и пределы
| Код | HTTP | Когда | Что делать |
|---|---|---|---|
wallet_suspended | 402 | Обмен остановлен: баланс кошелька арендатора отрицательный или кошелёк заблокирован. Любой маршрут, кроме GET /me и GET /wallet, отвечает этим кодом; тело содержит topup_url и balance. Подробнее — Кошелёк и остановка обмена | Пополнить кошелёк по topup_url; обмен возобновится сам не позже чем через 30 секунд после зачисления |
insufficient_funds | 402 | Недостаточно средств на кошельке для платной операции | Пополнить кошелёк |
spend_limit_reached | 402 | Достигнут предел трат (на арендатора или интеграцию), предел одновременных платных операций или максимум стоимости одной операции | Дождаться окна или повысить предел в кабинете |
rate_limited | 429 | Превышен предел частоты | Выждать Retry-After |
#Платформа
| Код | HTTP | Когда | Что делать |
|---|---|---|---|
upstream_unavailable | 503 | Недоступна внешняя зависимость, например языковая модель | Повторить с паузой и тем же Idempotency-Key |
service_unavailable | 503 | Недоступно хранилище пределов: платформа не может безопасно принять запись или платную операцию. Содержит Retry-After | Повторить с паузой и тем же Idempotency-Key |
#Не ошибки: причины отсутствия ответа ИИ
Эти значения приходят в теле успешного ответа, а не как ошибка. Приём сообщения клиента прошёл успешно; ИИ-продажник не ответил по названной причине. Состояние ответа ИИ передаётся полем ai_reply в ответе на сообщение клиента и в событии message.created; подробно — Серверный API диалогов.
ai_reply — объект {status, reason?}:
ai_reply.status | Смысл |
|---|---|
replied | ИИ ответил (ответ — отдельное сообщение диалога) |
queued | Ответ готовится: ждите событие message.created либо опросите операцию |
skipped | ИИ не ответит; причина в reason |
ai_reply.reason (при skipped) | Смысл |
|---|---|
operator_active | Диалог ведёт оператор, ИИ не отвечает |
spam | Сообщение признано спамом |
flow_reply | Ответил сценарий (поток), а не ИИ |
ai_unavailable_handoff | ИИ недоступен; диалог передан оператору |
no_agent | Для канала не назначен агент |
daily_limit | Исчерпан суточный предел ответов ИИ в канале: сообщение сохранено, диалог получает состояние handoff_pending, ответа ИИ не будет |
Не воспринимайте эти значения как сбой: сообщение принято и сохранено. В клиентском Chat API виджета тот же случай daily_limit сообщается полем queued: true (клиентский Chat API).
#Пагинация
Списки возвращаются курсором («keyset»), а не номерами страниц.
GET /api/public/v1/leads?limit=50&cursor=eyJ2IjoxLC4uLn0 HTTP/1.1
| Параметр | Тип | Обязательно | Ограничения | Пример |
|---|---|---|---|---|
limit | целое | нет | от 1 до 100; по умолчанию 25 | 50 |
cursor | строка | нет | значение next_cursor из предыдущего ответа; живёт 24 часа | eyJ2Ijox… |
include_deleted | логический | нет | true — вернуть также «надгробия» удалённых записей; по умолчанию false | true |
Ответ:
{
"data": [
{ "id": "lead_EXAMPLE01", "status": "new", "updated_at": "2026-10-11T08:15:30Z" }
],
"next_cursor": "eyJ2IjoxLC4uLn0"
}
Когда next_cursor равен null, список закончился.
#Свойства курсора
| Свойство | Описание |
|---|---|
| Порядок | по (updated_at, id), строго «больше» предыдущей позиции |
| Подпись | Курсор подписан платформой и привязан к арендатору, режиму ключа, позиции и набору фильтров. Подделать или перенести на другой запрос нельзя |
| Непрозрачность | Не разбирайте курсор и не конструируйте его |
| Фильтры | Меняя фильтры, начинайте обход заново, без cursor |
| Срок жизни | Курсор действителен 24 часа с момента выдачи. По истечении срока (или при недействительном курсоре) запрос вернёт 422 validation_failed с errors[].field = /cursor: начните обход заново |
| Агрегаты | Метрики и срезы аналитики считаются на стороне платформы и курсорами не обходятся |
#Что происходит при изменениях во время обхода
- Добавленные записи попадут в обход, если окажутся «после» текущей позиции.
- Изменённая запись получает новый
updated_atи поэтому может появиться повторно позже. Обрабатывайте записи как «upsert поid», а не «вставить». - Удалённые записи по умолчанию не возвращаются. С параметром
include_deleted=trueвместо них приходят «надгробия» — объекты{"id": "…", "deleted_at": "2026-10-11T08:15:30Z"}, чтобы вы могли убрать запись у себя. Надгробия хранятся 30 суток: синхронизируйтесь не реже. - Записи не пропускаются из-за изменений в соседних строках.
Для синхронизации в реальном времени используйте вебхуки, а периодический обход оставьте для сверки.
#Идемпотентность
Заголовок Idempotency-Key защищает от дублей при повторах после сетевых сбоев и тайм-аутов.
| Параметр | Значение |
|---|---|
| Где требуется | Все POST и PATCH. Для приёма сообщения клиента и других платных запросов — обязателен |
PUT и DELETE | Идемпотентны по природе: повтор даёт тот же результат. Заголовок Idempotency-Key принимается и игнорируется |
| Значение | Любая уникальная строка до разумной длины, обычно UUID v4, новая для каждой новой операции |
| Срок действия | 24 часа с первого запроса |
Поведение при повторе с тем же ключом:
| Ситуация | Результат |
|---|---|
| Тот же ключ, то же тело, операция завершена | Тот же результат: операция не выполняется заново, деньги не списываются второй раз. Ответ строится по идентификаторам созданных ресурсов, поэтому отражает их текущее состояние |
| Тот же ключ, другое тело | 409 idempotency_conflict |
| Тот же ключ, операция ещё выполняется | 409 operation_in_progress |
| Ключ старше 24 часов | Считается новой операцией |
Рекомендации:
- Генерируйте ключ до первой отправки и сохраняйте его вместе с задачей в своей очереди, чтобы повтор использовал тот же ключ.
- Для
429и5xxповторяйте запрос с тем же ключом. - Не используйте один ключ для разных операций.
Платформа не хранит ПДн в записях идемпотентности: повтор перерисовывает ответ из актуальных данных ресурса.
#Асинхронные операции
Долгие операции (ответ ИИ-продажника, генерация статьи, ход стратега) не держат соединение. Они подчиняются единой схеме: запрос возвращает 202 Accepted с объектом operation; результат можно подождать, опросить или получить событием.
POST … (Idempotency-Key) → 202 Accepted + {"operation": {…}}
│
┌──────────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
?wait=N (до 20 с): опрос событие вебхука
ждём то же задание GET /operations/{id} operation.completed
(и событие модуля)
#Конверт operation
Один и тот же объект возвращается из создающего запроса, из GET /operations/{id} и в событии operation.completed.
| Поле | Тип | Описание |
|---|---|---|
id | строка | Идентификатор операции |
type | строка | Вид операции, см. таблицу ниже |
status | строка | queued, running, succeeded, failed, canceled |
created_at | строка | Время создания, UTC |
completed_at | строка | Время завершения; только для succeeded, failed, canceled |
resource | объект {type, id} | Ресурс, к которому относится операция; по нему вы читаете результат |
error | объект | Только для failed: {code, message} — code из каталога либо код причины, названный на странице операции |
usage | объект | Только для платных операций: {cost: {amount, currency}, model} |
Пример 202:
{
"operation": {
"id": "op_EXAMPLE01",
"type": "conversation.ai_reply",
"status": "queued",
"created_at": "2026-10-11T08:15:30Z",
"resource": { "type": "message", "id": "msg_EXAMPLE01" }
}
}
Виды операций:
type | Запускается | resource.type | Где читать результат |
|---|---|---|---|
conversation.ai_reply | POST /conversations/{id}/messages | message | Серверный API диалогов |
article.generation | POST /articles/generation-jobs | generation_job | Генерация статей |
strategist.turn | POST /strategist/chat/sessions/{id}/turns | strategist_session | Разговор со стратегом |
Оценка лида (POST /leads/{id}/score) выполняется синхронно и в конверт не оборачивается.
#Способы получить результат
| Способ | Описание |
|---|---|
202 + operation | Операция принята. В теле — объект operation со статусом queued или running |
?wait=N | Параметр wait (секунды, не более 20) на создающем запросе просит подождать результат. Это ожидание того же задания, новое не запускается. Не дождались — вернётся 202 с текущим состоянием операции |
| Опрос | GET /operations/{id} возвращает конверт operation. Рекомендуемая пауза растёт: 1 с, 2 с, 4 с … |
| Событие | Событие operation.completed приходит при любом завершении (succeeded, failed, canceled); кроме него модуль присылает собственное событие (message.created, article.ready, strategist.turn.completed …). Подпишитесь на нужное (Вебхуки) и не опрашивайте |
#GET /operations/{id}
Возвращает операцию вашей интеграции. Отдельной области не требует: чтобы прочитать результат, нужны области самого ресурса (например, conversations:read). Операцию другой интеграции или другого арендатора вернёт как 404 not_found.
| Параметр | Тип | Обязательно | Ограничения | Пример |
|---|---|---|---|---|
id (путь) | строка | да | Идентификатор из конверта operation | op_EXAMPLE01 |
Ответ 200:
{
"operation": {
"id": "op_EXAMPLE01",
"type": "conversation.ai_reply",
"status": "succeeded",
"created_at": "2026-10-11T08:15:30Z",
"completed_at": "2026-10-11T08:15:41Z",
"resource": { "type": "message", "id": "msg_EXAMPLE01" },
"usage": { "cost": { "amount": "0.42", "currency": "RUB" }, "model": "model-EXAMPLE" }
}
}
Свойства:
- Задание восстанавливается после сбоя воркера; резерв платных средств возвращается, если операция не выполнена.
- Повторный запрос с тем же
Idempotency-Keyвозвращает ту же операцию. - Результат платной операции содержит
usage(см. ниже).
#Пределы частоты
Пределы считаются скользящим окном на ключ и на арендатора одновременно.
| Класс запросов | Предел на старте |
|---|---|
Чтение (GET) | 600 в минуту |
Запись (POST, PATCH, PUT, DELETE) | 120 в минуту |
| Платные операции | 30 в минуту |
Значения стартовые и могут измениться; пределы по тарифам не зафиксированы. Фактические значения для вашего ключа видны в GET /me (limits). Приём событий и офлайн-конверсий (analytics:write) относится к записи: 120 запросов в минуту, в пакете не более 100 событий.
#Заголовки
| Заголовок | Смысл |
|---|---|
RateLimit-Limit | Предел текущего класса запросов |
RateLimit-Remaining | Сколько запросов осталось в окне |
RateLimit-Reset | Через сколько секунд окно обновится |
Retry-After | При 429 и 503: через сколько секунд повторить |
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 23
Retry-After: 23
#Отказ хранилища пределов
- Для записи и платных операций режим «закрытый»: при недоступности хранилища пределов возвращается
503 service_unavailable(сRetry-After) — платформа не допустит неконтролируемых расходов. - Для чтения действует мягкий предел в памяти процесса, и запросы продолжают обслуживаться.
#Рекомендации
- Читайте
RateLimit-Remainingи замедляйтесь заранее. - Повторяйте
429послеRetry-Afterс «дрожанием» (jitter), а не лавиной. - Для массовой синхронизации используйте курсорный обход и вебхуки вместо частого опроса.
#Версионирование
- Версия входит в адрес:
/api/public/v1. - Внутри
v1вносятся только добавляющие изменения: новые конечные точки, новые необязательные параметры, новые поля ответа, новые значения перечислений там, где это оговорено. Клиент должен игнорировать неизвестные поля. - Несовместимое изменение выходит только в новой версии (
v2) и никогда не подменяетv1. - Устаревание объявляется заранее: заголовки
DeprecationиSunsetв ответах, запись в журнале изменений — не менее чем за 90 суток до отключения.
#Платные операции
Платные операции списывают деньги с кошелька арендатора: ответ ИИ-продажника, оценка лида, генерация статьи, ход стратега. Они выделены в отдельные области (Аутентификация).
#Защита от неконтролируемых расходов
| Механизм | Описание |
|---|---|
| Резерв | Перед выполнением сумма резервируется в той же транзакции, что и захват операции; при неуспехе резерв возвращается |
| Предел трат на арендатора | В валюте кошелька арендатора |
| Предел трат на интеграцию | В валюте кошелька арендатора |
| Предел одновременных платных операций | Ограничивает параллелизм. По умолчанию — 5 одновременных платных операций на арендатора |
| Максимум стоимости одной операции | Верхняя граница на один вызов. По умолчанию — эквивалент 100 ₽ в валюте кошелька |
| Предохранитель кошелька | Общий стоп арендатора |
| Предел частоты | 30 платных запросов в минуту |
Когда состояние кошелька неизвестно (источник баланса не ответил), платные операции закрыты: 503 upstream_unavailable с Retry-After. Бесплатное чтение при этом работает и несёт заголовок Sapport-Wallet-State: unknown.
Значения пределов настраиваются в кабинете арендатора; действующие значения параллелизма и дневной траты видны в GET /me (limits.paid_concurrency, wallet.daily_spend).
#Ответы
| Код | HTTP | Что произошло |
|---|---|---|
wallet_suspended | 402 | Обмен остановлен целиком (отрицательный баланс или блокировка кошелька), а не только платные операции |
insufficient_funds | 402 | Не хватает средств |
spend_limit_reached | 402 | Сработал один из пределов трат или параллелизма |
#Стоимость в ответе
Каждый успешный ответ платной операции, а также конверт operation платной операции содержит поле usage:
| Поле | Тип | Описание |
|---|---|---|
usage.cost | объект {amount, currency} | Стоимость в валюте кошелька арендатора; amount — строка |
usage.model | строка | Модель, выполнившая операцию |
Пример (иллюстративный) — оценка лида, синхронная платная операция:
{
"lead_id": "lead_EXAMPLE01",
"score": 72,
"usage": {
"cost": { "amount": "0.12", "currency": "RUB" },
"model": "model-EXAMPLE"
}
}
Тестовые ключи не списывают деньги (Аутентификация).
#Сводная шпаргалка
| Вопрос | Ответ |
|---|---|
| Как узнать, что повторять безопасно | Idempotency-Key + 429/5xx |
| Как узнать, что список кончился | next_cursor: null |
| Как узнать, что ресурс изменили до моей правки | 412 version_conflict |
| Как узнать, что ИИ не ответил | ai_reply: {status, reason} в теле 200/202, не ошибка |
| Как дождаться долгой операции | 202 + operation, затем ?wait≤20, GET /operations/{id} или operation.completed |
Что делать с 404 на «чужой» идентификатор | Считать, что ресурса нет |
Что делать, если пришёл неизвестный code | Ветвиться по status и показать detail |
#Кошелёк и остановка обмена
Если баланс кошелька арендатора ниже нуля или кошелёк заблокирован, обмен данными останавливается целиком: каждый маршрут, кроме GET /me и GET /wallet, отвечает 402 wallet_suspended ещё до проверки областей и до чтения тела запроса, а исходящие вебхуки не отправляются, а откладываются. Ответ несёт topup_url (где пополнить) и balance (остаток на момент отказа). Состояние всегда видно в GET /wallet и в блоке wallet ответа GET /me. Полное описание, состояния и примеры — в разделе Кошелёк и остановка обмена.