Finance OS / API

Коды ошибок

Ошибки 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 и обратитесь в поддержку.

Полный реестр кодов всего 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('превышено число попыток')