Любая ошибка PayBot возвращается единым JSON-объектом с полем error, внутри которого — type, category, code, message и, для ошибок ввода, param. Ветвите обработку по category (главный переключатель UX), логику — по code, а request_id показывайте в поддержку. Так один обработчик покрывает все ошибки API, включая непойманные исключения.
Все ошибки, вплоть до внутренних 500, проходят через единый обработчик и имеют одинаковую форму — вам не нужно разбирать разные форматы для разных endpoint'ов.
Как устроено тело ошибки
Ошибка приходит в конверте {"error": {...}}.
{
"error": {
"type": "validation_error",
"code": "invalid_parameter",
"message": "expires_at must be in the future",
"request_id": "req_8f3c9a12b7d4",
"param": "expires_at",
"category": "validation"
}
}Поля объекта error:
| Поле | Что это | Как использовать |
|---|---|---|
type | класс ошибки: authentication_error, api_error, validation_error, rate_limit_error, proxy_error (встречаются также authorization_error, not_found, not_implemented) | выбор канала показа |
code | машинный код | ветвление логики, ключ для i18n |
message | человекочитаемый текст (часть уже на русском и готова к показу) | fallback-текст |
request_id | ставится автоматически | «Сообщите поддержке ID: …» |
param | имя поля — только для validation | подсветка конкретного инпута |
category | auth, rate_limit, validation, payment, temporary, permanent | главный переключатель UX |
Если category не задана явно, она выводится из type: authentication_error → auth, rate_limit_error → rate_limit, validation_error → validation, proxy_error → temporary, api_error → permanent.
Как показывать ошибку по category
category — это и есть решение, что показать пользователю. Одна ветка на категорию покрывает подавляющее большинство случаев.
| category | Смысл | Что делать в UI |
|---|---|---|
auth | токен или ключ невалиден/отозван | разлогинить → экран входа. Не показывать красный тост |
validation | плохой ввод | inline-подсказка под полем param; если param нет — над формой |
rate_limit | слишком часто или исчерпан лимит плана | нейтральный тост «повторите». Для plan_limit_exceeded — апселл, а не ошибка |
temporary | Kaspi или сеть тупят | «Попробуйте ещё раз» + кнопка ретрая. Не пугать пользователя |
permanent | ретраем не исправится | явная ошибка + request_id |
payment | объявлена в схеме, но в коде не проставляется | обрабатывать как permanent |
HTTP-статусы
HTTP-код грубо совпадает с type, но окончательное решение принимайте по category и code, а не по статусу.
- •
400— ошибка ввода (validation_error). - •
401— не авторизован: невалидный ключ, истёкший или отозванный токен. - •
403— доступ запрещён (authorization_error), например действие заблокировано под импersonation. - •
404— объект не найден. - •
409— конфликт состояния (идемпотентность, неизменяемая ссылка). - •
429— превышен лимит запросов или лимит плана. - •
500— внутренняя ошибка. - •
501— функция отключена (например загрузка логотипа). - •
502/503— временные проблемы на стороне Kaspi или сессии.
Коды, которые обязан знать фронт
Ниже — коды с готовой реакцией интерфейса.
| HTTP | code | Реакция |
|---|---|---|
| 401 | invalid_api_key, invalid_token, token_expired, token_revoked | разлогин / переввод ключа |
| 401 | impersonation_session_expired | выход из режима партнёрского входа |
| 403 | impersonation_blocked_action | заранее дизейблить кнопку; текст уже на русском |
| 403 | not_owner | 404-подобный экран |
| 400 | idempotency_key_required (param: Idempotency-Key) | баг фронта — не показывать пользователю |
| 409 | idempotency_conflict | «запрос уже выполняется» — не дублировать |
| 400 | unknown_event | подсветить чекбоксы событий вебхука |
| 400 | bad_color / bad_email (param) | inline в форме брендинга |
| 400 | invalid_parameter (param) | inline под полем |
| 404 | payment_link_not_found, merchant_not_found, client_not_found | пустое состояние |
| 409 | payment_link_immutable, payment_link_not_active, payment_link_already_paid | дизейблить действие по status заранее |
| 429 | rate_limit_exceeded | бэкофф по X-RateLimit-Reset |
| 429 | plan_limit_exceeded | апселл (текст уже на русском) |
| 429 | payment_in_progress | «подождите 30 секунд» |
| 409 | trial_already_used | скрыть кнопку триала |
| 501 | logo_upload_disabled | fallback на ввод URL |
| 502 | kaspi_error (category: temporary) | кнопка «Обновить QR» |
| 503 | session_expired (proxy_error) | баннер «переподключите Kaspi» во всём кабинете |
| 503 | payment_unavailable, qr_creation_failed, qr_failed | «оплата временно недоступна» |
| 500 | internal_error | общий экран + request_id |
Особый случай — 503 session_expired: он возвращается на защищённых API-key endpoint'ах, когда сессия Kaspi мертва, а ключ не в тестовом режиме. Это не «ошибка запроса», а сигнал показать баннер переподключения Kaspi по всему кабинету.
Как обрабатывать ошибки на клиенте
Один обработчик разбирает конверт и маршрутизирует по category, а частные code уточняют поведение.
import requests
class PayBotError(Exception):
def __init__(self, err: dict):
self.type = err.get("type")
self.code = err.get("code")
self.message = err.get("message")
self.category = err.get("category")
self.param = err.get("param")
self.request_id = err.get("request_id")
super().__init__(self.message)
def call(method: str, url: str, **kwargs) -> dict:
resp = requests.request(method, url, **kwargs)
if resp.status_code >= 400:
raise PayBotError(resp.json().get("error", {}))
return resp.json()
def handle(err: PayBotError):
if err.category == "auth":
logout() # красный тост не показываем
elif err.category == "validation":
show_field_error(err.param, err.message) # или над формой, если param пуст
elif err.category == "rate_limit":
if err.code == "plan_limit_exceeded":
show_upsell()
else:
toast(err.message) # нейтрально, «повторите»
elif err.category == "temporary":
show_retry(err.message) # кнопка «Попробовать снова»
else: # permanent и payment
show_error(f"{err.message}. ID: {err.request_id}")Ключевые принципы: auth не показывают тостом (это разлогин), validation крепят к полю по param, temporary дают ретрайнуть, а permanent показывают вместе с request_id для поддержки.
FAQ
По какому полю ветвить обработку — type или category?
По category — это осознанный UX-переключатель на бэке. type полезен для грубого канала показа, а code — для точечной логики (например plan_limit_exceeded → апселл). category покрывает случаи, которые ещё не имеют отдельного кода.
Что делать с request_id?
Показывайте его пользователю при permanent/internal_error («Сообщите поддержке ID: …») и логируйте на своей стороне. Тот же request_id виден в GET /me/logs, что связывает ошибку с конкретным запросом.
Почему при validation иногда нет param?
param заполняется только когда ошибка привязана к конкретному полю. Если его нет — покажите message над формой целиком, а не под инпутом.
Категория payment — как её обрабатывать?
Как permanent. Значение payment объявлено в схеме ErrorDetail, но в коде не проставляется, поэтому не закладывайте на неё отдельную ветку UX.
Смежные разделы
- •Лимиты запросов — коды
429, заголовки и стратегия retry. - •Аутентификация — коды
401/403и работа с токенами. - •Идемпотентность — коды
idempotency_key_requiredиidempotency_conflict.