Коды ошибок
Ошибки AML API приходят в том же конверте, что и остальные вызовы /api/v1/*: машинный code в нижнем регистре, человекочитаемый message и идентификатор запроса request_id для обращения в поддержку. Ветвите логику по code, а не по тексту сообщения.
Responses
{
"error": {
"code": "invalid_request",
"message": "Некорректные данные запроса.",
"request_id": "req_a1B2c3D4e5F6g7H8i9J0kLmN",
"fields": {
"address": ["The address field is required."]
}
}
}
⚠
Две формы ошибок - учтите при разборе
Ошибки бизнес-логики (валидация, «не найдено», лимиты, нехватка средств) приходят во вложенном конверте error.code. Но гейт доступа - аутентификация (401) и B2B-доступ (403) - отвечает в плоской форме с полем error_code на верхнем уровне. Разбирайте обе: проверяйте и верхнеуровневый 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"
}
Справочник кодов
unauthorized
401
optional
Отсутствует или недействителен ключ
Bearer sk_(live|test)_….forbidden
403
optional
Ключ рабочий, но сервис AML отключён для вашей учётной записи. Подключение - admin@fin-os.io.
merchant_suspended
403
optional
Учётная запись приостановлена. Повтор не поможет - обратитесь в поддержку.
not_found
404
optional
Проверки с таким
id нет в вашем контуре и текущем окружении. Тем же кодом отвечает попытка прочитать проверку из другого окружения.invalid_request
422
optional
address не задан или длиннее 120 символов, network длиннее 20 символов, либо сеть не удалось определить по формату адреса. В последнем случае объект fields содержит ключ network с подсказкой указать сеть явно.service_balance_insufficient
402
optional
На мастер-счёте не хватает средств для оплаты проверки. Ответ дополнен полями
currency, required и available. Пополните счёт - см. Merchant Balance.rate_limited
429
optional
Слишком много запросов. Реализуйте экспоненциальный backoff и повтор с тем же
Idempotency-Key.service_unavailable
503
optional
Оценка временно недоступна. Деньги за такую проверку не списываются: если списание уже произошло, оно автоматически сторнируется.
internal_error
500
optional
Непредвиденная ошибка. Сохраните
request_id и обратитесь в поддержку.| Name | Type | Required | Description |
|---|---|---|---|
unauthorized
|
401
|
optional |
Отсутствует или недействителен ключ Bearer sk_(live|test)_….
|
forbidden
|
403
|
optional | Ключ рабочий, но сервис AML отключён для вашей учётной записи. Подключение - admin@fin-os.io. |
merchant_suspended
|
403
|
optional | Учётная запись приостановлена. Повтор не поможет - обратитесь в поддержку. |
not_found
|
404
|
optional |
Проверки с таким id нет в вашем контуре и текущем окружении. Тем же кодом отвечает попытка прочитать проверку из другого окружения.
|
invalid_request
|
422
|
optional |
address не задан или длиннее 120 символов, network длиннее 20 символов, либо сеть не удалось определить по формату адреса. В последнем случае объект fields содержит ключ network с подсказкой указать сеть явно.
|
service_balance_insufficient
|
402
|
optional |
На мастер-счёте не хватает средств для оплаты проверки. Ответ дополнен полями currency, required и available. Пополните счёт - см. Merchant Balance.
|
rate_limited
|
429
|
optional |
Слишком много запросов. Реализуйте экспоненциальный backoff и повтор с тем же Idempotency-Key.
|
service_unavailable
|
503
|
optional | Оценка временно недоступна. Деньги за такую проверку не списываются: если списание уже произошло, оно автоматически сторнируется. |
internal_error
|
500
|
optional |
Непредвиденная ошибка. Сохраните request_id и обратитесь в поддержку.
|
Полный реестр кодов всего Merchant API - Error Codes.
Ошибки и деньги
✦
Вы платите за результат
Если проверка не дошла до вердикта (сбой оценки, ошибка записи), списанная сумма возвращается автоматически строкой возврата в леджере мастер-счёта. Отдельных действий от вас не требуется. Ошибки валидации и доступа денег не списывают вовсе.
Responses
{
"error": {
"code": "service_balance_insufficient",
"message": "Недостаточно средств на сервисном счёте. Пополните мастер-счёт.",
"request_id": "req_3Nd8Vc1Xz5Bm7Kq0Lp2Rt6W",
"currency": "USDT",
"required": "0.2",
"available": "0.0500000000"
}
}
⊘
Повтор после таймаута - только с тем же Idempotency-Key
Сетевой таймаут не означает, что проверка не выполнена и деньги не списаны. Повторяйте запрос с тем же заголовком Idempotency-Key: вернётся ранее выполненная проверка без второго списания. Новый ключ на повторе - это новая платная проверка.
Частичные данные не равны «чисто»
Если часть источников временно недоступна, проверка всё равно возвращается - с тем скорингом, что удалось собрать. Поэтому низкий risk_score может означать и «чисто», и «часть источников не ответила». Для критичных операций трактуйте отсутствие сигналов консервативно и при необходимости перепроверяйте адрес позже.
Примеры кода
# Ошибка валидации: сеть не определилась по формату адреса
curl -i -X POST https://fin-os.io/api/v1/aml/checks \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}'
# HTTP/1.1 422 Unprocessable Entity
# { "error": { "code": "invalid_request", "request_id": "req_...",
# "fields": { "network": ["Не удалось определить сеть адреса - укажите параметр network."] } } }
# Пустой мастер-счёт
curl -i -X POST https://fin-os.io/api/v1/aml/checks \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: check-order-1042" \
-d '{"address": "TXYZ1234567890abcdefGHIJKLmnop", "network": "tron"}'
# HTTP/1.1 402 Payment Required
# { "error": { "code": "service_balance_insufficient", "currency": "USDT",
# "required": "0.2", "available": "0.05", "request_id": "req_..." } }
# Гейт доступа до активации сервиса - ПЛОСКАЯ форма
# { "error_code": "B2B_ACCESS_REQUIRED", "message": "...", "docs": "..." }
use Illuminate\Support\Facades\Http;
$resp = Http::withToken(config('services.finos.secret_key'))
->withHeaders(['Idempotency-Key' => "check-{$orderId}"]) // тот же ключ на всех ретраях
->acceptJson()
->post('https://fin-os.io/api/v1/aml/checks', ['address' => $address, 'network' => 'tron']);
if ($resp->failed()) {
// Гейт доступа отвечает плоской формой, всё остальное - вложенной
$flat = $resp->json('error_code');
$err = $resp->json('error') ?? ['code' => $flat, 'message' => $resp->json('message')];
match ($err['code']) {
'B2B_ACCESS_REQUIRED' => report('AML не активирован для учётной записи'),
'service_balance_insufficient'=> topUpServiceBalance($err['required'] ?? null),
'invalid_request' => report('проверьте address и network: ' . json_encode($resp->json('error.fields'))),
'rate_limited',
'service_unavailable' => retryLater(), // backoff, тот же Idempotency-Key
default => logger()->warning('AML error', (array) $err),
};
}
$data = $resp->json('data');
import time, requests, os
FINOS = 'https://fin-os.io/api/v1'
KEY = os.environ['FINOS_SECRET_KEY']
def check_with_retry(address, network=None, idem=None, attempts=4):
payload = {'address': address}
if network:
payload['network'] = network
headers = {'Authorization': f'Bearer {KEY}'}
if idem:
headers['Idempotency-Key'] = idem # один и тот же ключ на всех попытках
for i in range(attempts):
r = requests.post(f'{FINOS}/aml/checks', headers=headers, json=payload, timeout=60)
if r.status_code in (429, 503): # rate_limited / service_unavailable
time.sleep(2 ** i)
continue
if r.status_code == 402: # service_balance_insufficient
err = r.json()['error']
raise RuntimeError(f"пополните мастер-счёт: нужно {err['required']} {err['currency']}")
if r.status_code == 403:
raise RuntimeError('AML недоступен - проверьте активацию сервиса')
r.raise_for_status()
return r.json()['data']
raise RuntimeError('превышено число попыток')