Обзор
Twin Bridge — это B2B API-мост, соединяющий международных крипто-провайдеров (Провайдер и других) с лицензированными криптообменниками (VASP и другими).
Как это работает
Интеграция Twin Bridge — это три шага: вы создаёте транзакцию через API, предъявляете QR-код конечному пользователю для оплаты, и Twin Bridge автоматически доставляет криптовалюту после подтверждения платежа.
| Окружение | Base URL | Описание |
|---|---|---|
| Production | https://api.twinbridge.kg |
Боевое окружение |
| Sandbox | https://api.twinbridge.kg + X-Sandbox: true |
Mock-адаптеры, реальные сервисы не вызываются |
Поддерживаемые пары (MVP)
| ON_RAMP | OFF_RAMP | |
|---|---|---|
| Пользователь платит | Фиат (KGS через QR) | Крипто |
| Пользователь получает | Криптовалюту (USDT/USDC) | Фиат (KGS на карту) |
| Платёжная система | ELQR | USDT депозит-адрес (TRC20/ERC20/BEP20) |
| Статус |
Аутентификация
Все B2B-эндпоинты требуют подписи запроса с помощью HMAC-SHA256. Получите API-ключ и секрет от команды интеграции Twin Bridge.
Обязательные заголовки
| Заголовок | Описание |
|---|---|
X-API-Key |
Ваш партнёрский API-ключ |
X-Signature |
HMAC-SHA256 подпись запроса (hex, lowercase) |
X-Timestamp |
Unix timestamp (секунды). Допуск ±60 секунд от серверного времени |
Idempotency-Key |
UUID v4. Обязателен для всех POST-запросов |
Алгоритм подписи
# Шаг 1 — хеш тела запроса body_hash = SHA256(request_body_bytes) // hex, lowercase # Шаг 2 — сообщение для подписи message = timestamp + HTTP_METHOD + URL_PATH + body_hash # Шаг 3 — подпись X-Signature = HMAC-SHA256(api_secret, message) // hex, lowercase
Пример реализации (Python)
import hmac, hashlib, time, json api_key = "your_api_key" api_secret = "your_api_secret" timestamp = str(int(time.time())) method = "POST" path = "/v1/transactions" body = json.dumps({ "direction": "ON_RAMP", "fiat_amount": "5000", "fiat_currency": "KGS", "crypto_currency": "USDT", "flow": "KGS_FLOW", }).encode() body_hash = hashlib.sha256(body).hexdigest() message = timestamp + method + path + body_hash signature = hmac.new( api_secret.encode(), message.encode(), hashlib.sha256 ).hexdigest() headers = { "X-API-Key": api_key, "X-Signature": signature, "X-Timestamp": timestamp, "Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000", "Content-Type": "application/json", }
Пример реализации (Go)
func signRequest(secret, method, path, body string) (string, string) { timestamp := strconv.FormatInt(time.Now().Unix(), 10) bodyHash := fmt.Sprintf("%x", sha256.Sum256([]byte(body))) message := timestamp + method + path + bodyHash mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(message)) sig := fmt.Sprintf("%x", mac.Sum(nil)) return timestamp, sig }
Окружения
Twin Bridge предоставляет одинаковый эндпоинт для Sandbox и Production.
Режим определяется по заголовку X-Sandbox.
| Окружение | Base URL | API ключи |
|---|---|---|
| Production | https://api.twinbridge.kg |
Production ключи |
| Sandbox | https://api.twinbridge.kg |
Sandbox ключи (отдельные) |
| Local Dev | http://localhost:3000 |
Любые (mock) |
Sandbox Mode
Добавьте заголовок X-Sandbox: true для использования mock-адаптеров.
Транзакции проходят через полный state machine, но реальные платежи не выполняются.
POST /v1/transactions Content-Type: application/json X-API-Key: your_sandbox_api_key X-Signature: abc123... X-Timestamp: 1743415200 Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 X-Sandbox: true
INITIATED → PENDING_PAYMENT → PAID → PROCESSING → COMPLETED
в течение нескольких секунд.Жизненный цикл транзакции
Каждая транзакция в Twin Bridge проходит через чётко определённые статусы. Изменение статуса — атомарная операция в базе данных.
ON_RAMP флоу (KGS → USDT)
OFF_RAMP флоу (USDT → KGS)
Ошибочные переходы
Описание статусов
| Статус | Описание |
|---|---|
INITIATED | Транзакция создана, инициализация в процессе |
PENDING_PAYMENT | ON_RAMP: QR-код выдан, ожидается оплата. OFF_RAMP: депозитный адрес выдан, ожидается USDT |
PAID | ON_RAMP: платёж получен от ELQR. OFF_RAMP: USDT зачислен на депозитный адрес |
PROCESSING | Обменник обрабатывает транзакцию |
COMPLETED | ON_RAMP: криптовалюта доставлена на адрес пользователя. OFF_RAMP: KGS отправлен получателю |
CANCELLED | Отменена (по запросу или истёк QR) |
FAILED | Ошибка при обработке |
UNKNOWN, а не FAILED.
В UNKNOWN-статусе Twin Bridge выполняет polling. Никогда не вызывайте повторный Execute() в UNKNOWN — это создаст дублирующую транзакцию.
Smart Routing
Twin Bridge автоматически выбирает лучший обменник для каждой транзакции на основе текущих курсов, ликвидности и доступности.
Если основной обменник недоступен, запрос автоматически маршрутизируется к следующему оптимальному варианту — никакой дополнительной настройки не требуется. Алгоритм учитывает: текущий курс, спред, лимиты, время обработки и надёжность обменника.
Идемпотентность
Все POST-запросы требуют заголовка Idempotency-Key (UUID v4).
Повторный запрос с тем же ключом вернёт исходный ответ — без создания дублирующих транзакций.
POST /v1/transactions Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
- Генерируйте новый UUID для каждой новой транзакции
- Используйте тот же UUID при повторных попытках одной транзакции
- Ключ действителен в течение 24 часов
- При отсутствии ключа API вернёт
400 IDEMPOTENCY_MISSING
Вебхуки
Twin Bridge передаёт webhook-события на ваш зарегистрированный URL при изменении статуса транзакции. Ваш эндпоинт должен вернуть HTTP 200 для подтверждения.
Политика повторных попыток
| Попытка | Задержка |
|---|---|
| 1 | Немедленно |
| 2 | 1 секунда |
| 3 | 5 секунд |
| 4 | 30 секунд |
| 5 | 5 минут |
После 5 неудачных попыток создаётся алерт и событие логируется для ручного разбора.
Создать транзакцию
Создаёт новую ON_RAMP или OFF_RAMP транзакцию. Поля запроса и структура ответа различаются в зависимости от направления.
PROVIDER могут создавать транзакции.ON_RAMP (KGS → USDT)
Пользователь платит KGS через QR-код ELQR и получает USDT на свой крипто-адрес.
Тело запроса (ON_RAMP)
| Поле | Тип | Описание | |
|---|---|---|---|
| direction | string | required | ON_RAMP |
| fiat_amount | string | required | Сумма в фиате (строка с десятичной точкой) |
| fiat_currency | string | required | Код валюты, например KGS |
| crypto_currency | string | required | Крипто-токен, например USDT |
| network | string | required | Сеть: TRC-20, ERC-20 |
| crypto_address | string | required | Адрес кошелька получателя USDT |
| flow | string | required | Всегда KGS_FLOW для MVP |
| payment_method | string | optional | elqr (default) |
| callback_url | string | optional | URL для webhook-уведомлений |
| kyc_share_token | string | optional | KYC passthrough от провайдера (Sumsub) |
| metadata | object | optional | Произвольные данные (order_id, user_id и т.д.) |
{
"direction": "ON_RAMP",
"fiat_amount": "5000.00",
"fiat_currency": "KGS",
"crypto_currency": "USDT",
"network": "TRC-20",
"crypto_address": "TRx7KzjN4GqLhU3k9pF2mW5vB8dY6cE1n",
"payment_method": "elqr",
"flow": "KGS_FLOW",
"callback_url": "https://yourapp.com/webhooks/twinbridge",
"metadata": {
"order_id": "ORD-2026-001",
"user_id": "usr_12345"
}
}
Ответ содержит объект payment с QR-данными для оплаты через ELQR.
{
"tx_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "PENDING_PAYMENT",
"direction": "ON_RAMP",
"fiat_amount": "5000.00",
"fiat_currency": "KGS",
"crypto_currency": "USDT",
"network": "TRC-20",
"crypto_address": "TRx7KzjN4GqLhU3k9pF2mW5vB8dY6cE1n",
"rate": "88.42",
"vasp_id": "dantepay-01",
"estimated_completion": "2026-04-15T12:05:00Z",
"fee": {
"vasp_fee": "25.00",
"vasp_fee_currency": "KGS",
"network_fee": "1.00",
"network_fee_currency": "USDT",
"total_fee_fiat": "113.42",
"total_fee_currency": "KGS"
},
"payment": {
"qr_data": "00020101021226580014kg.elqr.payment...",
"qr_url": "https://elqr.kg/qr/abc123",
"expires_at": "2026-04-15T11:30:00Z",
"amount": "5000.00",
"currency": "KGS"
},
"created_at": "2026-04-15T11:00:00Z"
}
{ "error": "fiat_amount must be greater than 0", "code": "VALIDATION_ERROR" }
{ "error": "Idempotency-Key header is required for POST requests", "code": "IDEMPOTENCY_MISSING" }
{ "error": "crypto_address does not match TRC-20 format", "code": "INVALID_ADDRESS" }
{ "error": "invalid or missing API key", "code": "UNAUTHORIZED" }
{ "error": "invalid signature", "code": "INVALID_SIGNATURE" }
{ "error": "timestamp too old", "code": "TIMESTAMP_EXPIRED" }
{ "error": "duplicate idempotency key with different body", "code": "IDEMPOTENCY_CONFLICT" }
{ "error": "VASP unavailable", "code": "VASP_UNAVAILABLE" }
{ "error": "unsupported currency pair: EUR/USDT", "code": "UNSUPPORTED_PAIR" }
OFF_RAMP (USDT → KGS)
Пользователь отправляет USDT на депозитный адрес и получает KGS на указанный кошелёк или телефон. Сумма в KGS рассчитывается сервером: crypto_amount × rate.
Тело запроса (OFF_RAMP)
| Поле | Тип | Описание | |
|---|---|---|---|
| direction | string | required | OFF_RAMP |
| crypto_amount | string | required | Сумма USDT к отправке (строка с десятичной точкой). fiat_amount НЕ нужен — сервер рассчитает автоматически |
| crypto_currency | string | required | Всегда USDT для MVP |
| network | string | required | Сеть депозита: TRC20, ERC20, BEP20 |
| recipient_kgs_wallet | string | required* | Адрес KGS-кошелька получателя. Обязателен, если не указан recipient_phone |
| recipient_phone | string | required* | Номер телефона получателя (формат E.164). Обязателен, если не указан recipient_kgs_wallet |
| flow | string | required | Всегда KGS_FLOW для MVP |
| callback_url | string | optional | URL для webhook-уведомлений |
| kyc_share_token | string | optional | KYC passthrough от провайдера (Sumsub) |
| metadata | object | optional | Произвольные данные (order_id, user_id и т.д.) |
* Укажите одно из двух: recipient_kgs_wallet или recipient_phone.
{
"direction": "OFF_RAMP",
"crypto_amount": "56.50",
"crypto_currency": "USDT",
"network": "TRC20",
"recipient_kgs_wallet": "KG1001234567890",
"flow": "KGS_FLOW",
"callback_url": "https://yourapp.com/webhooks/twinbridge",
"metadata": {
"order_id": "ORD-2026-002"
}
}
{
"direction": "OFF_RAMP",
"crypto_amount": "56.50",
"crypto_currency": "USDT",
"network": "TRC20",
"recipient_phone": "+996700123456",
"flow": "KGS_FLOW",
"callback_url": "https://yourapp.com/webhooks/twinbridge"
}
Вместо объекта payment ответ содержит deposit_info — адрес, на который пользователь должен отправить USDT. Сумма в KGS (fiat_amount) рассчитана сервером.
{
"tx_id": "660e9500-f30c-52e5-b827-557766551111",
"status": "PENDING_PAYMENT",
"direction": "OFF_RAMP",
"crypto_amount": "56.50",
"crypto_currency": "USDT",
"fiat_amount": "4996.25",
"fiat_currency": "KGS",
"rate": "88.43",
"vasp_id": "dantepay-01",
"estimated_completion": "2026-04-15T12:15:00Z",
"fee": {
"vasp_fee": "0.50",
"vasp_fee_currency": "USDT",
"network_fee": "1.00",
"network_fee_currency": "USDT",
"total_fee_fiat": "132.65",
"total_fee_currency": "KGS"
},
"deposit_info": {
"address": "TBridge9xF2mW5vB8dY6cE1nKzjN4GqLhU3",
"network": "TRC20",
"amount": "56.50",
"currency": "USDT",
"expires_at": "2026-04-15T12:00:00Z"
},
"created_at": "2026-04-15T11:00:00Z"
}
Объект deposit_info
Присутствует в ответе только при direction: OFF_RAMP. Показывает, куда и сколько отправить USDT.
| Поле | Тип | Описание |
|---|---|---|
| address | string | Адрес USDT-депозита. Пользователь должен отправить средства именно на этот адрес |
| network | string | Блокчейн-сеть: TRC20, ERC20 или BEP20 |
| amount | decimal string | Точная сумма USDT для отправки (совпадает с crypto_amount из запроса) |
| currency | string | Всегда USDT |
| expires_at | string (ISO 8601) | Время истечения адреса. По истечении транзакция переходит в CANCELLED |
deposit_info.amount, и только в сети deposit_info.network. Отправка другой суммы или в другой сети приведёт к FAILED.payment.completed с fiat_amount и crypto_tx_hash.Получить транзакцию
Возвращает полные данные одной транзакции. Tenant isolation: вы можете получить только транзакции своего партнёра.
Path параметры
| Параметр | Тип | Описание |
|---|---|---|
| id | string (UUID) | ID транзакции из поля tx_id |
Возвращает полный объект транзакции (аналогичный ответу POST /v1/transactions).
{ "error": "transaction not found", "code": "NOT_FOUND" }
Получить котировку
Возвращает актуальную котировку для пары фиат/крипто. Включает зафиксированный курс, расчётную сумму крипто, комиссию и время истечения. Аутентификация не требуется.
Query параметры
| Параметр | Тип | Описание | |
|---|---|---|---|
| fiat_currency | string | required | Код фиатной валюты, например KGS |
| crypto_currency | string | required | Крипто-токен, например USDT |
| fiat_amount | string | required | Сумма в фиате |
| direction | string | required | ON_RAMP или OFF_RAMP |
{
"fiat_amount": "5000",
"fiat_currency": "KGS",
"crypto_currency": "USDT",
"crypto_amount": "56.4972",
"rate": "88.5",
"direction": "ON_RAMP",
"vasp": "dantepay",
"fee": "0.01",
"rate_expires_at": "2026-04-15T12:00:00Z",
"alternatives": []
}
Отменить транзакцию
Отменяет транзакцию. Только транзакции в статусе PENDING_PAYMENT могут быть отменены.
Идемпотентен: повторный запрос с тем же Idempotency-Key вернёт исходный ответ.
{ "reason": "Customer requested cancellation" }
Поле reason — опциональное.
{
"tx_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "CANCELLED",
"direction": "ON_RAMP",
"fiat_amount": "5000.00",
"fiat_currency": "KGS",
"crypto_currency": "USDT",
"created_at": "2026-04-15T10:00:00Z"
}
{ "error": "cannot cancel transaction in status COMPLETED", "code": "CANCEL_FAILED" }
Список транзакций
Возвращает пагинированный список транзакций вашего партнёра. Поддерживает фильтрацию по статусу и диапазону дат.
Query параметры
| Параметр | Тип | Default | Описание |
|---|---|---|---|
| page | integer | 1 | Номер страницы (от 1) |
| limit | integer | 20 | Элементов на странице (max 100) |
| status | string | — | Фильтр по статусу |
| from | datetime | — | Начало диапазона (RFC3339) |
| to | datetime | — | Конец диапазона (RFC3339) |
{
"data": [
{
"tx_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"direction": "ON_RAMP",
"fiat_amount": "5000.00",
"fiat_currency": "KGS",
"crypto_amount": "56.55",
"crypto_currency": "USDT",
"rate": "88.42",
"created_at": "2026-04-15T10:00:00Z",
"completed_at": "2026-04-15T10:08:00Z"
}
],
"total": 42,
"page": 1,
"limit": 20
}
transaction.completed
Отправляется когда транзакция успешно завершена и криптовалюта доставлена.
{
"event_type": "payment.completed",
"tx_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"timestamp": 1743415800,
"payload": {
"crypto_amount": "56.55",
"crypto_currency": "USDT",
"crypto_tx_hash": "a1b2c3d4e5f67890abcdef...",
"network": "TRC-20",
"rate": "88.42",
"fee": {
"vasp_fee": "25.00",
"network_fee": "1.00"
},
"metadata": {
"order_id": "ORD-2026-001",
"user_id": "usr_12345"
}
}
}
transaction.failed
Отправляется когда транзакция завершилась ошибкой или была отменена.
{
"event_type": "payment.failed",
"tx_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "FAILED",
"timestamp": 1743417000,
"payload": {
"reason": "vasp_timeout",
"error_code": "PROCESSING_FAILED"
}
}
// payment.cancelled
{
"event_type": "payment.cancelled",
"tx_id": "660e9500-f30c-52e5-b827-557766551111",
"status": "CANCELLED",
"timestamp": 1743417500,
"payload": {
"reason": "qr_expired"
}
}
Все события вебхуков
| Событие | Статус | Описание |
|---|---|---|
payment.created | PENDING_PAYMENT | Транзакция создана, QR выдан |
payment.completed | COMPLETED | Крипто доставлено |
payment.cancelled | CANCELLED | Отменена (пользователем или QR истёк) |
payment.failed | FAILED | Ошибка обработки |
payment.qr_scanned |
PAID |
|
payment.qr_expired |
FAILED |
Верификация подписи
Каждый webhook-запрос подписан HMAC-SHA256. Всегда проверяйте подпись перед обработкой.
Заголовки входящего запроса
X-TwinBridge-Signature: abc123def456... X-TwinBridge-Timestamp: 1743415800 Content-Type: application/json
Алгоритм верификации
expected = HMAC-SHA256(webhook_secret, timestamp + "." + raw_body) assert constant_time_compare(expected, received_signature)
Пример верификации (Python)
import hmac, hashlib def verify_webhook(webhook_secret: str, timestamp: str, raw_body: bytes, received_sig: str) -> bool: message = (timestamp + "." + raw_body.decode()).encode() expected = hmac.new( webhook_secret.encode(), message, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, received_sig) # Flask example @app.route("/webhooks/twinbridge", methods=["POST"]) def handle_webhook(): timestamp = request.headers["X-TwinBridge-Timestamp"] signature = request.headers["X-TwinBridge-Signature"] if not verify_webhook(WEBHOOK_SECRET, timestamp, request.data, signature): return "Forbidden", 403 # process event... return "", 200
Коды ошибок
Все ошибки возвращаются в единообразном формате: {"error": "...", "code": "..."}
| HTTP | Код ошибки | Описание |
|---|---|---|
| 400 | VALIDATION_ERROR | Ошибка валидации входных данных |
| 400 | IDEMPOTENCY_MISSING | Отсутствует заголовок Idempotency-Key |
| 400 | INVALID_ADDRESS | Некорректный формат крипто-адреса |
| 400 | INVALID_ID | Некорректный формат ID транзакции |
| 401 | UNAUTHORIZED | Отсутствует или неверный API ключ |
| 401 | INVALID_SIGNATURE | Неверная HMAC-подпись |
| 401 | TIMESTAMP_EXPIRED | Timestamp старше ±60 секунд |
| 403 | FORBIDDEN | Нет прав на данную операцию |
| 404 | NOT_FOUND | Транзакция не найдена |
| 409 | IDEMPOTENCY_CONFLICT | Ключ уже использован с другим телом |
| 409 | MAX_RETRIES_EXCEEDED | Превышено число повторных попыток |
| 422 | EXCHANGER_UNAVAILABLE | Обменник недоступен |
| 422 | UNSUPPORTED_PAIR | Валютная пара не поддерживается |
| 422 | CANCEL_FAILED | Невозможно отменить в текущем статусе |
| 422 | RATE_UNAVAILABLE | Курс недоступен для данной пары |
| 429 | RATE_LIMIT_EXCEEDED | Превышен лимит 100 запросов в минуту |
| 500 | INTERNAL_ERROR | Внутренняя ошибка сервера |
| 503 | SERVICE_UNAVAILABLE | Сервис временно недоступен |
Формат ответа с ошибкой
{
"error": "fiat_amount must be greater than 0",
"code": "VALIDATION_ERROR"
}