Finance OS / API

Webhooks

Finance OS отправляет webhooks для событий, которые требуют асинхронной обработки на стороне клиента: входящие крипто-депозиты, изменения статуса вывода, KYC-вердикты, исполнение торговых ордеров. Также Finance OS обрабатывает входящие события от своих платёжных и compliance-провайдеров — это внутренняя интеграция, на которую вы как клиент API не подписываетесь.

Регистрация webhook URL

В Личном Кабинете: fin-os.io/cabinet/api-tokens → раздел «Webhooks» → добавить URL. Endpoint должен:

  • Принимать POST с JSON-телом.
  • Возвращать 2xx в течение 10 секунд.
  • Быть на HTTPS (HTTP отклоняется).

Формат payload

{
  "id": "evt_5f8d7a3c-1234-4567-89ab-cdef01234567",
  "type": "wallet.deposit.confirmed",
  "created_at": "2026-05-27T14:23:00Z",
  "data": {
    "transaction_id": "tx_8e23f...",
    "wallet_id": "wlt_9c12d...",
    "amount": "100.000000",
    "currency": "USDT",
    "network": "TRC20",
    "tx_hash": "0xabc..."
  }
}

Структура event

id string (uuid) optional
Уникальный ID события. Используйте для idempotency на вашей стороне.
type string optional
Тип события (см. список ниже).
created_at timestamp optional
Когда событие произошло на платформе.
data object optional
Payload, специфичный для типа события.

Поддерживаемые события

wallet.deposit.detected optional
Крипто-депозит обнаружен в mempool (0 подтверждений).
wallet.deposit.confirmed optional
Депозит подтверждён (TRC20: 1 conf, BTC: 2 conf).
wallet.withdraw.pending optional
Вывод отправлен в сеть, ждём подтверждения.
wallet.withdraw.completed optional
Вывод подтверждён в сети.
wallet.withdraw.failed optional
Вывод отвергнут (compliance/network).
kyc.approved optional
KYC-верификация пройдена.
kyc.rejected optional
KYC отклонён. Причина в data.reason.
trading.order.filled optional
Торговый ордер исполнен.
trading.order.cancelled optional
Торговый ордер отменён.
otc.deal.completed optional
OTC-сделка завершена (обе стороны подписали).
otc.deal.disputed optional
OTC-сделка передана в арбитраж.

Проверка подписи

Каждый webhook содержит заголовок X-FinOs-Signature — HMAC-SHA256 от raw body, с ключом, который вы получаете при регистрации webhook URL в ЛК.

Всегда проверяйте подпись
Без проверки подписи злоумышленник может отправить вам поддельный webhook (например, «деньги пришли») и спровоцировать выдачу товара. Сравнение должно быть constant-time (hash_equals в PHP, crypto.timingSafeEqual в Node).

Retry политика

Если ваш endpoint вернул не-2xx или превысил 10s таймаут, Finance OS повторит доставку:

  • 1-я повторная попытка — через 30 секунд
  • 2-я — через 5 минут
  • 3-я — через 30 минут
  • 4-я — через 6 часов
  • 5-я — через 24 часа

После 5 неуспешных попыток событие помечается как failed и письмо уходит на email владельца аккаунта. Историю всех попыток можно посмотреть в ЛК в разделе «Webhooks → Logs».

Идемпотентная обработка

Каждое событие имеет уникальный id. Сохраняйте обработанные ID в БД и игнорируйте повторы — это защитит от double-processing при retry.

Внутренние входящие webhooks

Finance OS также принимает события от своих back-end интеграций (платёжный шлюз, провайдер верификации, торговый bridge). Эти эндпоинты внутренние — внешние интеграторы их не используют.

Эти эндпоинты обслуживают входящий трафик от платёжного шлюза (события по СБП-операциям и крипто-операциям), от верификационного провайдера (вердикты KYC) и от движка алгоритмического трейдинга. Они закрыты HMAC-подписью провайдера и недоступны для внешних вызовов.

Примеры кода

# Тестовая отправка webhook на ваш endpoint (для отладки)
curl -X POST https://your-app.example.com/webhooks/finos \
  -H "Content-Type: application/json" \
  -H "X-FinOs-Signature: sha256=abc123..." \
  -d '{
    "id": "evt_5f8d7a3c-1234-4567-89ab-cdef01234567",
    "type": "wallet.deposit.confirmed",
    "created_at": "2026-05-27T14:23:00Z",
    "data": {
      "transaction_id": "tx_8e23f...",
      "amount": "100.000000",
      "currency": "USDT",
      "network": "TRC20"
    }
  }'
// Node.js / Express — обработчик webhook
import express from 'express';
import crypto from 'crypto';

const app = express();
const WEBHOOK_SECRET = process.env.FINOS_WEBHOOK_SECRET;
const processedEvents = new Set(); // в реальности — Redis/БД

app.post('/webhooks/finos', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.header('X-FinOs-Signature') || '';
    const expected = 'sha256=' + crypto
        .createHmac('sha256', WEBHOOK_SECRET)
        .update(req.body)
        .digest('hex');

    // Constant-time compare
    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
        return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body.toString());

    // Idempotency check
    if (processedEvents.has(event.id)) return res.status(200).send('Already processed');
    processedEvents.add(event.id);

    switch (event.type) {
        case 'wallet.deposit.confirmed':
            console.log('Deposit:', event.data.amount, event.data.currency);
            break;
        case 'kyc.approved':
            // unlock user features
            break;
    }

    res.status(200).send('OK');
});
// Laravel webhook controller
use Illuminate\Http\Request;

class FinOsWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $signature = $request->header('X-FinOs-Signature', '');
        $expected = 'sha256=' . hash_hmac(
            'sha256',
            $request->getContent(),
            config('services.finos.webhook_secret')
        );

        if (! hash_equals($expected, $signature)) {
            abort(401, 'Invalid signature');
        }

        $event = $request->json()->all();

        // Idempotency
        if (WebhookEvent::where('event_id', $event['id'])->exists()) {
            return response()->noContent();
        }

        WebhookEvent::create(['event_id' => $event['id'], 'type' => $event['type']]);

        match ($event['type']) {
            'wallet.deposit.confirmed' => app(DepositHandler::class)->handle($event['data']),
            'kyc.approved'             => $this->unlockUser($event['data']),
            default                    => logger()->info('Unhandled webhook', $event),
        };

        return response()->noContent();
    }
}
# Flask / Django webhook handler
import hmac, hashlib
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['FINOS_WEBHOOK_SECRET']
processed = set()  # use Redis in production

@app.route('/webhooks/finos', methods=['POST'])
def webhook():
    signature = request.headers.get('X-FinOs-Signature', '')
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(),
        request.data,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)

    event = request.json
    if event['id'] in processed:
        return '', 204
    processed.add(event['id'])

    if event['type'] == 'wallet.deposit.confirmed':
        process_deposit(event['data'])
    elif event['type'] == 'kyc.approved':
        unlock_user(event['data'])

    return '', 204