Помилки
Сталий конверт відповіді та повний перелік кодів помилок із поясненнями.
Будь-яка помилка приходить в одному й тому самому конверті — незалежно від ендпоінта й причини:
{
"error": {
"code": "validation_error",
"message": "capacity must be a number",
"requestId": "0f2b8c1e-6a4d-4a4b-9a30-1c9a2e5f8b77"
}
}
code— стабільний машинний код. Він не змінюється й саме за ним потрібно писати логіку.message— пояснення для людини. Формулювання може змінитися будь-коли, не парсіть його.requestId— унікальний ідентифікатор запиту. Логуйте його: за ним підтримка знайде конкретний виклик у логах сервера.
HTTP-статус завжди узгоджений із кодом, тож перевіряти можна будь-що з двох — але code
точніший: два різні коди можуть ділити один статус.
Перелік кодів
| Код | HTTP | Коли трапляється | Що робити |
|---|---|---|---|
unauthorized | 401 | немає заголовка Authorization, ключ хибний, відкликаний або тариф компанії більше не містить API | перевірити ключ і тариф; повторювати без змін немає сенсу |
forbidden | 403 | ключу бракує потрібної області доступу; запит з IP поза переліком дозволених; немає прав на конкретну заявку | видати ключ із потрібними областями або виправити перелік IP |
not_found | 404 | об'єкта не існує або він належить іншій компанії | перевірити id; API навмисно не розрізняє ці два випадки |
conflict | 409 | інший запит із тим самим Idempotency-Key ще виконується | повторити за 1–2 секунди |
validation_error | 400, 422 | не пройшли валідацію тіло чи параметри; надіслано зайве поле (наприклад companyId); той самий Idempotency-Key використано з іншим тілом (422); заявку піднято занадто рано або вичерпано ліміт піднять | виправити запит; повтор без змін дасть той самий результат |
rate_limited | 429 | перевищено ліміт запитів за хвилину | зачекати згідно з Retry-After |
quota_exceeded | 429 | вичерпано добову квоту записів | продовжити наступної доби; Retry-After показує, скільки лишилось |
internal | 5xx | збій на нашому боці | повторити з експоненційною затримкою; якщо повторюється — надіслати requestId у підтримку |
Чому кодів саме стільки
Внутрішні причини відмови навмисно не виносяться в окремі коди. Наприклад, спроба підняти
заявку може провалитися через «занадто рано» або «вичерпано ліміт піднять» — обидві приходять як
validation_error, а конкретна причина лишається в message. Так набір кодів залишається
маленьким і стабільним: інтеграція, написана сьогодні, не зламається, коли на бекенді з'явиться
нова перевірка.
Із цього випливає практичне правило: розгалужуйте логіку за code, показуйте користувачеві
message, у підтримку надсилайте requestId.
Приклади
Немає заголовка авторизації:
{ "error": { "code": "unauthorized", "message": "Missing API key", "requestId": "…" } }
Ключ без потрібної області:
{ "error": { "code": "forbidden", "message": "Insufficient API key scope", "requestId": "…" } }
Зайве поле в тілі (тут — companyId, який завжди береться з ключа):
{ "error": { "code": "validation_error", "message": "property companyId should not exist", "requestId": "…" } }
Вичерпано добову квоту:
{ "error": { "code": "quota_exceeded", "message": "Daily write quota exceeded", "requestId": "…" } }
Обробка на клієнті
async function call(path, init) {
const res = await fetch(`${API_BASE}/api/public/v1${path}`, init);
if (res.ok) return res.json();
const { error } = await res.json();
switch (error.code) {
case "rate_limited":
case "quota_exceeded":
throw new RetryableError(error, Number(res.headers.get("Retry-After") ?? 60));
case "internal":
throw new RetryableError(error, 5);
case "unauthorized":
case "forbidden":
throw new ConfigurationError(error); // повтор не допоможе — потрібне втручання
default:
throw new RequestError(error); // виправляйте запит
}
}