Ошибки и ответы
Каждый публичный API-эндпоинт возвращает предсказуемые формы ответа. Ошибки несут стабильный машиночитаемый код в теле, чтобы Вы могли разветвлять на нём без парсинга английского.
Конверт ответа
Успешные ответы возвращают ресурс (или список ресурсов) напрямую. Нет верхнеуровневой обёртки — клиенты могут десериализовать прямо в типизированную модель:
GET /api/public/v1/org
{
"id": "e188fc87-…",
"name": "Acme Corp",
"plan": "growth",
…
}Ответы об отказе используют небольшую единую форму с кодом error как единственным обязательным полем. Опциональные поля дают контекст, помогающий исправить вызов:
{
"error": "insufficient_scope",
"required": "assignments:write",
"present": ["assignments:read", "users:read"]
}Справочник кодов состояния
| Статус | Когда видите | Что делать |
|---|---|---|
200 | Запрос успешен. Тело содержит ресурс или сообщение подтверждения. | Обрабатывайте тело. |
400 | Неправильный запрос: отсутствующее обязательное поле, неверное enum-значение, тело слишком большое, не парсящийся JSON. | Исправьте клиентский запрос — не повторяйте ту же полезную нагрузку. |
401 | Отсутствующий, неправильно сформированный, отозванный или истёкший токен. | Проверьте заголовок Authorization. Ротируйте токен, если он был отозван. |
403 | Токен валиден, но не имеет требуемой области. | Добавьте область в админ-интерфейсе; ответ несёт точное имя области. |
404 | Ресурс не существует или принадлежит другой организации. | Перезагрузите родительскую коллекцию. Не следуйте устаревшим ID между организациями. |
409 | Конфликт — например, email уже используется. | Рассматривайте как успех, если Ваша операция идемпотентна. |
413 | Тело запроса превышает лимит на эндпоинт. Возвращается Kestrel для жёсткого переполнения; эндпоинт SARIF также возвращает это как 400 body_too_large, когда объявленный Content-Length превышает 10 МБ. | Сократите полезную нагрузку. Для SARIF принимайте находки одного инструмента инкрементально. |
429 | Лимит запросов достигнут на этом API-ключе. | Уважайте Retry-After; см. Лимиты запросов. |
5xx | Проблема на стороне сервера. | Повторите с экспоненциальным backoff. Захватите заголовок ответа X-Request-Id и цитируйте его в любом тикете поддержки. |
Коды ошибок
Поле error стабильно между версиями — разветвляйте на нём в клиентском коде, а не на HTTP-статусе, так как один и тот же статус может нести разные коды:
| Код | Статус | Значение |
|---|---|---|
unauthorized | 401 | Нет пригодного bearer-токена в запросе. |
insufficient_scope | 403 | Токен в порядке, область отсутствует. Ответ включает required и present. |
rate_limited | 429 | Лимит превышен. См. заголовок Retry-After. |
user_not_found | 404 | User ID не существует или не в Вашей организации. |
team_not_found | 404 | Team ID не существует или не в Вашей организации. |
assignment_not_found | 404 | Assignment ID не существует или не в Вашей организации. |
empty_body | 400 | POST или PATCH отправлен без тела, где оно обязательно. |
body_too_large | 400 / 413 | Тело запроса превысило лимит эндпоинта. SARIF эмитирует это как 400 с maxBytes в теле, когда объявленный Content-Length слишком велик; Kestrel эмитирует голый 413, когда тело фактически переполняется. |
Request ID
Каждый ответ несёт заголовок X-Request-Id (UUID v4). Захватите его на стороне клиента и включите в запросы поддержки — это позволяет нам вытащить точный серверный trace для неудачного вызова. Для 5xx-ответов request ID — это единственный самый быстрый путь к диагнозу.
Ошибки валидации
Сбои валидации на телах POST и PATCH возвращают 400 с плоской error, описывающей первое сбойное правило. Многополевая поверхность валидации не часть v1 — исправьте первое сообщённое поле и повторите. Будущие версии могут добавить детали на поле; верхнеуровневый контракт error останется.
Разветвление на ошибках — пример
const res = await fetch(url, { headers });
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
switch (body.error) {
case 'rate_limited':
return retryWithBackoff(url);
case 'insufficient_scope':
throw new Error(`API key missing scope: ${body.required}`);
case 'unauthorized':
throw new Error('API key revoked or invalid — rotate the secret');
default:
throw new Error(`API error ${res.status}: ${body.error || 'unknown'}`);
}