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
type
string
optional
created_at
timestamp
optional
data
object
optional
| Name | Type | Required | Description |
|---|---|---|---|
id
|
string (uuid)
|
optional | Уникальный ID события. Используйте для idempotency на вашей стороне. |
type
|
string
|
optional | Тип события (см. список ниже). |
created_at
|
timestamp
|
optional | Когда событие произошло на платформе. |
data
|
object
|
optional | Payload, специфичный для типа события. |
Поддерживаемые события
wallet.deposit.detected
optional
wallet.deposit.confirmed
optional
wallet.withdraw.pending
optional
wallet.withdraw.completed
optional
wallet.withdraw.failed
optional
kyc.approved
optional
kyc.rejected
optional
data.reason.trading.order.filled
optional
trading.order.cancelled
optional
otc.deal.completed
optional
otc.deal.disputed
optional
| Name | Type | Required | Description |
|---|---|---|---|
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 в ЛК.
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