Twin Bridge
API Docs
99.9% uptime
100 req/min
< 2s response
Связаться
Начало работы

Обзор

Twin Bridge — это B2B API-мост, соединяющий международных крипто-провайдеров (Провайдер и других) с лицензированными криптообменниками (VASP и другими).

ℹ️
Юридический статус: Twin Bridge — IT-решение, зарегистрированное в США. Мы не являемся VASP, обменником или финансовой компанией. Twin Bridge никогда не держит и не перемещает средства — мы передаём данные как почтальон.

Как это работает

Интеграция Twin Bridge — это три шага: вы создаёте транзакцию через API, предъявляете QR-код конечному пользователю для оплаты, и Twin Bridge автоматически доставляет криптовалюту после подтверждения платежа.

ОкружениеBase URLОписание
Production https://api.twinbridge.kg Боевое окружение
Sandbox https://api.twinbridge.kg + X-Sandbox: true Mock-адаптеры, реальные сервисы не вызываются

Поддерживаемые пары (MVP)

ON_RAMPOFF_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-запросов

Алгоритм подписи

Pseudocode
# Шаг 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)

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)

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 URLAPI ключи
Production https://api.twinbridge.kg Production ключи
Sandbox https://api.twinbridge.kg Sandbox ключи (отдельные)
Local Dev http://localhost:3000 Любые (mock)
⚠️
Sandbox и Production используют разные API-ключи. Убедитесь, что используете правильный ключ для каждого окружения.

Начало работы

Sandbox Mode

Добавьте заголовок X-Sandbox: true для использования mock-адаптеров. Транзакции проходят через полный state machine, но реальные платежи не выполняются.

HTTP
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
В Sandbox-режиме транзакция автоматически переходит через все статусы: INITIATED → PENDING_PAYMENT → PAID → PROCESSING → COMPLETED в течение нескольких секунд.

Основные концепции

Жизненный цикл транзакции

Каждая транзакция в Twin Bridge проходит через чётко определённые статусы. Изменение статуса — атомарная операция в базе данных.

ON_RAMP флоу (KGS → USDT)

INITIATED
создана
PENDING_PAYMENT
ожидает оплату
PAID
оплачено
PROCESSING
обрабатывается
COMPLETED
завершено

OFF_RAMP флоу (USDT → KGS)

INITIATED
создана
PENDING_PAYMENT
ожидает USDT
PAID
USDT получено
PROCESSING
KGS отправляется
COMPLETED
KGS доставлен

Ошибочные переходы

PENDING_PAYMENT
CANCELLED
PENDING_PAYMENT
FAILED
PROCESSING
FAILED

Описание статусов

СтатусОписание
INITIATEDТранзакция создана, инициализация в процессе
PENDING_PAYMENTON_RAMP: QR-код выдан, ожидается оплата. OFF_RAMP: депозитный адрес выдан, ожидается USDT
PAIDON_RAMP: платёж получен от ELQR. OFF_RAMP: USDT зачислен на депозитный адрес
PROCESSINGОбменник обрабатывает транзакцию
COMPLETEDON_RAMP: криптовалюта доставлена на адрес пользователя. OFF_RAMP: KGS отправлен получателю
CANCELLEDОтменена (по запросу или истёк QR)
FAILEDОшибка при обработке
⚠️
Правило UNKNOWN: Если обменник не ответил в течение таймаута, транзакция переходит в статус UNKNOWN, а не FAILED. В UNKNOWN-статусе Twin Bridge выполняет polling. Никогда не вызывайте повторный Execute() в UNKNOWN — это создаст дублирующую транзакцию.

Основные концепции

Smart Routing

Twin Bridge автоматически выбирает лучший обменник для каждой транзакции на основе текущих курсов, ликвидности и доступности.

Если основной обменник недоступен, запрос автоматически маршрутизируется к следующему оптимальному варианту — никакой дополнительной настройки не требуется. Алгоритм учитывает: текущий курс, спред, лимиты, время обработки и надёжность обменника.

ℹ️
В MVP-версии доступен один обменник — VASP. Многообменное Smart Routing будет доступно в Phase 2.

Основные концепции

Идемпотентность

Все POST-запросы требуют заголовка Idempotency-Key (UUID v4). Повторный запрос с тем же ключом вернёт исходный ответ — без создания дублирующих транзакций.

HTTP
POST /v1/transactions
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
  • Генерируйте новый UUID для каждой новой транзакции
  • Используйте тот же UUID при повторных попытках одной транзакции
  • Ключ действителен в течение 24 часов
  • При отсутствии ключа API вернёт 400 IDEMPOTENCY_MISSING

Основные концепции

Вебхуки

Twin Bridge передаёт webhook-события на ваш зарегистрированный URL при изменении статуса транзакции. Ваш эндпоинт должен вернуть HTTP 200 для подтверждения.

Политика повторных попыток

ПопыткаЗадержка
1Немедленно
21 секунда
35 секунд
430 секунд
55 минут

После 5 неудачных попыток создаётся алерт и событие логируется для ручного разбора.


API Reference

Создать транзакцию

POST /v1/transactions ✅ Реализовано

Создаёт новую ON_RAMP или OFF_RAMP транзакцию. Поля запроса и структура ответа различаются в зависимости от направления.

Оба направления реализованы: ON_RAMP (KGS → USDT) и OFF_RAMP (USDT → KGS). Только партнёры типа PROVIDER могут создавать транзакции.

ON_RAMP (KGS → USDT)

Пользователь платит KGS через QR-код ELQR и получает USDT на свой крипто-адрес.

Тело запроса (ON_RAMP)

ПолеТипОписание
directionstringrequiredON_RAMP
fiat_amountstringrequiredСумма в фиате (строка с десятичной точкой)
fiat_currencystringrequiredКод валюты, например KGS
crypto_currencystringrequiredКрипто-токен, например USDT
networkstringrequiredСеть: TRC-20, ERC-20
crypto_addressstringrequiredАдрес кошелька получателя USDT
flowstringrequiredВсегда KGS_FLOW для MVP
payment_methodstringoptionalelqr (default)
callback_urlstringoptionalURL для webhook-уведомлений
kyc_share_tokenstringoptionalKYC passthrough от провайдера (Sumsub)
metadataobjectoptionalПроизвольные данные (order_id, user_id и т.д.)
JSON — ON_RAMP
{
  "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"
  }
}
201 Created — ON_RAMP

Ответ содержит объект payment с QR-данными для оплаты через ELQR.

JSON
{
  "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"
}
400 Bad Request
JSON
{ "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" }
401 Unauthorized
JSON
{ "error": "invalid or missing API key", "code": "UNAUTHORIZED" }
{ "error": "invalid signature", "code": "INVALID_SIGNATURE" }
{ "error": "timestamp too old", "code": "TIMESTAMP_EXPIRED" }
409 Conflict
JSON
{ "error": "duplicate idempotency key with different body", "code": "IDEMPOTENCY_CONFLICT" }
422 Unprocessable Entity
JSON
{ "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.

POST /v1/transactions direction: OFF_RAMP ✅ Реализовано

Тело запроса (OFF_RAMP)

ПолеТипОписание
directionstringrequiredOFF_RAMP
crypto_amountstringrequiredСумма USDT к отправке (строка с десятичной точкой). fiat_amount НЕ нужен — сервер рассчитает автоматически
crypto_currencystringrequiredВсегда USDT для MVP
networkstringrequiredСеть депозита: TRC20, ERC20, BEP20
recipient_kgs_walletstringrequired*Адрес KGS-кошелька получателя. Обязателен, если не указан recipient_phone
recipient_phonestringrequired*Номер телефона получателя (формат E.164). Обязателен, если не указан recipient_kgs_wallet
flowstringrequiredВсегда KGS_FLOW для MVP
callback_urlstringoptionalURL для webhook-уведомлений
kyc_share_tokenstringoptionalKYC passthrough от провайдера (Sumsub)
metadataobjectoptionalПроизвольные данные (order_id, user_id и т.д.)

* Укажите одно из двух: recipient_kgs_wallet или recipient_phone.

JSON — OFF_RAMP (кошелёк)
{
  "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"
  }
}
JSON — OFF_RAMP (телефон)
{
  "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"
}
201 Created — OFF_RAMP

Вместо объекта payment ответ содержит deposit_info — адрес, на который пользователь должен отправить USDT. Сумма в KGS (fiat_amount) рассчитана сервером.

JSON
{
  "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.

ПолеТипОписание
addressstringАдрес USDT-депозита. Пользователь должен отправить средства именно на этот адрес
networkstringБлокчейн-сеть: TRC20, ERC20 или BEP20
amountdecimal stringТочная сумма USDT для отправки (совпадает с crypto_amount из запроса)
currencystringВсегда USDT
expires_atstring (ISO 8601)Время истечения адреса. По истечении транзакция переходит в CANCELLED
⚠️
Отправляйте точную сумму USDT, указанную в deposit_info.amount, и только в сети deposit_info.network. Отправка другой суммы или в другой сети приведёт к FAILED.
ℹ️
После подтверждения транзакции в блокчейне VASP автоматически отправляет KGS получателю. Вы получите webhook payment.completed с fiat_amount и crypto_tx_hash.

API Reference

Получить транзакцию

GET /v1/transactions/:id ✅ Реализовано

Возвращает полные данные одной транзакции. Tenant isolation: вы можете получить только транзакции своего партнёра.

Path параметры

ПараметрТипОписание
idstring (UUID)ID транзакции из поля tx_id
200 OK

Возвращает полный объект транзакции (аналогичный ответу POST /v1/transactions).

404 Not Found
JSON
{ "error": "transaction not found", "code": "NOT_FOUND" }

API Reference

Получить котировку

GET /v1/quote ✅ Реализовано

Возвращает актуальную котировку для пары фиат/крипто. Включает зафиксированный курс, расчётную сумму крипто, комиссию и время истечения. Аутентификация не требуется.

ℹ️
Используйте этот эндпоинт перед созданием транзакции, чтобы показать пользователю точный курс.

Query параметры

ПараметрТипОписание
fiat_currencystringrequiredКод фиатной валюты, например KGS
crypto_currencystringrequiredКрипто-токен, например USDT
fiat_amountstringrequiredСумма в фиате
directionstringrequiredON_RAMP или OFF_RAMP
200 OK
JSON
{
  "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":   []
}

API Reference

Отменить транзакцию

POST /v1/transactions/:id/cancel ✅ Реализовано

Отменяет транзакцию. Только транзакции в статусе PENDING_PAYMENT могут быть отменены. Идемпотентен: повторный запрос с тем же Idempotency-Key вернёт исходный ответ.

JSON
{ "reason": "Customer requested cancellation" }

Поле reason — опциональное.

200 OK
JSON
{
  "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"
}
422 Unprocessable Entity
JSON
{ "error": "cannot cancel transaction in status COMPLETED", "code": "CANCEL_FAILED" }

API Reference

Список транзакций

GET /v1/transactions ✅ Реализовано

Возвращает пагинированный список транзакций вашего партнёра. Поддерживает фильтрацию по статусу и диапазону дат.

Query параметры

ПараметрТипDefaultОписание
pageinteger1Номер страницы (от 1)
limitinteger20Элементов на странице (max 100)
statusstringФильтр по статусу
fromdatetimeНачало диапазона (RFC3339)
todatetimeКонец диапазона (RFC3339)
200 OK
JSON
{
  "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

Отправляется когда транзакция успешно завершена и криптовалюта доставлена.

payment.completed → status: COMPLETED
JSON Payload
{
  "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

Отправляется когда транзакция завершилась ошибкой или была отменена.

payment.failed → status: FAILED
JSON Payload
{
  "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.createdPENDING_PAYMENTТранзакция создана, QR выдан
payment.completedCOMPLETEDКрипто доставлено
payment.cancelledCANCELLEDОтменена (пользователем или QR истёк)
payment.failedFAILEDОшибка обработки
payment.qr_scanned PAID Phase 2
payment.qr_expired FAILED Phase 2

Вебхуки

Верификация подписи

Каждый webhook-запрос подписан HMAC-SHA256. Всегда проверяйте подпись перед обработкой.

Заголовки входящего запроса

HTTP Headers
X-TwinBridge-Signature: abc123def456...
X-TwinBridge-Timestamp: 1743415800
Content-Type:           application/json

Алгоритм верификации

Pseudocode
expected = HMAC-SHA256(webhook_secret, timestamp + "." + raw_body)
assert constant_time_compare(expected, received_signature)

Пример верификации (Python)

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Код ошибкиОписание
400VALIDATION_ERRORОшибка валидации входных данных
400IDEMPOTENCY_MISSINGОтсутствует заголовок Idempotency-Key
400INVALID_ADDRESSНекорректный формат крипто-адреса
400INVALID_IDНекорректный формат ID транзакции
401UNAUTHORIZEDОтсутствует или неверный API ключ
401INVALID_SIGNATUREНеверная HMAC-подпись
401TIMESTAMP_EXPIREDTimestamp старше ±60 секунд
403FORBIDDENНет прав на данную операцию
404NOT_FOUNDТранзакция не найдена
409IDEMPOTENCY_CONFLICTКлюч уже использован с другим телом
409MAX_RETRIES_EXCEEDEDПревышено число повторных попыток
422EXCHANGER_UNAVAILABLEОбменник недоступен
422UNSUPPORTED_PAIRВалютная пара не поддерживается
422CANCEL_FAILEDНевозможно отменить в текущем статусе
422RATE_UNAVAILABLEКурс недоступен для данной пары
429RATE_LIMIT_EXCEEDEDПревышен лимит 100 запросов в минуту
500INTERNAL_ERRORВнутренняя ошибка сервера
503SERVICE_UNAVAILABLEСервис временно недоступен

Формат ответа с ошибкой

JSON
{
  "error": "fiat_amount must be greater than 0",
  "code":  "VALIDATION_ERROR"
}
ℹ️
Вопросы по интеграции? Напишите на [email protected] или свяжитесь с командой.