Ошибки¶
Конверт ошибки¶
Любая ошибка, которую возвращает API, выглядит одинаково — три ключа:
message — это код ошибки: идентификатор в верхнем регистре вроде INVALID_INVOICE_AMOUNT, а
не фраза. Он стабилен, машиночитаем, и ветвиться нужно именно по нему. Отдельного поля code нет, и
разбора по полям, что именно не так с телом запроса, тоже нет: 400 INVALID_REQUEST_BODY говорит,
что тело отклонено, но не говорит, какое поле в этом виновато.
У двух ошибок есть четвёртый ключ, details. 422 NOTHING_TO_SWEEP добавляет сумму, оставшуюся
на адресе, и сетевую комиссию, в которую обошлось бы её перемещение; см.
свип депозитного адреса. 422 DESTINATION_NOT_ACTIVATED
добавляет minAmount — наименьшую выплату, которая создаёт счёт получателя; см.
вывод на RIPPLE. Других ошибок с details нет.
statusCode дублирует HTTP-статус. timestamp — момент формирования ошибки, в UTC.
Сопоставляйте по message, а не по статусу
Один статус достаётся сразу нескольким разным ситуациям. Тот же 403 покрывает и
заблокированного мерчанта, и IP вне белого списка, и чужой ресурс — три случая, на которые
реагировать надо по-разному. Статус лишь сужает круг причин, а конкретную называет message.
Общие коды¶
Ошибка, у которой нет более точного кода, отвечает общим кодом своего статуса — например,
несуществующий маршрут даёт 404 NOT_FOUND.
message |
Статус |
|---|---|
BAD_REQUEST |
400, а также любой другой 4xx без собственного кода |
UNAUTHORIZED |
401 |
FORBIDDEN |
403 |
NOT_FOUND |
404 |
CONFLICT |
409 |
UNPROCESSABLE_ENTITY |
422 |
TOO_MANY_REQUESTS |
429 |
SERVICE_UNAVAILABLE |
503. Есть в справочнике, но сейчас любой 5xx отвечает INTERNAL_ERROR; обрабатывайте оба одинаково |
INTERNAL_ERROR |
Любой 5xx; см. раздел про 5xx ниже |
Незнакомый код обрабатывайте по его статусу, по таблице из раздела повторы.
По статусам¶
400 — запрос сформирован неверно¶
message |
Причина |
|---|---|
INVALID_REQUEST_BODY |
Нет обязательного поля, у поля неверный тип, значение enum недопустимо или в теле есть поле, которого эндпоинт не знает |
INVALID_QUERY_PARAMETER |
Query-параметр эндпоинту неизвестен, передан дважды там, где значение одно, не передан или некорректен: фильтр по id — не UUID, в оценке комиссии вывода не передан chainId или tokenId, либо в GET /api/v1/tokens/rate идентификатор токена не передан или не UUID, сумма — не положительное целое в базовых единицах, или переданы и amountIn, и amountOut |
INVALID_PATH_PARAMETER |
Идентификатор в пути — не UUID, например walletId при свипе |
UNSUPPORTED_SORT_BY |
sortBy называет поле, по которому на этом эндпоинте сортировать нельзя |
TOO_MANY_FILTER_VALUES |
В один повторяющийся параметр фильтра передано больше 500 значений |
INVALID_DATE_FILTER |
Фильтр по дате не разбирается: settledFrom / settledTo в GET /merchant/fees. Вывернутый наизнанку диапазон — не ошибка: под него просто ничего не подходит |
400 повторять бессмысленно: запрос отклонят ровно так же.
401 — вас не опознали¶
message |
Причина |
|---|---|
INVALID_API_KEY |
Отсутствующий, некорректный, неизвестный или отозванный x-api-key |
Все четыре причины намеренно неразличимы — по ответу не понять, какая сработала.
403 — опознали, но не разрешили¶
message |
Причина |
|---|---|
CALLER_IP_NOT_ALLOWED |
Ключ верный, но адрес источника не входит в его белый список |
MERCHANT_BLOCKED |
Мерчант заблокирован |
PERMISSION_DENIED |
Указанный вами ресурс принадлежит другому мерчанту |
403 — это не только про запись
MERCHANT_BLOCKED выбрасывается при аутентификации, до выполнения маршрута, поэтому
относится и к чтению. Если ваш клиент трактует 403 как «в записи отказано», блокировка мерчанта
покажется ему сбоем одного отдельного запроса.
Обратите внимание на разницу между PERMISSION_DENIED и 404: существующий, но не ваш id даёт
403, а id, которого нет нигде, — 404. Поэтому 403 на GET подтверждает, что ресурс существует.
404 — ничего нет¶
message |
Что именует |
|---|---|
INVOICE_NOT_FOUND |
invoiceId |
INVOICE_PAYMENT_NOT_FOUND |
paymentId, или платёж не относится к этому инвойсу |
STATIC_WALLET_NOT_FOUND |
staticWalletId |
CHAIN_NOT_FOUND |
chainId |
TOKEN_NOT_FOUND |
tokenId, или токен не относится к этой сети |
WALLET_NOT_FOUND |
walletId при свипе, или адрес принадлежит другому мерчанту |
MERCHANT_NOT_FOUND |
Мерчанта, стоящего за вашим ключом |
TOKENS_RATE_NOT_FOUND |
Пару в GET /api/v1/tokens/rate сейчас невозможно оценить |
Две выделенные строки — это ошибки несоответствия, поданные как «объект не найден»: настоящий
paymentId под чужим invoiceId даст «платёж не найден», а настоящий tokenId в паре не с той
chainId — «токен не найден». Прежде чем решить, что id неверен, проверьте пару.
WALLET_NOT_FOUND — единственное исключение из правила «чужое — это 403» выше: свип
адреса другого мерчанта даёт 404, так что ответ не подтверждает, что такой id существует.
409 — уже существует¶
message |
Причина |
|---|---|
INVOICE_EXTERNAL_ID_ALREADY_EXISTS |
Инвойс с таким externalId уже существует у вашего мерчанта |
WITHDRAWAL_EXTERNAL_ID_CONFLICT |
Вывод с таким externalId у вашего мерчанта уже есть, но с другими параметрами |
WALLET_HAS_NO_OWNER |
POST /wallets/{walletId}/sweep для адреса, не закреплённого ни за одним мерчантом. Исправить это может только поддержка |
Это не идемпотентность
Повторный POST /api/v1/invoices с тем же externalId отклоняется; он не возвращает уже
созданный вами инвойс. Выходите из положения чтением:
Выводы ведут
себя наоборот — там повторный externalId с теми же параметрами возвращает существующую запись,
и только повтор с другими параметрами даёт 409.
422 — корректно, но недопустимо¶
Самое многочисленное семейство. Запрос разобран, ресурсы на месте — операцию не допускает текущее состояние.
message |
Где возникает |
|---|---|
INVALID_INVOICE_AMOUNT |
POST /invoices — expectedAmount нулевой, отрицательный или не похож на целое число базовых единиц |
INVALID_INVOICE_EXPIRATION |
POST /invoices — expiresInSeconds вне диапазона 60–604800 |
INVALID_INVOICE_STATUS_TRANSITION |
Смена статуса, запрещённая жизненным циклом |
EXPECTED_INVOICE_PAYMENT_CANNOT_BE_RESOLVED |
POST .../resolve для платежа EXPECTED, не провалившего скрининг |
INVOICE_PAYMENT_ALREADY_RESOLVED |
POST .../resolve для платежа, у которого resolution уже не PENDING |
REFUND_ADDRESS_REQUIRED |
resolution: "REFUNDED" отправлен без refundAddress |
REFUND_ADDRESS_NOT_ALLOWED |
refundAddress или refundTag отправлен с резолюцией, отличной от REFUNDED |
INVOICE_PAYMENT_NOT_REFUNDED |
POST .../refund до того, как платёж получил резолюцию REFUNDED |
INVOICE_PAYMENT_REFUND_ALREADY_RECORDED |
Возврат по этому платежу уже зарегистрирован |
INVALID_STATIC_WALLET_REFERENCE |
reference состоит только из пробелов или длиннее 128 символов после обрезки |
TOKEN_IS_DISABLED |
POST /invoices — токен не принимает платежи: его srcStatus равен DISABLED |
NOTHING_TO_SWEEP |
POST /wallets/{walletId}/sweep — на адресе нет ничего чистого сверх пыли. Несёт details |
SWEEP_AMOUNT_TOO_SMALL |
POST /wallets/{walletId}/sweep — слишком мало, чтобы перемещать. Не ожидается: на пыль отвечает NOTHING_TO_SWEEP; обрабатывайте так же |
INSUFFICIENT_MERCHANT_BALANCE |
POST /merchant/withdrawals — баланс не покрывает сумму и комиссию |
TOKEN_PRICE_UNAVAILABLE |
POST /merchant/withdrawals, GET /merchant/withdrawals/fee-quote — у токена нет курса, чтобы взять в нём комиссию |
FEE_EXCEEDS_MAX_FEE |
POST /merchant/withdrawals — комиссия оказалась выше maxFee |
AMOUNT_DOES_NOT_COVER_FEE |
POST /merchant/withdrawals — задан subtractFeeFromAmount, а комиссия не меньше amount |
RECIPIENT_ADDRESS_BLOCKED |
POST /merchant/withdrawals — адрес назначения не прошёл скрининг |
WITHDRAWAL_ADDRESS_NOT_WHITELISTED |
POST /merchant/withdrawals — ваш белый список выводов включён, а адреса назначения в нём для этой сети нет; на RIPPLE — нет с этим toTag |
DESTINATION_TAG_REQUIRED |
POST /merchant/withdrawals на RIPPLE — счёт получателя не принимает платежи без тега назначения, а toTag не передан |
DESTINATION_NOT_ACTIVATED |
POST /merchant/withdrawals на RIPPLE — счёта получателя ещё нет, а выплата меньше суммы, нужной для его создания. Несёт details |
422 без изменений повторять бессмысленно: должно поменяться либо состояние, либо сам запрос.
5xx — запрос не выполнен на нашей стороне¶
Любой 5xx отвечает INTERNAL_ERROR. Код не говорит, что именно сломалось; статус говорит, где
примерно.
| Статус | Значение |
|---|---|
500 |
Непредвиденная ошибка при обработке запроса |
502 |
Часть платформы за API дала сбой или не ответила — в том числе когда запрос не дошёл до места, где его классифицировали бы |
503, 504 |
Часть платформы за API была недоступна или не ответила вовремя. Обрабатывайте как 502 |
502 означает, что ответ неизвестен, а не что ничего не произошло
При чтении это безобидно — просто повторите. При записи операция могла успеть пройти до того, как
ответ потерялся. POST /merchant/withdrawals и POST /static-wallets повторяйте свободно: оба
идемпотентны по вашему собственному референсу — вывод, только если вы передали externalId.
POST /wallets/{walletId}/sweep можно повторять как есть. А перед повтором POST /invoices
сначала поищите запись по externalId; см. безопасные повторы.
Повторы¶
| Статус | Повторять? |
|---|---|
400, 422 |
Нет. Исправьте запрос или состояние |
401, 403 |
Нет. Исправьте ключ, белый список или блокировку |
404 |
Нет — разве что вы обгоняете ресурс, который ещё создаётся |
409 |
Нет. Вместо этого прочитайте существующую запись или отправьте новый externalId |
500, 502, 503, 504 |
Да, с задержкой |
Используйте экспоненциальную задержку с джиттером и ограничивайте число попыток. Бесконечные повторы
502 во время сбоя внешнего сервиса только добавляют ему нагрузки.
Пример обработчика¶
resp = requests.post(url, headers=headers, json=body, timeout=30)
if resp.ok:
return resp.json()
err = resp.json()
message = err.get("message")
if message == "INVOICE_EXTERNAL_ID_ALREADY_EXISTS":
return fetch_invoice_by_external_id(body["externalId"]) # для вас это не ошибка
if message == "MERCHANT_BLOCKED":
raise AccountBlocked() # позовите дежурного
if resp.status_code >= 500:
raise Retryable(message) # задержка и новая попытка
raise Permanent(f"{resp.status_code} {message}") # логируйте весь конверт
В каждой ветке пишите в лог весь конверт вместе с timestamp: именно по нему поддержка находит
конкретный запрос.