Finance OS / API

Payments

Buy (RUB → USDT через СБП), Sell (USDT → RUB через СБП), Crypto Deposit и Withdraw на managed customers. Все операции требуют kyc_status='verified'.

KYC gate
Любая операция без верифицированного customer'а вернёт 422 KYC_REQUIRED. Сначала пройдите KYC.

Котировки

Курсы обновляются у Finance OS раз в минуту и фиксируются на 60 секунд после первой котировки в сессии.

Buy quote

POST /api/v1/customers/{uuid}/payments/buy/calculate
Bearer Token

Сколько USDT за N рублей

Body

amount_rub numeric required
100..100000 RUB.

Sell quote

POST /api/v1/customers/{uuid}/payments/sell/calculate
Bearer Token

Сколько RUB за N USDT

Body

amount_usdt numeric required
1..1000 USDT.

Текущие курсы (без customer)

GET /api/v1/payments/rates
Bearer Token

Buy/sell rate без привязки к customer'у

Buy (RUB → USDT)

POST /api/v1/customers/{uuid}/payments/buy
Bearer Token

Создать СБП-платёж

Возвращает transaction с QR-кодом для СБП. End-user сканирует QR в своём банковском приложении, переводит RUB, USDT зачисляется на баланс customer'а.

Body

amount_rub numeric required
100..100000.
bank_id string required
Идентификатор банка-получателя.

Responses

{
  "data": {
    "uuid": "1a7ee75b-7f0e-48af-a075-a56721f1140e",
    "type": "payment",
    "status": "pending",
    "status_code": 0,
    "amount_from": "1000",
    "amount_to": "12.9602550000",
    "currency_from": "RUB",
    "currency_to": "USDT",
    "rate": "0.012960254775",
    "qr_url": "https://fin-os.io/api/v1/sandbox/qr/abc123.png",
    "expires_at": "2026-05-27T14:38:00+00:00",
    "env": "sandbox"
  }
}

Buy status

GET /api/v1/customers/{uuid}/payments/buy/{txUuid}/status
Bearer Token

Проверить статус buy-транзакции

В sandbox через ~5 секунд автоматически переходит в completed и USDT кредитится на баланс customer'а. В live статус обновляется через провайдерский webhook.

Sell (USDT → RUB)

POST /api/v1/customers/{uuid}/payments/sell
Bearer Token

Создать СБП-выплату на банковский счёт

Сумма USDT блокируется (locked) в момент создания. При success — consume'ится; при failure — возвращается в available.

Body

amount_usdt numeric required
1..1000.
bank_id string required
Банк-получатель.
phone string required
Телефон счёта в СБП (E.164).

Sell status

GET /api/v1/customers/{uuid}/payments/sell/{txUuid}/status
Bearer Token

Проверить статус sell-транзакции

Crypto deposit

POST /api/v1/customers/{uuid}/payments/deposit/address
Bearer Token

Получить адрес для крипто-депозита

Возвращает адрес для пополнения customer'а в указанной сети. End-user отправляет криптовалюту по этому адресу — балансы customer'а обновятся при подтверждении транзакции в блокчейне.

Body

currency enum optional
USDT, BTC, ETH, LTC, KAS.
Default: USDT

Responses

{
  "data": {
    "currency": "USDT",
    "network": "TRC20",
    "address": "TXYZ1234567890abcdef...",
    "min_deposit": "1.000000",
    "env": "live",
    "warning": "Send only USDT in TRC20 network to this address."
  }
}

Crypto withdraw

POST /api/v1/customers/{uuid}/payments/withdraw
Bearer Token

Crypto-вывод на внешний адрес

Сумма блокируется (locked). После транзакции в сети — consume'ится.

Body

currency enum required
USDT/BTC/ETH/LTC/KAS.
amount numeric required
Минимум 0.000001.
address string required
Внешний адрес.

Коды ошибок

KYC.'], ['name' => 'INSUFFICIENT_BALANCE', 'type' => '422', 'desc' => 'Недостаточно средств для lock (sell/withdraw).'], ['name' => 'AMOUNT_OUT_OF_RANGE', 'type' => '422', 'desc' => 'Сумма вне разрешённого диапазона.'], ['name' => 'INVALID_BANK', 'type' => '422', 'desc' => 'bank_id не в списке поддерживаемых СБП-банков.'], ['name' => 'INVALID_ADDRESS', 'type' => '422', 'desc' => 'Некорректный crypto-адрес для указанной сети.'], ['name' => 'PROVIDER_DOWN', 'type' => '503', 'desc' => 'Платёжный шлюз временно недоступен. Retry через минуту.'], ]" />

Примеры кода

# Buy: RUB → USDT через СБП
curl -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": 1000, "bank_id": "sber"}'

# Sell: USDT → RUB через СБП
curl -X POST https://fin-os.io/api/v1/customers/$UUID/payments/sell \
  -H "Authorization: Bearer sk_test_..." \
  -d '{"amount_usdt": 10, "bank_id": "sber", "phone": "+79001234567"}'

# Crypto deposit address
curl -X POST https://fin-os.io/api/v1/customers/$UUID/payments/deposit/address \
  -H "Authorization: Bearer sk_test_..." \
  -d '{"currency": "USDT"}'

# Crypto withdraw
curl -X POST https://fin-os.io/api/v1/customers/$UUID/payments/withdraw \
  -H "Authorization: Bearer sk_test_..." \
  -d '{"currency": "USDT", "amount": 5, "address": "TXYZabc..."}'
async function buyUsdt(uuid, amountRub, bankId) {
  const res = await fetch(
    `https://fin-os.io/api/v1/customers/${uuid}/payments/buy`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.FINOS_SECRET_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ amount_rub: amountRub, bank_id: bankId }),
    }
  );
  const { data } = await res.json();
  console.log('Show QR to user:', data.qr_url);
  return data; // poll status via /payments/buy/{data.uuid}/status
}

async function pollUntilCompleted(uuid, txUuid, timeoutSec = 600) {
  const deadline = Date.now() + timeoutSec * 1000;
  while (Date.now() < deadline) {
    const r = await fetch(
      `https://fin-os.io/api/v1/customers/${uuid}/payments/buy/${txUuid}/status`,
      { headers: { 'Authorization': `Bearer ${process.env.FINOS_SECRET_KEY}` } }
    );
    const { data } = await r.json();
    if (['completed', 'failed', 'cancelled'].includes(data.status)) return data;
    await new Promise(r => setTimeout(r, 3000));
  }
  throw new Error('timeout');
}
use Illuminate\Support\Facades\Http;

class FinosPayments
{
    private string $base = 'https://fin-os.io/api/v1';

    public function __construct(private string $secretKey) {}

    public function buy(string $customerUuid, string $amountRub, string $bankId): array
    {
        return Http::withToken($this->secretKey)
            ->post("{$this->base}/customers/{$customerUuid}/payments/buy", [
                'amount_rub' => $amountRub,
                'bank_id'    => $bankId,
            ])
            ->json('data');
    }

    public function sell(string $uuid, string $amountUsdt, string $bankId, string $phone): array
    {
        return Http::withToken($this->secretKey)
            ->post("{$this->base}/customers/{$uuid}/payments/sell", compact('amountUsdt', 'bankId', 'phone'))
            ->json('data');
    }

    public function withdraw(string $uuid, string $currency, string $amount, string $address): array
    {
        return Http::withToken($this->secretKey)
            ->post("{$this->base}/customers/{$uuid}/payments/withdraw", compact('currency', 'amount', 'address'))
            ->json('data');
    }
}
import requests, time, os

FINOS = 'https://fin-os.io/api/v1'
KEY   = os.environ['FINOS_SECRET_KEY']
H     = {'Authorization': f'Bearer {KEY}'}

def buy(uuid, amount_rub, bank_id):
    r = requests.post(
        f'{FINOS}/customers/{uuid}/payments/buy',
        headers=H,
        json={'amount_rub': amount_rub, 'bank_id': bank_id},
    )
    r.raise_for_status()
    return r.json()['data']

def poll_buy(uuid, tx_uuid, timeout=600):
    deadline = time.time() + timeout
    while time.time() < deadline:
        r = requests.get(
            f'{FINOS}/customers/{uuid}/payments/buy/{tx_uuid}/status', headers=H
        )
        data = r.json()['data']
        if data['status'] in ('completed', 'failed', 'cancelled'):
            return data
        time.sleep(3)
    raise TimeoutError('Buy did not complete in time')

# usage
tx = buy(customer_uuid, 1000, 'sber')
print('Show QR:', tx['qr_url'])
result = poll_buy(customer_uuid, tx['uuid'])
print('Final:', result['status'])