Перейти к содержимому

API

На странице

Обработка ошибок

Обрабатывайте ошибки 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"
}