Коды ответа: 400, 401, 402, 403, 404, 409, 413, 422, 429
Разбор каждого кода отказа ActionPulse по симптомам — какие машинные коды за ним стоят, как отличить причины друг от друга, чем проверить и что безопасно исправить.
На этой странице
- Как читать ответ
- Отказ авторизации и приостановка доступа
- 401 — запрос отклонён как неавторизованный
- 403 — доступ есть, но запрещён именно этот запрос
- 404 — ресурс не найден, хотя он существует
- 402 — оплата или пробный период
- Запрос не принят
- 413 — тело запроса больше предела
- 422 — запрос не прошёл проверку
- 400 — точечный отказ вместо 422
- Конфликт состояния
- 409 — состояние не позволяет выполнить запрос
- Ограничения и время
- 429 — слишком часто или закончился объём
- 408 и 504 — запрос не успел
- 503 — сервис временно недоступен
- Что повторять, а что нет
- Что проверить
Код HTTP говорит только о классе отказа. Настоящую причину несёт поле
error.code в теле ответа: за одним 403 стоят восемь разных кодов, за 429 —
три, а 413 внутри вообще помечен как validation_failed. Отсюда первое
правило: читайте тело, а не только статус.
Ниже — разбор каждого кода: чем отличаются его причины, что проверить и что безопасно исправить. Быстрый путь «событие не появилось» — в дереве диагностики, полный перечень кодов — в справочнике ошибок.
Как читать ответ
Все отказы приходят в одном конверте:
{ "error": { "code": "validation_failed", "message": "batch is empty", "details": {} } }
code — машинный, по нему пишут условия в коде. message — человекочитаемый и
может меняться, не завязывайтесь на него. details — необязательный объект,
состав которого зависит от кода: адрес оплаты, признак «можно повторить».
Учитывайте, что поверхностей две и они отвечают немного по-разному:
| Поверхность | База | Авторизация | Особенность |
|---|---|---|---|
| Приём событий | адрес приёма, /v1/track и другие /v1/* |
ключ проекта | 202 приходит и при частичных отказах |
| Основной API | /api/v1 |
токен доступа | отказ по правам скрывается за 404 |
Отказ авторизации и приостановка доступа
401 — запрос отклонён как неавторизованный
Вероятная причина. Ключ или токен отсутствует, не разбирается, отозван,
истёк либо передан недопустимым способом. Код всегда один — unauthorized, и
сообщение намеренно одинаковое для всех причин, чтобы по ответу нельзя было
подбирать ключи.
| Что произошло | Где встречается |
|---|---|
Заголовка Authorization нет вовсе |
сообщение missing credential |
| Значение не разбирается, отозвано или истекло | сообщение invalid credential |
Браузерный токен или серверный ключ передан в адресе ?key= |
в адресе принимается только pp_wk_… |
Не браузерный токен в поле credential тела запроса |
это поле только для pp_bt_… |
| Токен доступа основного API просрочен | срок жизни токена доступа — 15 минут |
Как проверить. Отправьте на приём заведомо пустой пакет: авторизация
проверяется до разбора тела, поэтому 422 означает, что ключ принят, а любой
другой ответ — что дело в ключе. Событий такой запрос не создаёт.
curl -sS -o /dev/null -w '%{http_code}\n' \
-X POST "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/track" \
-H "Authorization: Bearer $ACTIONPULSE_SERVER_KEY" \
-d '{"batch":[]}'
Для основного API тот же смысл несёт любой безопасный GET — например
/api/v1/projects/YOUR_PROJECT_ID с заголовком Authorization.
Безопасное исправление. Сверьте префикс ключа — первые 8 символов после
второго подчёркивания — со списком credentials проекта: там видно тип, срок и
статус. Если ключ активен, а запрос всё равно 401, проблема в сборке
заголовка: должно быть ровно Authorization: Bearer <значение>. Для основного
API обновите токен доступа, а не выпускайте новые ключи.
Если не помогло. Значение могло быть обрезано при копировании или подстановке шаблонизатором: у браузерных и серверных ключей секрет — ровно 32 символа. Полный разбор — в ключах и origin.
403 — доступ есть, но запрещён именно этот запрос
Вероятная причина. Аутентификация прошла, отказала авторизация. Отличать
причины нужно строго по error.code:
| Код | Что означает |
|---|---|
forbidden |
роли пользователя не хватает для действия |
credential_scope_forbidden |
тип ключа или его область действия не подходят для этого запроса |
credential_origin_forbidden |
origin браузерного запроса не совпал со списком токена |
credential_rotation_required |
ключ выпущен до разделения прав на связывание, нужна ротация |
feature_locked |
тариф действует, но возможность в него не входит |
feature_locked_trial |
возможность откроется после оплаты, в пробном периоде закрыта |
trial_pending_review |
аккаунт на проверке, приём приостановлен |
license_invalid |
только в установке на своих серверах: лицензия недействительна или просрочена |
Как проверить. Посмотрите error.code и details.feature, если он есть:
последний прямо называет заблокированную возможность. Для forbidden сравните
свою роль с требуемой (роли в организации: owner, admin, analyst, viewer), для credential_* —
посмотрите тип ключа в списке credentials проекта.
Безопасное исправление. Отказ по роли снимает администратор организации — повысив роль или выполнив действие сам. Отказ по ключу лечится ключом нужного типа: связывание идентификаторов — серверным, записи сессий — браузерным. Отказ по тарифу — переходом на подходящий тариф, правки запроса его не обходят.
Если не помогло. credential_rotation_required приходит с
details.rotation_required и касается только операций идентификации: ротация
ключа сохраняет весь профиль и решает вопрос. Разбор 403 по симптомам — в
ключах и origin.
404 — ресурс не найден, хотя он существует
Вероятная причина. У 404 три смысла, и они неразличимы намеренно:
объекта действительно нет; объект есть, но принадлежит другой организации или
другому проекту — отказ по правам скрыт под «не найдено», чтобы перебором
нельзя было узнать чужие идентификаторы; маршрут не смонтирован в этом режиме
установки — методы оплаты отсутствуют в установке на своих серверах, а
установка лицензии в облаке.
Как проверить. Запросите список, а не конкретный объект: GET /api/v1/projects возвращает только доступные вам проекты. Если
идентификатор из неудачного запроса в списке отсутствует — это второй случай, а
не первый.
Безопасное исправление. Проверьте, что работаете в нужной организации: токен
доступа привязан к организации, и проект другой организации не виден. Если нужен
доступ — попросите администратора пригласить вас. Для третьего случая сверьтесь
с режимом установки: в облаке GET /api/v1/billing отвечает полем mode, а
в установке на своих серверах сам отвечает 404 — это и есть признак режима.
Если не помогло. Убедитесь, что не потеряли базовый путь: у основного API это
/api/v1, а приём живёт на другом адресе и не под /api/v1 — запрос к
приёму по адресу кабинета даст 404 независимо от прав.
402 — оплата или пробный период
Вероятная причина. Приём и изменения приостановлены по биллингу, а не по технической причине. Два кода:
| Код | Что означает |
|---|---|
trial_expired |
пробный период закончился без оплаты |
subscription_required |
нет действующей подписки и нет живого пробного периода |
Ответ приходит на приём событий, на изменяющие запросы и на зависящие от тарифа
возможности. В details.billing_url, когда он есть, лежит адрес страницы оплаты.
Как проверить. Откройте раздел оплаты в кабинете или запросите состояние
подписки — GET /api/v1/billing (метод доступен в облаке).
Безопасное исправление. Оплатите тариф в кабинете. Никакие изменения клиента не помогут; повторы бессмысленны, пока подписка не восстановлена. Данные при этом не удаляются: приём стоит на паузе.
Если не помогло. Проверьте, что смотрите на нужную организацию: квоты,
подписка и пробный период — свойства организации, а не проекта. У только что
зарегистрированного аккаунта отказ может быть другим — 403
trial_pending_review.
Запрос не принят
413 — тело запроса больше предела
Вероятная причина. Превышен байтовый предел метода. Код внутри —
validation_failed, отдельного кода у 413 нет. Пределы разные:
| Запрос | Предел тела |
|---|---|
POST /v1/track |
256 КБ |
POST /v1/identify и POST /v1/alias |
64 КБ |
POST /v1/replay |
800 КБ вместе с обёрткой JSON и base64 |
POST /v1/picker/capture |
8 КБ |
Пределы остальных методов в контракте не зафиксированы — не рассчитывайте на конкретное число там, где оно не названо.
Как проверить. Измерьте тело, которое реально уходит, а не исходные данные:
в оболочке это printf '%s' "$BODY" | wc -c, в коде — длина сериализованной
строки в байтах.
Безопасное исправление. Для приёма режьте пакет: не больше 500
элементов и не больше 256 КБ в теле, по тому пределу, который наступит
раньше. Если одно событие не влезает в одиночку, уменьшайте свойства: предел
значения — 8 КБ.
Если не помогло. Считайте байты, а не символы: русская буква в UTF-8 — два байта, эмодзи — четыре. Подробности — в лимитах приёма.
422 — запрос не прошёл проверку
Вероятная причина. Основной код — validation_failed, и разбор тела
строгий: неизвестное поле в любом месте структуры отклоняет весь запрос, а не
игнорируется. У нескольких методов 422 несёт свой код:
| Код | Когда |
|---|---|
validation_failed |
форма запроса, пустой пакет, превышение числа элементов |
confirmation_required |
не передан обязательный заголовок подтверждения опасного действия |
disposable_email |
домен адреса при регистрации принадлежит сервису одноразовой почты |
license_invalid |
установка лицензии отклонена (только на своих серверах) |
Отдельный случай — правила именованных событий: у них в details.reason приходят
устойчивые значения segment_not_found, segment_cycle,
segment_complexity_limit, unsupported_segment_rule.
Как проверить. Сравните тело с контрактом метода в
справочнике API и проверьте его на лишние поля. Для приёма самые частые
сообщения — invalid JSON body, batch is empty, batch exceeds 500 items.
Безопасное исправление. Уберите свои поля из структуры запроса: на приёме всё
своё кладётся в props, а не рядом с ним. Для опасных операций добавьте
требуемый заголовок подтверждения — он существует, чтобы случайный запрос не
удалил данные.
Если не помогло. Сверьтесь с реестром событий проекта: в строгом режиме
несовпадение схемы приходит не как 422, а как поэлементный отказ
schema_violation внутри 202.
400 — точечный отказ вместо 422
Вероятная причина. 400 встречается ровно на трёх группах методов: захват
элемента выбором на странице; операции с конкретным правилом именованных
событий — чтение, изменение, удаление, включение и выключение; публичная
страница отписки от писем. Код внутри тот же validation_failed; на страницах
отписки вместо конверта приходит HTML.
Как проверить. Посмотрите путь запроса. У /v1/picker/capture проверьте
тело: selector до 128 байт, url до 2048 байт и обязательно http/https,
text до 64 байт. У правила именованных событий — идентификатор в пути: он
целый и положительный.
Безопасное исправление. Приведите тело к пределам метода. Для захвата
элемента проще перезапустить выбор из кабинета: токен захвата одноразовый и
живёт 10 минут, повторно использованный даёт 409.
Если не помогло. Отказ 400 от промежуточного узла — прокси, WAF,
балансировщика — выглядит иначе: обычно это HTML или пустое тело без конверта
error. Проверьте, доходит ли запрос до ActionPulse вообще.
Конфликт состояния
409 — состояние не позволяет выполнить запрос
Вероятная причина. Запрос корректен, но состояние не позволяет его выполнить. Коды различаются сильно, и лечатся они по-разному:
| Код | Когда |
|---|---|
privacy_job_active |
идёт удаление данных субъекта; identify и alias временно закрыты, details.retryable = true |
picker_token_consumed |
одноразовый токен захвата элемента уже использован |
demo_project |
операция недоступна для демонстрационного проекта: ключи приёма, импорты, повторное заполнение |
idempotency_conflict |
тот же ключ идемпотентности прислан с другим телом |
domain_has_org |
при регистрации: организация на этом корпоративном домене уже есть |
phone_already_used |
номер уже использовался для пробного периода менее 90 дней назад |
sms_unavailable |
подтверждение по SMS к текущему состоянию аккаунта не применимо |
| без отдельного кода | имя события или версия схемы в реестре уже заняты; задание импорта уже завершилось и не отменяемо |
Как проверить. Для privacy_job_active посмотрите активные задания удаления:
GET /api/v1/projects/YOUR_PROJECT_ID/gdpr/forget. Для idempotency_conflict
сравните тело повторного запроса с первым: при совпадении тот же ключ даёт 200
и заголовок Idempotency-Replayed.
Безопасное исправление. privacy_job_active — единственный случай, который
лечится ожиданием: повторите после завершения задания, признак
details.retryable для этого и стоит. Для конфликтов имён выберите другое имя
или измените существующую запись. Для одноразового токена — выпустите новый.
Если не помогло. Не «переоткрывайте» операцию новым ключом идемпотентности, если не уверены, что первая не применилась: сначала прочитайте состояние объекта. Отметку о деплое, например, безопаснее найти в списке отметок.
Ограничения и время
429 — слишком часто или закончился объём
Вероятная причина. Один статус, три разных кода, и путать их дорого:
| Код | Что означает | Повтор помогает |
|---|---|---|
rate_limited |
превышена частота запросов | да, после Retry-After |
quota_exceeded |
исчерпан объём событий тарифа | нет |
trial_quota_exceeded |
исчерпан объём событий пробного периода | нет |
rate_limited означает «слишком быстро» и лечится задержкой; оба остальных —
«объём закончился», и повторы только жгут лимит частоты.
Как проверить. Прочитайте и код, и заголовки:
curl -sS -D - -o /dev/null \
-X POST "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/track" \
-H "Authorization: Bearer $ACTIONPULSE_SERVER_KEY" \
-d '{"batch":[]}'
Retry-After в секундах приходит вместе с rate_limited. У ответов про объём
его нет — это надёжный признак, что ждать бесполезно.
Безопасное исправление. Соблюдайте Retry-After, дальше повторяйте с
экспоненциальной задержкой. Отправляйте пакетами: стоимость запроса на приёме
считается как большее из двух — число событий в пакете и размер тела в
килобайтах. При quota_exceeded меняйте тариф или дожидайтесь периода.
Если не помогло. Из браузера заголовок Retry-After скрипту недоступен:
приём намеренно не открывает странице список заголовков ответа для запросов
записи. Смотрите его во вкладке «Сеть». Подробности про бюджеты — в
ограничениях частоты.
408 и 504 — запрос не успел
Вероятная причина. Оба статуса несут один код query_timeout: запрос не
уложился в предел времени этого метода. Какой статус придёт, зависит от метода,
поэтому обрабатывайте 408 и 504 одинаково. Единого предела времени у API
нет — он свой у каждого метода.
Как проверить. Убедитесь, что это query_timeout, а не таймаут вашего
клиента или прокси: у отказа от ActionPulse есть конверт error, у обрыва
соединения — нет.
Безопасное исправление. Сузьте запрос: короче интервал дат, меньше фильтров и разбивок. Тяжёлые выборки берите выгрузкой, а не запросом отчёта. Повтор без сужения почти всегда даёт тот же результат.
Если не помогло. У потоковых выгрузок отказ выглядит иначе: статус остаётся
200, а результат приходит в трейлере X-PP-Export-Status со значением
timeout. Разбирайте каждую строку и проверяйте трейлер — его отсутствие не
является доказательством успеха.
503 — сервис временно недоступен
Вероятная причина. Недоступна одна из подсистем: реестр событий,
конфигурация сбора, состояние записей сессий, хранилище захвата. Важная деталь:
часть ответов 503 означает, что изменение уже применено и ожидает только
побочного эффекта. Считать 503 за «ничего не произошло» нельзя.
Как проверить. Посмотрите message — он называет подсистему. Затем
проверьте живость: GET /healthz отвечает 200 у обоих процессов, когда
их зависимости на связи.
Безопасное исправление. Для чтений — повторите позже. Для изменений с ключом идемпотентности повтор с тем же ключом безопасен и штатен. Для изменений без ключа сначала прочитайте состояние объекта.
Если не помогло. Если 503 приходит на конфигурацию сбора, трекер остаётся
в безопасном состоянии и выключает необязательный сбор: базовые события
продолжат идти, автосбор и записи — нет.
Что повторять, а что нет
| Код | Повторять |
|---|---|
408, 504, 429 rate_limited, 503, 5xx |
да, с задержкой; при 429 — после Retry-After, при таймауте — сузив запрос |
409 privacy_job_active |
да, после завершения задания |
400, 401, 402, 403, 404, 413, 422 |
нет, повтор не изменит результат |
429 quota_exceeded, trial_quota_exceeded |
нет, нужен другой тариф или следующий период |
Что проверить
- В коде клиента ветвление идёт по
error.code, а не только по статусу HTTP. - Ответы приёма проверяются на непустой
rejected—202не означает «всё принято». - Повторяются только коды из таблицы выше, с экспоненциальной задержкой и
уважением к
Retry-After. - В логи приложения попадают код, статус и префикс ключа — и никогда само значение ключа.
- Для выгрузок проверяется трейлер
X-PP-Export-Status, а не код ответа.