Errors
Единый обезличенный конверт ошибок (unified error envelope) для B2B API. Один и тот же формат возвращается на любую неуспешную операцию - валидация, аутентификация, недостаток средств, конфликт состояния или недоступность сервиса. Разбирайте ошибки по стабильному строковому полю code, а не по тексту message (текст локализован и может меняться).
/api/v1/* и заменяет (supersedes) легаси-формат ошибок с полями error_code / docs на верхнем уровне - кроме одного исключения, описанного ниже. Если ваш код всё ещё читает старые поля - переключитесь на error.code + error.request_id.
/api/v1/* возвращает плоскую форму с полем error_code на верхнем уровне (UNAUTHENTICATED при 401, B2B_ACCESS_REQUIRED при 403). Разбирайте обе формы: сначала верхнеуровневый error_code, затем вложенный error.code.
Responses
{
"message": "B2B access is not enabled for this account. Contact admin@fin-os.io to request B2B integration onboarding.",
"error_code": "B2B_ACCESS_REQUIRED",
"docs": "https://fin-os.io/docs/b2b"
}
Формат конверта (envelope)
Успех - всегда объект с ключом data. Ошибка - всегда объект с ключом error, внутри которого code, message и request_id. Поле fields встречается только у invalid_request, но не у каждого: ошибки валидации формата приходят с разбором по полям, а доменные отказы с тем же кодом (например, сумма не проходит бизнес-проверку) - без fields. Не полагайтесь на его присутствие.
Responses
{
"data": {
"...": "полезная нагрузка операции"
}
}
{
"error": {
"code": "string",
"message": "string",
"request_id": "req_...",
"fields": {
"field_name": ["сообщение валидации", "..."]
}
}
}
code- стабильный машиночитаемый идентификатор ошибки (см. таблицу ниже). Ветвите логику по нему.message- человекочитаемое, уже обезличенное сообщение. Никогда не раскрывает внутреннюю инфраструктуру. Для отображения пользователю пригодно как есть.request_id- уникальный идентификатор запроса форматаreq_<random>для обращения в поддержку.fields- присутствует только приinvalid_request; объект вида{ "имя_поля": ["ошибка", ...] }.
request_id (для поддержки)
Каждый ответ об ошибке содержит уникальный request_id формата req_<random> - префикс req_ плюс 24 случайных символа (например req_a1B2c3D4e5F6g7H8i9J0kLmN). Логируйте его на своей стороне и указывайте при обращении в поддержку: по этому идентификатору во внутреннем канале Finance OS хранится полная техническая детализация инцидента, которая никогда не отдаётся наружу. В merchant-ответ и merchant-лог попадает только обезличенная версия.
Коды ошибок
Полный реестр кодов. Каждый код жёстко привязан к одному HTTP-статусу. В таблице приведён точный текст сообщения по умолчанию (в кавычках) и семантика кода.
Общие
Error codes
invalid_request
422
optional
fields с разбором по полям; у доменных отказов fields нет.unauthorized
401
optional
Authorization: Bearer sk_live_… / sk_test_….forbidden
403
optional
not_found
404
optional
sk_test_ к live-ресурсу и наоборот) - намеренно 404, а не 403.method_not_allowed
405
optional
conflict
409
optional
Idempotency-Key с другими параметрами, попытка снять блокировку, наложенную комплаенсом.rate_limited
429
optional
Retry-After и X-RateLimit-*: повторяйте с экспоненциальной задержкой (backoff).service_unavailable
503
optional
internal_error
500
optional
request_id и обратитесь в поддержку.sandbox_unsupported
403
optional
sk_live_.merchant_suspended
403
optional
| Name | Type | Required | Description |
|---|---|---|---|
invalid_request
|
422
|
optional |
«Некорректные данные запроса.» - не прошла валидация тела/параметров. У ошибок формата ответ содержит объект fields с разбором по полям; у доменных отказов fields нет.
|
unauthorized
|
401
|
optional |
«Требуется аутентификация. Проверьте ключ API.» - ключ отсутствует, недействителен или отозван. Проверьте заголовок Authorization: Bearer sk_live_… / sk_test_….
|
forbidden
|
403
|
optional | «Доступ запрещён.» - ключ аутентифицирован, но у него нет прав на этот ресурс или действие (в том числе роль сотрудника на денежной операции). |
not_found
|
404
|
optional |
«Ресурс не найден.» - объект отсутствует. Также возвращается при попытке доступа к ресурсу из другого окружения (sk_test_ к live-ресурсу и наоборот) - намеренно 404, а не 403.
|
method_not_allowed
|
405
|
optional | «Метод не разрешён для этого адреса - проверьте HTTP-метод в документации.» - путь существует, но не поддерживает использованный HTTP-метод. |
conflict
|
409
|
optional |
«Конфликт состояния операции.» - состояние ресурса несовместимо с запросом: операция в терминальном статусе, повтор Idempotency-Key с другими параметрами, попытка снять блокировку, наложенную комплаенсом.
|
rate_limited
|
429
|
optional |
«Слишком много запросов. Повторите позже.» - превышен лимит частоты. Ответ несёт заголовки Retry-After и X-RateLimit-*: повторяйте с экспоненциальной задержкой (backoff).
|
service_unavailable
|
503
|
optional | «Сервис временно недоступен. Попробуйте позже.» - вышестоящий сервис временно недоступен или контур ещё не активирован. Запрос можно повторить позже. |
internal_error
|
500
|
optional |
«Внутренняя ошибка. Обратитесь в поддержку с указанным request_id.» - непредвиденная ошибка на стороне Finance OS. Сохраните request_id и обратитесь в поддержку.
|
sandbox_unsupported
|
403
|
optional |
«Песочница для этого сервиса недоступна - используйте боевой ключ.» - сервис не имеет тестового окружения, вызов возможен только ключом sk_live_.
|
merchant_suspended
|
403
|
optional | «Учётная запись мерчанта приостановлена - обратитесь в поддержку.» - ключ рабочий, но учётная запись приостановлена. Ошибка не в запросе: повтор не поможет. |
Клиенты
Error codes
verification_required
403
optional
duplicate_phone
409
optional
PATCH номер, который вы назначаете, принадлежит другому вашему клиенту. Создание (POST) эту ошибку не возвращает: там повтор номера идемпотентно отдаёт существующего клиента.customer_blocked
409
optional
charge, payout и удаление.customer_has_funds
409
optional
customer_has_wallets
409
optional
| Name | Type | Required | Description |
|---|---|---|---|
verification_required
|
403
|
optional | «Требуется завершить верификацию клиента.» - операция недоступна, пока клиент не прошёл верификацию. Сначала завершите верификацию. |
duplicate_phone
|
409
|
optional |
«Этот номер телефона уже занят другим клиентом.» - при PATCH номер, который вы назначаете, принадлежит другому вашему клиенту. Создание (POST) эту ошибку не возвращает: там повтор номера идемпотентно отдаёт существующего клиента.
|
customer_blocked
|
409
|
optional |
«Клиент заблокирован - операции по нему запрещены.» - по заблокированному клиенту запрещены вывод, charge, payout и удаление.
|
customer_has_funds
|
409
|
optional | «У клиента есть остатки на счетах - удаление запрещено.» - сначала выведите или спишите остатки, включая замороженные. |
customer_has_wallets
|
409
|
optional | «У клиента есть крипто-адреса или история движений - удаление запрещено.» - клиент с выданными адресами или движениями по кошельку не удаляется даже с нулевым остатком. |
Кошельки клиентов и мастер-счёт
Error codes
idempotency_key_required
400
optional
charge, payout и внутренних переводов мастер-счёта.insufficient_funds
402
optional
service_balance_insufficient
402
optional
currency, required и available, чтобы вы могли показать точную нехватку. Пополнение - Merchant Balance.wallets_disabled
403
optional
withdraw_disabled
403
optional
coin_not_allowed
422
optional
charge / payout).amount_below_minimum
422
optional
min_withdraw витрины тарифа и в предпросмотре вывода.invalid_address
422
optional
network и адрес из одной сети.withdraw_limit_exceeded
422
optional
max_per_tx, daily_left).aml_blocked
422
optional
customer.withdrawal.blocked.aml_screening_in_progress
409
optional
| Name | Type | Required | Description |
|---|---|---|---|
idempotency_key_required
|
400
|
optional |
«Требуется заголовок Idempotency-Key.» - обязателен для вывода, charge, payout и внутренних переводов мастер-счёта.
|
insufficient_funds
|
402
|
optional | «Недостаточно средств для операции.» - не хватает средств на счёте клиента (с учётом обеих комиссий) либо на счёте финансирования владельца при пополнении мастер-счёта. |
service_balance_insufficient
|
402
|
optional |
«Недостаточно средств на сервисном счёте. Пополните мастер-счёт.» - ответ дополнен полями currency, required и available, чтобы вы могли показать точную нехватку. Пополнение - Merchant Balance.
|
wallets_disabled
|
403
|
optional | «Крипто-кошельки клиентов сейчас недоступны.» - выдача адресов клиентам недоступна вашей учётной записи. |
withdraw_disabled
|
403
|
optional | «Вывод средств клиентов сейчас недоступен.» - вывод недоступен вашей учётной записи. |
coin_not_allowed
|
422
|
optional |
«Эта монета недоступна для вашей учётной записи.» - монета или сеть не поддерживаются, отключены для вас, либо адрес получателя принадлежит платформе (внутренние расчёты делаются через charge / payout).
|
amount_below_minimum
|
422
|
optional |
«Сумма меньше минимальной для этой монеты.» - минимум смотрите в min_withdraw витрины тарифа и в предпросмотре вывода.
|
invalid_address
|
422
|
optional |
«Некорректный адрес получателя для указанной сети.» - формат адреса не соответствует сети. Проверьте, что network и адрес из одной сети.
|
withdraw_limit_exceeded
|
422
|
optional |
«Превышен лимит вывода.» - сумма больше лимита на операцию либо суточного лимита клиента. Актуальные значения - в предпросмотре (max_per_tx, daily_left).
|
aml_blocked
|
422
|
optional |
«Операция отклонена комплаенс-проверкой.» - адрес назначения не прошёл проверку. Отказ приходит до списания; уходит вебхук customer.withdrawal.blocked.
|
aml_screening_in_progress
|
409
|
optional | «По клиенту идёт комплаенс-проверка поступления. Повторите позже.» - по клиенту есть поступление без вердикта. Состояние временное, повторите запрос через некоторое время. |
message предназначен для показа человеку и может меняться. Программную логику (retry, показ формы, пополнение баланса) стройте по error.code и HTTP-статусу.
Примеры тел ошибок
Реальные тела ответов для типичных ситуаций.
Responses
{
"error": {
"code": "idempotency_key_required",
"message": "Требуется заголовок Idempotency-Key.",
"request_id": "req_5Hj7Kd2Nf8Rt1Yq4Wp6Vb3M"
}
}
{
"error": {
"code": "unauthorized",
"message": "Требуется аутентификация. Проверьте ключ API.",
"request_id": "req_M9n8B7v6C5x4Z3a2S1d0F1gH"
}
}
{
"error": {
"code": "insufficient_funds",
"message": "Недостаточно средств для операции.",
"request_id": "req_7Kp2Rt9Wx4Yz1Qb6Nm3Vc8L"
}
}
{
"error": {
"code": "service_balance_insufficient",
"message": "Недостаточно средств на сервисном счёте. Пополните мастер-счёт.",
"request_id": "req_3Nd8Vc1Xz5Bm7Kq0Lp2Rt6W",
"currency": "USDT",
"required": "0.2",
"available": "0.0500000000"
}
}
{
"error": {
"code": "invalid_request",
"message": "Некорректные данные запроса.",
"request_id": "req_a1B2c3D4e5F6g7H8i9J0kLmN",
"fields": {
"amount_rub": ["The amount rub field must be at least 100."],
"bank_id": ["The bank id field is required."]
}
}
}
{
"error": {
"code": "rate_limited",
"message": "Слишком много запросов. Повторите позже.",
"request_id": "req_Qw3Er5Ty7Ui9Op1As2Df4Gh6"
}
}
{
"error": {
"code": "service_unavailable",
"message": "Сервис временно недоступен. Попробуйте позже.",
"request_id": "req_Zx8Cv6Bn4Mk2Lj0Hg9Fd7Sa5"
}
}
invalid_request, - service_balance_insufficient: к обязательным code, message и request_id добавляются currency, required и available. Их можно показать пользователю как есть: «не хватает 0.15 USDT». Остальные коды расширений не имеют - не рассчитывайте на них при разборе.
Что повторять, а что нет
Повторять с задержкой
429, 503
optional
rate_limited и service_unavailable - временные. На 429 опирайтесь на заголовок Retry-After, дальше - экспоненциальная задержка.Повторить позже, без изменений
409
optional
aml_screening_in_progress - состояние временное, запрос корректен. Остальные 409 повторять бессмысленно: сначала измените запрос или состояние ресурса.Исправить запрос
400, 422
optional
Пополнить счёт
402
optional
insufficient_funds) или мастер-счёт (service_balance_insufficient) и повторите.Обратиться в поддержку
403, 500
optional
merchant_suspended, wallets_disabled, withdraw_disabled снимаются на нашей стороне; для internal_error сохраните request_id.| Name | Type | Required | Description |
|---|---|---|---|
Повторять с задержкой
|
429, 503
|
optional |
rate_limited и service_unavailable - временные. На 429 опирайтесь на заголовок Retry-After, дальше - экспоненциальная задержка.
|
Повторить позже, без изменений
|
409
|
optional |
aml_screening_in_progress - состояние временное, запрос корректен. Остальные 409 повторять бессмысленно: сначала измените запрос или состояние ресурса.
|
Исправить запрос
|
400, 422
|
optional | Валидация, лимиты, адрес, монета, минимальная сумма. Повтор без изменений даст тот же ответ. |
Пополнить счёт
|
402
|
optional |
Пополните счёт клиента (insufficient_funds) или мастер-счёт (service_balance_insufficient) и повторите.
|
Обратиться в поддержку
|
403, 500
|
optional |
merchant_suspended, wallets_disabled, withdraw_disabled снимаются на нашей стороне; для internal_error сохраните request_id.
|
charge, payout и внутренние переводы мастер-счёта требуют заголовок Idempotency-Key. Повторяя запрос после таймаута или 503, отправляйте тот же ключ: так повтор вернёт результат первой операции с duplicate: true вместо второго списания. Новый ключ на ретрае - это новая операция и вторые деньги.
sk_test_… - sandbox, sk_live_… - live. Отдельного заголовка окружения нет. Обрабатывайте ошибки одинаково в обоих окружениях. См. Sandbox.
Примеры кода
# Любой /api/v1/* endpoint при неуспехе отдаёт конверт ошибки.
# Пример: недостаточно средств под sell → 402.
curl -i -X POST https://fin-os.io/api/v1/customers/$UUID/payments/sell \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"amount_usdt": 999999, "bank_id": "sber", "phone": "+79001234567"}'
# HTTP/1.1 402 Payment Required
# {
# "error": {
# "code": "insufficient_funds",
# "message": "Недостаточно средств для операции.",
# "request_id": "req_7Kp2Rt9Wx4Yz1Qb6Nm3Vc8L"
# }
# }
# Невалидное тело → 422 с объектом fields:
curl -i -X POST https://fin-os.io/api/v1/customers/$UUID/payments/buy \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{"amount_rub": 5}'
# HTTP/1.1 422 Unprocessable Entity
# { "error": { "code": "invalid_request", "request_id": "req_...",
# "fields": { "amount_rub": ["..."], "bank_id": ["..."] } } }
// Единый разбор конверта ошибки для всех вызовов /api/v1/*
async function apiCall(path, opts = {}) {
const res = await fetch(`https://fin-os.io/api/v1${path}`, {
...opts,
headers: {
'Authorization': `Bearer ${process.env.FINOS_SECRET_KEY}`, // sk_test_ или sk_live_
'Content-Type': 'application/json',
...(opts.headers || {}),
},
});
const body = await res.json();
if (!res.ok) {
const { code, message, request_id, fields } = body.error;
switch (code) {
case 'invalid_request':
console.error('Validation failed:', fields);
break;
case 'unauthorized':
console.error('Проверьте API-ключ (Bearer sk_...)');
break;
case 'insufficient_funds':
console.error('Пополните баланс клиента');
break;
case 'rate_limited':
case 'service_unavailable':
// повторить с экспоненциальной задержкой (backoff)
break;
}
// request_id - для обращения в поддержку
throw Object.assign(new Error(message), { code, request_id, fields });
}
return body.data;
}
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
$response = Http::withToken($secretKey) // sk_test_ или sk_live_
->acceptJson()
->post("https://fin-os.io/api/v1/customers/{$uuid}/payments/sell", [
'amount_usdt' => '10',
'bank_id' => 'sber',
'phone' => '+79001234567',
]);
if ($response->failed()) {
$err = $response->json('error'); // ['code','message','request_id','fields'?]
match ($err['code']) {
'invalid_request' => report_fields($err['fields'] ?? []),
'insufficient_funds' => notify_top_up($uuid),
'rate_limited',
'service_unavailable' => retry_later(), // backoff + retry
default => Log::warning('B2B error', $err),
};
throw new \RuntimeException(
"{$err['code']}: {$err['message']} (request_id: {$err['request_id']})"
);
}
$data = $response->json('data');
import os, requests
FINOS = 'https://fin-os.io/api/v1'
KEY = os.environ['FINOS_SECRET_KEY'] # sk_test_ или sk_live_
H = {'Authorization': f'Bearer {KEY}', 'Content-Type': 'application/json'}
def api_call(method, path, **kwargs):
resp = requests.request(method, f'{FINOS}{path}', headers=H, **kwargs)
if not resp.ok:
err = resp.json()['error']
code = err['code']
if code == 'invalid_request':
print('Validation failed:', err.get('fields'))
elif code == 'unauthorized':
print('Проверьте API-ключ (Bearer sk_...)')
elif code == 'insufficient_funds':
print('Пополните баланс клиента')
elif code in ('rate_limited', 'service_unavailable'):
pass # повторить с экспоненциальной задержкой (backoff)
# request_id - указать при обращении в поддержку
raise RuntimeError(
f"{code}: {err['message']} (request_id: {err['request_id']})"
)
return resp.json()['data']