На странице
Обработка ошибок
Обрабатывайте ошибки Merchant API единообразно: формат ответа, группы HTTP-статусов, машинные коды, лимиты запросов и правила повторов.
API использует стандартные HTTP-коды. Успешные ответы возвращают тело ресурса напрямую. Ошибки соответствуют RFC 9457 Problem Details for HTTP APIs (application/problem+json) и добавляют поля сверх стандарта (extension members, RFC 9457 §3.2): верхнеуровневое code для обработки в коде и верхнеуровневое field (snake_case) при ошибке валидации по одному полю. Когда не проходят сразу несколько полей, field заменяется массивом errors[] с детализацией по каждому из них.
Переключайтесь по code для программной обработки и локализации — никогда не по тексту title или detail. Строковые поля могут переформулироваться между релизами; code — стабильный контракт.
{
"type": "https://paymos.io/docs/errors/codes#insufficient_balance",
"title": "Conflict",
"status": 409,
"detail": "Merchant balance is insufficient.",
"code": "insufficient_balance"
}
Любая ошибка соответствует RFC 9457 Problem Details for HTTP APIs, отдаётся как Content-Type: application/problem+json.
Одиночная ошибка (большинство ответов)
{
"type": "https://paymos.io/docs/errors/codes#insufficient_balance",
"title": "Conflict",
"status": 409,
"detail": "Merchant balance is insufficient.",
"code": "insufficient_balance"
}
Несколько ошибок валидации
Возвращается, только когда валидацию не проходят сразу несколько полей. Код верхнего уровня (code) всегда validation_failed; детализация по полям — в errors[]:
{
"type": "https://paymos.io/docs/errors/codes#validation_failed",
"title": "Bad Request",
"status": 400,
"detail": "Validation failed.",
"code": "validation_failed",
"errors": [
{ "code": "field_required", "field": "currency", "message": "Currency is required." },
{ "code": "field_must_be_positive", "field": "amount", "message": "Amount must be > 0." }
]
}
Описание полей
| Поле | Тип | RFC | Описание |
|---|---|---|---|
type |
string (URI) | RFC 9457 §3.1.1 | Прямая ссылка на строку документации с описанием конкретного code. После публикации не меняется. |
title |
string | RFC 9457 §3.1.3 | Краткий заголовок HTTP ("Bad Request", "Conflict", …). |
status |
integer | RFC 9457 §3.1.2 | HTTP-статус ответа (совпадает с кодом ответа). |
detail |
string | RFC 9457 §3.1.4 | Англоязычное объяснение конкретного случая. |
code |
string | extension (§3.2) | Стабильный машинно-читаемый идентификатор — см. Коды ошибок. Ветвите логику интеграции по нему (switch). |
field |
string | extension (§3.2) | Имя поля так, как оно приходит по сети (snake_case), для ошибок валидации по одному полю. Если ошибка не привязана к полю, ключа в JSON нет вообще — не null, а отсутствие ключа. Присутствует, только когда errors[] отсутствует. |
errors |
array | extension (§3.2) | Присутствует, только когда валидацию не проходят сразу несколько полей. Каждая запись: { code, field, message }. |
Зачем структурированные коды (а не только message)
Текст в message и detail может изменяться между версиями, локализоваться или дополняться контекстом. Стабильный контракт — это code. После публикации код никогда не меняет семантику. Используйте code для логики обработки и локализации.
import { RateLimitError, ValidationError } from '@paymos/sdk';
try {
await paymos.withdrawals.create(request);
} catch (error) {
if (error instanceof ValidationError) {
for (const item of error.errors) highlight(item.field, item.message);
} else if (error instanceof RateLimitError) {
scheduleRetry(error.retryAfterSeconds);
}
throw error;
}
use paymos::{ApiErrorKind, Error};
match paymos.withdrawals().create(&request).await {
Ok(withdrawal) => process(withdrawal),
Err(Error::Api(error)) if error.kind == ApiErrorKind::Validation => {
for item in &error.errors {
highlight(item.field.as_deref(), &item.message);
}
}
Err(Error::Api(error)) if error.kind == ApiErrorKind::RateLimit => {
schedule_retry(error.retry_after);
}
Err(error) => return Err(error),
}
Типы ошибок
Каждый статус приходит в конверте ошибки с полем title и машиночитаемым code. Один статус может покрывать несколько сценариев — ветвитесь по code, а не по статусу.
| HTTP-код | title |
Описание |
|---|---|---|
400 |
Bad Request |
Ошибка валидации входных данных или неверное тело запроса. Ошибки по одному полю несут code + field на верхнем уровне; ошибки по нескольким полям заполняют errors[]. |
401 |
Unauthorized |
Учётные данные HMAC отсутствуют, неверны, устарели или имеют неправильный формат. Конкретная причина — в code (invalid_credentials, timestamp_expired, authorization_malformed, …). |
403 |
Forbidden |
Аутентификация прошла, но тип ключа, окружение или права доступа не разрешают операцию. Конкретная причина — в code (merchant_suspended, whitelist_required и т.п.). |
404 |
Not Found |
Ресурс не существует, недоступен вызывающей стороне (чужой ресурс возвращает not_found, а не forbidden), либо ни один эндпоинт не обрабатывает этот путь (route_not_found). |
405 |
Method Not Allowed |
HTTP-метод недопустим для этого пути. code = method_not_allowed. |
409 |
Conflict |
Запрос конфликтует с текущим состоянием — недопустимый переход, ресурс уже существует или его блокирует проверка. Конкретная причина — в code (insufficient_balance, withdrawal_quota_exceeded, invoice_cannot_be_cancelled и т.д.). |
410 |
Gone |
Ресурс больше нельзя использовать (например, истёкший или отменённый инвойс). |
413 |
Payload Too Large |
Тело запроса превышает лимит размера (1 MiB). code = payload_too_large. |
415 |
Unsupported Media Type |
Content-Type не application/json. code = unsupported_media_type. |
429 |
Too Many Requests |
Превышен лимит запросов для мерчанта. Перед повтором прочитайте заголовок Retry-After (секунды). |
500 |
Internal Server Error |
Сбой на нашей стороне. Повторите запрос с экспоненциальной задержкой. |
503 |
Service Unavailable |
Временная проблема (exchange_rate_unavailable, acceptance_disabled, outbound_frozen и т.п.). Повторите с экспоненциальной задержкой. |
Для логики клиента используйте именно code, а не HTTP-статус — несколько разных бизнес-сценариев делят один HTTP-код, и машинный code в ответе их различает.
Лимиты запросов
API по умолчанию допускает 30 запросов в секунду на мерчанта, а на POST /v1/invoices действует более строгий лимит — 5 запросов в секунду. Лимит считается на мерчанта, в окне в одну секунду: все ваши ключи расходуют общий бюджет. Оба лимита настраиваются индивидуально для мерчанта. При превышении вернётся 429 Too Many Requests с заголовком Retry-After: 1.
{
"type": "https://paymos.io/docs/errors/codes#rate_limited",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded (30 req/sec). Retry after 1 second.",
"code": "rate_limited"
}