◆ Sapport для разработчиков Справочник API Каталог ошибок OpenAPI
Разделы

#Соглашения API

Статус: Доступно. Общие правила, одинаковые для всех конечных точек серверного API.

Назначение: формат запросов и ответов, ошибки и полный каталог кодов, пагинация, идемпотентность, асинхронные операции, пределы частоты, версионирование и платные операции.

Примеры JSON ниже иллюстративны: имена необязательных полей уточнятся в спецификации OpenAPI 3.1.

#Содержание

  1. Формат запросов и ответов
  2. Ошибки
  3. Каталог кодов ошибок
  4. Пагинация
  5. Идемпотентность
  6. Асинхронные операции
  7. Пределы частоты
  8. Версионирование
  9. Платные операции
  10. Кошелёк и остановка обмена

#Формат запросов и ответов

ПравилоЗначение
Базовый адрес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-After429, 503Через сколько секунд повторить
Deprecation, Sunsetесли конечная точка устареваетСм. Версионирование

#Заголовки запроса

ЗаголовокОбязательноНазначение
AuthorizationдаКлюч
Content-Typeесли есть телоapplication/json
Idempotency-Keyдля POST и PATCHЗащита от дублей, см. Идемпотентность. Для приёма сообщения клиента обязателен. Для PUT и DELETE принимается и игнорируется
If-Matchдля PATCH /leads/{id}Версия лида: оптимистичная блокировка

#Ошибки

Все ошибки — application/problem+json по RFC 9457. Текст ошибки не содержит трассировок стека и сведений о базе данных.

#Поля

ПолеТипВсегдаОписание
typeURIдаСсылка на описание кода в документации: 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Проверить ключ и адрес ячейки. Автоматически не повторять
402wallet_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_key401Заголовка нет, ключ не найден, отозван или истёк. Ответ намеренно одинаков для всех случаев: платформа не подтверждает, что такой ключ существовалПроверить ключ; срок действия виден в GET /me (key.expires_at); при необходимости выпустить новый
wrong_cell401Ключ выпущен в другой ячейке. В ответе — поле correct_base_urlИспользовать базовый адрес своей ячейки (correct_base_url)
signature_invalid401Зарезервирован для усиленного режима подписи запроса. В v1 не возвращается—
scope_missing403Нет нужной области, либо права срезаны ролью интеграции, правами владельца или тарифомПроверить права запроса
ip_not_allowed403Запрос с адреса вне списка ключаДобавить адрес в список или вызывать с разрешённого
feature_not_in_plan403Лицензия или тариф не включают функцию (api_access или конкретную возможность)Сменить тариф или обратиться в поддержку
pii_transfer_not_allowed403Ячейка RU: интеграция запрашивает область с ПДн (conversations:*, leads:*) или include_pii, а страна размещения принимающей системы не РФРазместить принимающую систему в РФ или не запрашивать области с ПДн и include_pii

#Запрос и состояние ресурса

КодHTTPКогдаЧто делать
validation_failed422Параметры не прошли проверку; детали в errors[]Исправить запрос
not_found404Ресурса нет либо он принадлежит другому арендаторуПроверить идентификатор
payload_too_large413Тело запроса больше 256 КБУменьшить тело
idempotency_conflict409Idempotency-Key уже использован с другим телом запросаИспользовать новый ключ для нового запроса либо повторить тот же запрос
operation_in_progress409Параллельный повтор: операция с этим Idempotency-Key ещё выполняетсяПодождать и повторить через Retry-After либо опросить операцию
invalid_state409Действие недопустимо в текущем состоянии ресурса (например, отклонить тему стратега не из состояния proposed)Перечитать ресурс и проверить его состояние
version_conflict412If-Match не совпал с текущей версией ресурса (его изменили)Перечитать ресурс, применить правку к актуальной версии
precondition_required428Для изменения не передан If-MatchПередать версию из последнего чтения
editorial_profile_required409Для генерации статей не заполнен профиль редакции арендатораЗаполнить профиль в кабинете

#Деньги и пределы

КодHTTPКогдаЧто делать
wallet_suspended402Обмен остановлен: баланс кошелька арендатора отрицательный или кошелёк заблокирован. Любой маршрут, кроме GET /me и GET /wallet, отвечает этим кодом; тело содержит topup_url и balance. Подробнее — Кошелёк и остановка обменаПополнить кошелёк по topup_url; обмен возобновится сам не позже чем через 30 секунд после зачисления
insufficient_funds402Недостаточно средств на кошельке для платной операцииПополнить кошелёк
spend_limit_reached402Достигнут предел трат (на арендатора или интеграцию), предел одновременных платных операций или максимум стоимости одной операцииДождаться окна или повысить предел в кабинете
rate_limited429Превышен предел частотыВыждать Retry-After

#Платформа

КодHTTPКогдаЧто делать
upstream_unavailable503Недоступна внешняя зависимость, например языковая модельПовторить с паузой и тем же Idempotency-Key
service_unavailable503Недоступно хранилище пределов: платформа не может безопасно принять запись или платную операцию. Содержит 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; по умолчанию 2550
cursorстроканетзначение next_cursor из предыдущего ответа; живёт 24 часаeyJ2Ijox…
include_deletedлогическийнетtrue — вернуть также «надгробия» удалённых записей; по умолчанию falsetrue

Ответ:

{
  "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: начните обход заново
АгрегатыМетрики и срезы аналитики считаются на стороне платформы и курсорами не обходятся

#Что происходит при изменениях во время обхода

Для синхронизации в реальном времени используйте вебхуки, а периодический обход оставьте для сверки.

#Идемпотентность

Заголовок Idempotency-Key защищает от дублей при повторах после сетевых сбоев и тайм-аутов.

ПараметрЗначение
Где требуетсяВсе POST и PATCH. Для приёма сообщения клиента и других платных запросов — обязателен
PUT и DELETEИдемпотентны по природе: повтор даёт тот же результат. Заголовок Idempotency-Key принимается и игнорируется
ЗначениеЛюбая уникальная строка до разумной длины, обычно UUID v4, новая для каждой новой операции
Срок действия24 часа с первого запроса

Поведение при повторе с тем же ключом:

СитуацияРезультат
Тот же ключ, то же тело, операция завершенаТот же результат: операция не выполняется заново, деньги не списываются второй раз. Ответ строится по идентификаторам созданных ресурсов, поэтому отражает их текущее состояние
Тот же ключ, другое тело409 idempotency_conflict
Тот же ключ, операция ещё выполняется409 operation_in_progress
Ключ старше 24 часовСчитается новой операцией

Рекомендации:

Платформа не хранит ПДн в записях идемпотентности: повтор перерисовывает ответ из актуальных данных ресурса.

#Асинхронные операции

Долгие операции (ответ ИИ-продажника, генерация статьи, ход стратега) не держат соединение. Они подчиняются единой схеме: запрос возвращает 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_replyPOST /conversations/{id}/messagesmessageСерверный API диалогов
article.generationPOST /articles/generation-jobsgeneration_jobГенерация статей
strategist.turnPOST /strategist/chat/sessions/{id}/turnsstrategist_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 (путь)строкадаИдентификатор из конверта operationop_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" }
  }
}

Свойства:

#Пределы частоты

Пределы считаются скользящим окном на ключ и на арендатора одновременно.

Класс запросовПредел на старте
Чтение (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

#Отказ хранилища пределов

#Рекомендации

#Версионирование

#Платные операции

Платные операции списывают деньги с кошелька арендатора: ответ ИИ-продажника, оценка лида, генерация статьи, ход стратега. Они выделены в отдельные области (Аутентификация).

#Защита от неконтролируемых расходов

МеханизмОписание
РезервПеред выполнением сумма резервируется в той же транзакции, что и захват операции; при неуспехе резерв возвращается
Предел трат на арендатораВ валюте кошелька арендатора
Предел трат на интеграциюВ валюте кошелька арендатора
Предел одновременных платных операцийОграничивает параллелизм. По умолчанию — 5 одновременных платных операций на арендатора
Максимум стоимости одной операцииВерхняя граница на один вызов. По умолчанию — эквивалент 100 ₽ в валюте кошелька
Предохранитель кошелькаОбщий стоп арендатора
Предел частоты30 платных запросов в минуту

Когда состояние кошелька неизвестно (источник баланса не ответил), платные операции закрыты: 503 upstream_unavailable с Retry-After. Бесплатное чтение при этом работает и несёт заголовок Sapport-Wallet-State: unknown.

Значения пределов настраиваются в кабинете арендатора; действующие значения параллелизма и дневной траты видны в GET /me (limits.paid_concurrency, wallet.daily_spend).

#Ответы

КодHTTPЧто произошло
wallet_suspended402Обмен остановлен целиком (отрицательный баланс или блокировка кошелька), а не только платные операции
insufficient_funds402Не хватает средств
spend_limit_reached402Сработал один из пределов трат или параллелизма

#Стоимость в ответе

Каждый успешный ответ платной операции, а также конверт 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. Полное описание, состояния и примеры — в разделе Кошелёк и остановка обмена.