Errors
Finance OS API возвращает стандартные HTTP-коды и единообразное JSON-тело для всех ошибок. Это позволяет писать унифицированный обработчик в клиенте.
Формат тела ошибки
{
"message": "The given data was invalid.",
"errors": {
"email": ["The email field is required."],
"password": ["The password must be at least 8 characters."]
},
"error_code": "VALIDATION_FAILED"
}
Поля
message
string
required
Человекочитаемое описание ошибки. На английском (локализация — v1.1).
errors
object
optional
Только для
422: словарь поле → массив строк. Используйте для подсветки конкретных input в UI.error_code
string
optional
Машиночитаемый код. Используйте для switch-логики и логирования.
request_id
string (uuid)
optional
UUID запроса. Приложите его в support-тикет — мы найдём по логам.
| Name | Type | Required | Description |
|---|---|---|---|
message
|
string
|
required | Человекочитаемое описание ошибки. На английском (локализация — v1.1). |
errors
|
object
|
optional |
Только для 422: словарь поле → массив строк. Используйте для подсветки конкретных input в UI.
|
error_code
|
string
|
optional | Машиночитаемый код. Используйте для switch-логики и логирования. |
request_id
|
string (uuid)
|
optional | UUID запроса. Приложите его в support-тикет — мы найдём по логам. |
HTTP-коды
Семейство 2xx — успех
200 OK
optional
Чтение успешно. Тело — JSON с данными.
201 Created
optional
Ресурс создан. Заголовок
Location указывает на новый ресурс.202 Accepted
optional
Запрос принят в обработку (асинхронно — например, вывод средств).
204 No Content
optional
Успех без тела (после
DELETE).| Name | Type | Required | Description |
|---|---|---|---|
200 OK
|
optional | Чтение успешно. Тело — JSON с данными. | |
201 Created
|
optional |
Ресурс создан. Заголовок Location указывает на новый ресурс.
|
|
202 Accepted
|
optional | Запрос принят в обработку (асинхронно — например, вывод средств). | |
204 No Content
|
optional |
Успех без тела (после DELETE).
|
Семейство 4xx — клиентская ошибка
400 Bad Request
optional
Запрос не парсится (битый JSON, неправильный Content-Type).
401 Unauthorized
optional
API-ключ отсутствует, невалиден или перегенерирован. Возьмите актуальный ключ в кабинете.
403 Forbidden
optional
Ключ валидный, но контур не подключён или ресурс принадлежит другой учётной записи.
404 Not Found
optional
Ресурс не существует или удалён.
409 Conflict
optional
Конфликт состояния (двойной submit, уже подтверждённая операция).
422 Unprocessable Entity
optional
Валидация не прошла.
errors содержит детали по каждому полю.423 Locked
optional
Аккаунт заморожен (KYC-fail, compliance-hold).
429 Too Many Requests
optional
Превышен лимит. См. Rate Limiting.
| Name | Type | Required | Description |
|---|---|---|---|
400 Bad Request
|
optional | Запрос не парсится (битый JSON, неправильный Content-Type). | |
401 Unauthorized
|
optional | API-ключ отсутствует, невалиден или перегенерирован. Возьмите актуальный ключ в кабинете. | |
403 Forbidden
|
optional | Ключ валидный, но контур не подключён или ресурс принадлежит другой учётной записи. | |
404 Not Found
|
optional | Ресурс не существует или удалён. | |
409 Conflict
|
optional | Конфликт состояния (двойной submit, уже подтверждённая операция). | |
422 Unprocessable Entity
|
optional |
Валидация не прошла. errors содержит детали по каждому полю.
|
|
423 Locked
|
optional | Аккаунт заморожен (KYC-fail, compliance-hold). | |
429 Too Many Requests
|
optional | Превышен лимит. См. Rate Limiting. |
Семейство 5xx — серверная ошибка
500 Internal Server Error
optional
Неожиданная ошибка на сервере. Логи у нас, в теле —
request_id. Повторите через минуту.502 Bad Gateway
optional
Сбой апстрима (платёжный шлюз или verification-провайдер недоступен). Повторите через минуту.
503 Service Unavailable
optional
Плановое обслуживание. Заголовок
Retry-After укажет ожидаемое окно.504 Gateway Timeout
optional
Долгий ответ от апстрима. Часто — операции с блокчейном. Повторите с idempotency-key.
| Name | Type | Required | Description |
|---|---|---|---|
500 Internal Server Error
|
optional |
Неожиданная ошибка на сервере. Логи у нас, в теле — request_id. Повторите через минуту.
|
|
502 Bad Gateway
|
optional | Сбой апстрима (платёжный шлюз или verification-провайдер недоступен). Повторите через минуту. | |
503 Service Unavailable
|
optional |
Плановое обслуживание. Заголовок Retry-After укажет ожидаемое окно.
|
|
504 Gateway Timeout
|
optional | Долгий ответ от апстрима. Часто — операции с блокчейном. Повторите с idempotency-key. |
Машиночитаемые коды
Часто встречающиеся error_code значения:
VALIDATION_FAILED
422
optional
Ошибки в полях — детали в
errors.TOKEN_INVALID
401
optional
API-ключ невалиден или перегенерирован.
INSUFFICIENT_BALANCE
422
optional
Недостаточно средств для операции (вывод, перевод).
KYC_REQUIRED
403
optional
Операция требует завершённой верификации клиента. Проведите KYC и повторите.
SANCTION_HIT
423
optional
Адрес/контрагент попал под санкции — операция заблокирована.
ACCOUNT_FROZEN
423
optional
Аккаунт заморожен (compliance).
NETWORK_CONGESTED
503
optional
Сеть блокчейна перегружена — попробуйте позже или измените сеть.
IDEMPOTENCY_CONFLICT
409
optional
Idempotency-key уже использован с другим телом запроса.
| Name | Type | Required | Description |
|---|---|---|---|
VALIDATION_FAILED
|
422
|
optional |
Ошибки в полях — детали в errors.
|
TOKEN_INVALID
|
401
|
optional | API-ключ невалиден или перегенерирован. |
INSUFFICIENT_BALANCE
|
422
|
optional | Недостаточно средств для операции (вывод, перевод). |
KYC_REQUIRED
|
403
|
optional | Операция требует завершённой верификации клиента. Проведите KYC и повторите. |
SANCTION_HIT
|
423
|
optional | Адрес/контрагент попал под санкции — операция заблокирована. |
ACCOUNT_FROZEN
|
423
|
optional | Аккаунт заморожен (compliance). |
NETWORK_CONGESTED
|
503
|
optional | Сеть блокчейна перегружена — попробуйте позже или измените сеть. |
IDEMPOTENCY_CONFLICT
|
409
|
optional | Idempotency-key уже использован с другим телом запроса. |
Идемпотентность
Для всех POST на создание ресурса (особенно денежных операций) клиент может передать заголовок Idempotency-Key — UUID, который сервер запомнит на 24 часа и при повторном запросе с тем же ключом вернёт оригинальный ответ, не выполняя операцию повторно.
curl -X POST https://fin-os.io/api/v1/customers/{uuid}/wallet/withdraw \
-H "Authorization: Bearer ${TOKEN}" \
-H "Idempotency-Key: 5f8d-7a3c-…" \
-d '{"amount": "10.00", "currency": "USDT", "network": "TRON", "address": "T..."}'
⚠
Используйте Idempotency-Key для money-операций
Сеть моргнула, ваш запрос не дошёл — вы повторяете. Без idempotency-key возможен двойной вывод. С ним — гарантия «exactly-once».
Примеры кода
# Пример валидационной ошибки (422)
curl -X POST https://fin-os.io/api/v1/customers \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"phone": "not-a-phone"}'
# HTTP/2 422
# {
# "message": "The given data was invalid.",
# "errors": {
# "phone": ["The phone must be a valid E.164 number."]
# },
# "error_code": "VALIDATION_FAILED",
# "request_id": "5f8d7a3c-1234-4567-89ab-cdef01234567"
# }
// Унифицированный обработчик
async function apiCall(url, init = {}) {
const res = await fetch(url, {
...init,
headers: { 'Accept': 'application/json', ...init.headers },
});
if (res.ok) return res.json();
const err = await res.json().catch(() => ({ message: 'Network error' }));
switch (err.error_code) {
case 'TOKEN_INVALID':
throw new Error('API-ключ невалиден - обновите секрет в конфигурации сервиса');
case 'INSUFFICIENT_BALANCE':
throw new Error('Недостаточно средств');
default:
throw new Error(err.message || `HTTP ${res.status}`);
}
}
try {
$response = Http::withToken($token)
->throw()
->post('https://fin-os.io/api/v1/customers/' . $uuid . '/wallet/withdraw', $data);
} catch (\Illuminate\Http\Client\RequestException $e) {
$body = $e->response->json();
$code = $body['error_code'] ?? null;
match ($code) {
'INSUFFICIENT_BALANCE' => throw new InsufficientBalanceException($body['message']),
'KYC_REQUIRED' => throw new KycRequiredException($body['message']),
'SANCTION_HIT' => logger()->alert('Sanction hit', ['request_id' => $body['request_id'] ?? null]),
default => throw $e,
};
}
import requests
class FinanceOsError(Exception):
def __init__(self, code, message, request_id=None):
self.code = code
self.message = message
self.request_id = request_id
super().__init__(f'{code}: {message}')
def api_call(method, url, **kwargs):
r = requests.request(method, url, **kwargs)
if r.ok:
return r.json()
body = r.json() if 'application/json' in r.headers.get('Content-Type', '') else {}
raise FinanceOsError(
code=body.get('error_code', f'HTTP_{r.status_code}'),
message=body.get('message', 'Unknown error'),
request_id=body.get('request_id'),
)
try:
balance = api_call('GET', 'https://fin-os.io/api/v1/merchant/balance',
headers={'Authorization': f'Bearer {TOKEN}'})
except FinanceOsError as e:
if e.code == 'TOKEN_INVALID':
print('Ключ невалиден - обновите секрет')
else:
print(f'API error: {e.code} ({e.request_id})')