openapi: 3.0.3
info:
  title: Twin Bridge API
  version: 1.1.0
  description: |
    **Twin Bridge** — это B2B API-мост, соединяющий международных криптопровайдеров
    (например, Guardarian) с лицензированными криптообменниками (DantePay и другими).

    Twin Bridge передаёт информацию между провайдерами и обменниками — никогда не держит
    и не двигает средства. Платежи обрабатываются лицензированным обменником через ELQR.

    ## Базовый URL

    | Окружение   | Базовый URL |
    |-------------|-------------|
    | Продакшн    | `https://api.twinbridge.kg` |
    | Песочница   | `https://api.twinbridge.kg` + заголовок `X-Sandbox: true` |

    ## Аутентификация

    Все B2B-эндпоинты требуют подписи запроса с помощью **HMAC-SHA256**.

    **Обязательные заголовки для аутентифицированных запросов:**

    | Заголовок | Описание |
    |-----------|----------|
    | `X-API-Key` | Ваш партнёрский API-ключ |
    | `X-Signature` | Подпись HMAC-SHA256 (см. ниже) |
    | `X-Timestamp` | Unix timestamp (в секундах). Должен быть в пределах ±60с от серверного времени |
    | `Idempotency-Key` | UUID v4. **Обязателен для всех POST-запросов** |

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

    ```
    body_hash    = SHA256(байты_тела_запроса)  // hex-строка, нижний регистр
    message      = timestamp + HTTP_METHOD + URL_PATH + body_hash
    X-Signature  = HMAC-SHA256(api_secret, message)  // hex-строка, нижний регистр
    ```

    **Пример (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({"fiat_amount": "5000", "fiat_currency": "KGS",
                             "crypto_currency": "USDT", "direction": "ON_RAMP",
                             "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()
    ```

    ## Лимит запросов

    **100 запросов в минуту** на каждый API-ключ (алгоритм token bucket).
    При превышении API возвращает `429 Too Many Requests`.

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

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

    ## Режим песочницы

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

    ## Трёхшаговый flow для партнёров

    Twin Bridge использует **трёхшаговый процесс** для всех транзакций:

    1. **Обмен** — вызовите `POST /v1/transactions`, получите `tx_id`, курс обмена и данные QR для оплаты
    2. **Оплата** — покажите QR конечному пользователю для оплаты фиатом
    3. **Подтверждение** — Twin Bridge автоматически подтверждает оплату и доставляет крипто (ON_RAMP) или фиат (OFF_RAMP)

    ## Smart Routing (умная маршрутизация)

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

    ## Разница между ON_RAMP и OFF_RAMP

    | | ON_RAMP | OFF_RAMP |
    |---|---|---|
    | **Действие пользователя** | Платит фиатом (KGS по QR через ELQR) | Отправляет крипто |
    | **Получает** | Крипто (USDT/USDC) | Фиат (KGS через ELQR) |
    | **Обязательные поля** | `crypto_address`, `network` | `payment_method: elqr` |
    | **Сценарий** | Купить крипто за KGS | Обналичить крипто в KGS |

  contact:
    name: Поддержка интеграции Twin Bridge
    email: api@twinbridge.kg
  license:
    name: Proprietary
    url: https://twinbridge.kg

servers:
  - url: https://api.twinbridge.kg
    description: Продакшн
  - url: https://api.twinbridge.kg
    description: Песочница (добавьте заголовок X-Sandbox true)

tags:
  - name: Public
    description: Эндпоинты, не требующие аутентификации
  - name: Currencies
    description: Поддерживаемые валютные пары и методы оплаты
  - name: Transactions
    description: Управление жизненным циклом транзакций (требует аутентификации)
  - name: Webhooks
    description: Исходящие события, отправляемые Twin Bridge на ваш URL вебхука

paths:
  /v1/health:
    get:
      tags: [Public]
      summary: Проверка состояния системы
      description: |
        Возвращает текущий статус работоспособности системы Twin Bridge.
        Используйте этот эндпоинт для мониторинга доступности и проверки готовности сервиса.
      operationId: getHealth
      responses:
        '200':
          description: Статус системы
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              examples:
                healthy:
                  summary: Все системы работают в штатном режиме
                  value:
                    status: "ok"
                    db: "ok"
                    redis: "ok"
                    vasp: "ok"
                degraded:
                  summary: Проблема с базой данных
                  value:
                    status: "degraded"
                    db: "error"
                    redis: "ok"
                    vasp: "ok"

  /v1/currencies:
    get:
      tags: [Currencies]
      summary: Список поддерживаемых валютных пар
      description: |
        Возвращает список всех поддерживаемых пар фиат/крипто с лимитами,
        доступными сетями и поддержкой on-ramp/off-ramp.
        Аутентификация не требуется.
      operationId: getCurrencies
      responses:
        '200':
          description: Список поддерживаемых валютных пар
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrenciesResponse'
              example:
                currencies:
                  - fiat: "KGS"
                    crypto: "USDT"
                    networks: ["TRC-20", "ERC-20"]
                    min_fiat_amount: "500.00"
                    max_fiat_amount: "500000.00"
                    min_crypto_amount: "5.00"
                    max_crypto_amount: "5000.00"
                    on_ramp: true
                    off_ramp: true
                    status: "active"
                  - fiat: "KGS"
                    crypto: "USDC"
                    networks: ["TRC-20", "ERC-20"]
                    min_fiat_amount: "500.00"
                    max_fiat_amount: "500000.00"
                    min_crypto_amount: "5.00"
                    max_crypto_amount: "5000.00"
                    on_ramp: true
                    off_ramp: false
                    status: "active"

  /v1/payment-methods:
    get:
      tags: [Currencies]
      summary: Список методов оплаты
      description: |
        Возвращает все поддерживаемые методы приёма и выплаты фиата с лимитами,
        поддерживаемыми валютами и направлением (ON_RAMP / OFF_RAMP).
        Аутентификация не требуется.
      operationId: getPaymentMethods
      responses:
        '200':
          description: Список методов оплаты
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethodsResponse'
              example:
                payment_methods:
                  - id: "elqr"
                    name: "ELQR"
                    type: "qr_code"
                    currencies: ["KGS"]
                    direction: ["ON_RAMP", "OFF_RAMP"]
                    min_amount: "100.00"
                    max_amount: "500000.00"
                    processing_time: "instant"
                    status: "active"
                  - id: "bank_transfer"
                    name: "Банковский перевод"
                    type: "bank_transfer"
                    currencies: ["KGS"]
                    direction: ["ON_RAMP", "OFF_RAMP"]
                    min_amount: "500.00"
                    max_amount: "500000.00"
                    processing_time: "1-24 часа"
                    status: "active"

  /v1/rates:
    get:
      tags: [Public]
      summary: Получить текущий курс обмена
      description: |
        Возвращает текущий курс обмена для указанной валютной пары.
        Курсы кэшируются и обновляются в реальном времени от подключённых обменников.
        Аутентификация не требуется.
      operationId: getRates
      parameters:
        - name: pair
          in: query
          description: Валютная пара в формате `ФИАТ/КРИПТО`
          required: false
          schema:
            type: string
            default: KGS/USDT
            example: KGS/USDT
            enum:
              - KGS/USDT
              - KGS/USDC
      responses:
        '200':
          description: Текущий курс для запрошенной пары
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateResponse'
              example:
                pair: "KGS/USDT"
                rate: "88.42"
                spread: "0.50"
                min_amount: "500.00"
                max_amount: "500000.00"
                available_networks: ["TRC-20", "ERC-20"]
                updated_at: "2026-03-31T11:00:00Z"
        '503':
          description: Курс недоступен (проблема с подключением к обменнику)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "rate unavailable for pair: KGS/USDT"
                code: "RATE_UNAVAILABLE"

  /v1/transactions:
    post:
      tags: [Transactions]
      summary: Создать транзакцию
      description: |
        Создаёт новую транзакцию типа on-ramp или off-ramp.

        **Порядок действий:**
        1. Вызовите `POST /v1/transactions` → получите `tx_id` и данные QR-кода для оплаты
        2. Покажите QR конечному пользователю для оплаты фиатом
        3. Twin Bridge отслеживает обменник и транслирует статусы от VASP провайдеру
        4. Ваш вебхук получает уведомления о смене статуса на каждом этапе

        **On-ramp (фиат → крипто):**
        Пользователь оплачивает QR-кодом в KGS → VASP отправляет USDT/USDC на `crypto_address`, Twin Bridge передаёт уведомление провайдеру.

        **Off-ramp (крипто → фиат):**
        Twin Bridge передаёт `deposit_address` от VASP пользователю → пользователь отправляет крипто →
        обменник выплачивает KGS через ELQR.

        **Идемпотентность:** всегда передавайте уникальный `Idempotency-Key`.
        Повторный запрос с тем же ключом вернёт существующую транзакцию.

        > Создавать транзакции могут только партнёры с типом `PROVIDER`.
      operationId: createTransaction
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XApiKey'
        - $ref: '#/components/parameters/XSignature'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XSandbox'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTransactionRequest'
            examples:
              on_ramp_kgs_usdt:
                summary: ON_RAMP KGS → USDT (TRC-20)
                value:
                  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"
              on_ramp_with_kyc:
                summary: KGS → USDT с прохождением KYC
                value:
                  direction: "ON_RAMP"
                  fiat_amount: "50000.00"
                  fiat_currency: "KGS"
                  crypto_currency: "USDT"
                  network: "TRC-20"
                  crypto_address: "TRx7KzjN4GqLhU3k9pF2mW5vB8dY6cE1n"
                  payment_method: "elqr"
                  flow: "KGS_FLOW"
                  kyc_share_token: "ssn_applicant_abc123xyz"
                  sumsub_applicant_id: "6234a2f8b3c4d5e6f7a8b9c0"
              off_ramp_usdt_kgs:
                summary: OFF_RAMP USDT → KGS (на карту ЭЛКАРТ)
                value:
                  direction: "OFF_RAMP"
                  fiat_amount: "5000.00"
                  fiat_currency: "KGS"
                  crypto_currency: "USDT"
                  network: "TRC-20"
                  recipient_card: "4169123456781234"
                  payment_method: "elqr"
                  flow: "KGS_FLOW"
                  metadata:
                    order_id: "ORD-2026-002"
      responses:
        '201':
          description: |
            Транзакция создана и переведена в статус `PENDING_PAYMENT`.
            Объект `payment` содержит QR-код для конечного пользователя.
            Для OFF_RAMP: `deposit_address` содержит крипто-адрес обменника.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
              example:
                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-03T12: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-03-31T11:30:00Z"
                  amount: "5000.00"
                  currency: "KGS"
                created_at: "2026-03-31T11:00:00Z"
        '400':
          description: Ошибка валидации
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                validation_error:
                  value:
                    error: "fiat_amount must be greater than 0"
                    code: "VALIDATION_ERROR"
                idempotency_missing:
                  value:
                    error: "Idempotency-Key header is required for POST requests"
                    code: "IDEMPOTENCY_MISSING"
                network_missing:
                  value:
                    error: "network is required for ON_RAMP transactions"
                    code: "VALIDATION_ERROR"
                invalid_address:
                  value:
                    error: "crypto_address does not match TRC-20 format"
                    code: "INVALID_ADDRESS"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Доступ запрещён — только партнёры типа PROVIDER могут создавать транзакции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "only providers can create transactions"
                code: "FORBIDDEN"
        '422':
          description: Ошибка бизнес-логики (обменник недоступен, пара не поддерживается и т.д.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

    get:
      tags: [Transactions]
      summary: Список транзакций
      description: |
        Возвращает постраничный список транзакций аутентифицированного партнёра.
        Поддерживает фильтрацию по статусу и диапазону дат.
      operationId: listTransactions
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/XApiKey'
        - $ref: '#/components/parameters/XSignature'
        - $ref: '#/components/parameters/XTimestamp'
        - name: page
          in: query
          description: Номер страницы (начиная с 1)
          schema:
            type: integer
            minimum: 1
            default: 1
            example: 1
        - name: limit
          in: query
          description: Количество записей на странице (макс. 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
            example: 20
        - name: status
          in: query
          description: Фильтр по статусу транзакции
          schema:
            $ref: '#/components/schemas/TransactionStatus'
        - name: from
          in: query
          description: Начало диапазона дат (RFC3339)
          schema:
            type: string
            format: date-time
            example: "2026-03-01T00:00:00Z"
        - name: to
          in: query
          description: Конец диапазона дат (RFC3339)
          schema:
            type: string
            format: date-time
            example: "2026-03-31T23:59:59Z"
      responses:
        '200':
          description: Постраничный список транзакций
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListTransactionsResponse'
              example:
                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"
                    network: "TRC-20"
                    rate: "88.42"
                    crypto_tx_hash: "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
                    created_at: "2026-03-31T10:00:00Z"
                    paid_at: "2026-03-31T10:05:00Z"
                    completed_at: "2026-03-31T10:08:00Z"
                total: 42
                page: 1
                limit: 20
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /v1/transactions/{id}:
    get:
      tags: [Transactions]
      summary: Получить детали транзакции
      description: |
        Возвращает полные данные одной транзакции.

        **Изоляция по арендатору:** вы можете получить только транзакции, принадлежащие
        вашему API-ключу. Запрос транзакции другого партнёра вернёт `404`.
      operationId: getTransaction
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/TransactionId'
        - $ref: '#/components/parameters/XApiKey'
        - $ref: '#/components/parameters/XSignature'
        - $ref: '#/components/parameters/XTimestamp'
      responses:
        '200':
          description: Данные транзакции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
        '400':
          description: Неверный формат идентификатора транзакции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "invalid transaction id"
                code: "INVALID_ID"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /v1/transactions/{id}/cancel:
    post:
      tags: [Transactions]
      summary: Отменить транзакцию
      description: |
        Отменяет транзакцию. Отменить можно только транзакции в статусе `PENDING_PAYMENT`.
        Попытка отменить транзакцию в любом другом статусе вернёт ошибку.

        **Идемпотентно:** безопасно повторять с тем же `Idempotency-Key`.
      operationId: cancelTransaction
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/TransactionId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XApiKey'
        - $ref: '#/components/parameters/XSignature'
        - $ref: '#/components/parameters/XTimestamp'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelRequest'
            example:
              reason: "Отмена по запросу клиента"
      responses:
        '200':
          description: Транзакция успешно отменена
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionResponse'
              example:
                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-03-31T10:00:00Z"
        '400':
          description: Неверный идентификатор транзакции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Невозможно отменить — транзакция не находится в отменяемом статусе
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "cannot cancel transaction in status COMPLETED"
                code: "CANCEL_FAILED"
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /webhooks/your-endpoint:
    post:
      tags: [Webhooks]
      summary: События вебхука (исходящие)
      description: |
        Twin Bridge передаёт события вебхука на зарегистрированный вами URL обратного вызова
        при изменении статуса транзакции.

        **Этот путь представляет ВАШ эндпоинт** — настройте его в параметрах интеграции.
        Twin Bridge будет отправлять подписанные JSON-данные методом `POST`.

        Также можно переопределить URL вебхука для конкретной транзакции через
        поле `callback_url` в `CreateTransactionRequest`.

        ## Безопасность вебхука

        Каждый запрос вебхука подписан с помощью HMAC-SHA256:

        ```
        X-TwinBridge-Signature: HMAC-SHA256(webhook_secret, timestamp + "." + body)
        X-TwinBridge-Timestamp: <unix_timestamp>
        ```

        **Проверка на вашей стороне:**
        ```python
        import hmac, hashlib

        expected = hmac.new(
            webhook_secret.encode(),
            (timestamp + "." + raw_body).encode(),
            hashlib.sha256
        ).hexdigest()

        assert hmac.compare_digest(expected, received_signature)
        ```

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

        Если ваш эндпоинт вернул не-2xx ответ, Twin Bridge повторяет попытку:

        | Попытка | Задержка |
        |---------|----------|
        | 1 | немедленно |
        | 2 | 1 секунда |
        | 3 | 5 секунд |
        | 4 | 30 секунд |
        | 5 | 5 минут |

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

        ## Ответ

        Ваш эндпоинт **обязан вернуть HTTP 200** для подтверждения получения.
        Любой другой статус-код инициирует повторную попытку.

      operationId: webhookReceiver
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            examples:
              payment_created:
                summary: payment.created — транзакция создана
                value:
                  event_type: "payment.created"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "PENDING_PAYMENT"
                  timestamp: 1743415200
                  payload:
                    fiat_amount: "5000.00"
                    fiat_currency: "KGS"
                    crypto_currency: "USDT"
                    network: "TRC-20"
                    qr_expires_at: "2026-03-31T11:30:00Z"

              payment_qr_scanned:
                summary: payment.qr_scanned — QR-код отсканирован
                value:
                  event_type: "payment.qr_scanned"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "PAID"
                  timestamp: 1743415500

              payment_qr_expired:
                summary: payment.qr_expired — QR-код не был отсканирован вовремя
                value:
                  event_type: "payment.qr_expired"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "FAILED"
                  timestamp: 1743417000
                  payload:
                    reason: "qr_expired"

              payment_completed_onramp:
                summary: payment.completed — ON_RAMP криптовалюта доставлена
                value:
                  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: "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
                    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"

              payment_completed_offramp:
                summary: payment.completed — OFF_RAMP фиат выплачен
                value:
                  event_type: "payment.completed"
                  tx_id: "660e9500-f30c-52e5-b827-557766551111"
                  status: "COMPLETED"
                  timestamp: 1743416000
                  payload:
                    fiat_amount: "5000.00"
                    fiat_currency: "KGS"
                    payout_method: "elqr"
                    payout_reference: "REF123456"
                    rate: "88.42"
                    metadata: {}

              payment_cancelled:
                summary: payment.cancelled — транзакция отменена
                value:
                  event_type: "payment.cancelled"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "CANCELLED"
                  timestamp: 1743415650

              kyc_verified:
                summary: payment.kyc_verified — KYC пройден
                value:
                  event_type: "payment.kyc_verified"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "PROCESSING"
                  timestamp: 1743415700

              kyc_rejected:
                summary: payment.kyc_rejected — KYC отклонён
                value:
                  event_type: "payment.kyc_rejected"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "CANCELLED"
                  timestamp: 1743415700
                  payload:
                    reason: "document_mismatch"

              payment_partial:
                summary: payment.partial — частичная оплата получена
                value:
                  event_type: "payment.partial"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "DISPUTED"
                  timestamp: 1743415700
                  payload:
                    paid_amount: "2500.00"
                    expected_amount: "5000.00"
                    fiat_currency: "KGS"

              payment_late:
                summary: payment.late — оплата после истечения QR
                value:
                  event_type: "payment.late"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "DISPUTED"
                  timestamp: 1743417500
                  payload:
                    paid_at: "2026-03-31T12:00:00Z"
                    expired_at: "2026-03-31T11:30:00Z"

              kyc_verified_event:
                summary: kyc.verified — KYC пройден
                value:
                  event_type: "kyc.verified"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "PROCESSING"
                  timestamp: 1743415700

              kyc_rejected_event:
                summary: kyc.rejected — KYC отклонён
                value:
                  event_type: "kyc.rejected"
                  tx_id: "550e8400-e29b-41d4-a716-446655440000"
                  status: "CANCELLED"
                  timestamp: 1743415700
                  payload:
                    reason: "document_mismatch"

      parameters:
        - name: X-TwinBridge-Signature
          in: header
          required: true
          description: HMAC-SHA256(webhook_secret, timestamp + "." + тело_запроса)
          schema:
            type: string
            example: "a3f8c21d4e..."
        - name: X-TwinBridge-Timestamp
          in: header
          required: true
          description: Unix timestamp события
          schema:
            type: string
            example: "1743415200"
      responses:
        '200':
          description: Вебхук успешно принят

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Ваш партнёрский API-ключ Twin Bridge.
        Должен использоваться совместно с заголовками `X-Signature` и `X-Timestamp`.

  parameters:
    XApiKey:
      name: X-API-Key
      in: header
      required: true
      description: Партнёрский API-ключ
      schema:
        type: string
        example: "tb_live_abc123xyz"

    XSignature:
      name: X-Signature
      in: header
      required: true
      description: |
        Подпись HMAC-SHA256.
        `HMAC-SHA256(api_secret, timestamp + method + path + sha256(body))`
      schema:
        type: string
        example: "a3f8c21d4e9b7c2a..."

    XTimestamp:
      name: X-Timestamp
      in: header
      required: true
      description: Unix timestamp в секундах. Должен быть в пределах ±60с от серверного времени.
      schema:
        type: string
        example: "1743415200"

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: UUID v4. Обязателен для всех POST-запросов. Повторный запрос с тем же ключом вернёт исходный ответ.
      schema:
        type: string
        format: uuid
        example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"

    XSandbox:
      name: X-Sandbox
      in: header
      required: false
      description: Установите `true` для использования песочницы и мок-адаптеров. Реальные платежи не выполняются.
      schema:
        type: string
        enum: ["true", "false"]
        example: "true"

    TransactionId:
      name: id
      in: path
      required: true
      description: UUID транзакции
      schema:
        type: string
        format: uuid
        example: "550e8400-e29b-41d4-a716-446655440000"

  schemas:
    TransactionStatus:
      type: string
      description: |
        Текущий статус транзакции.

        **Штатный поток:**
        ```
        INITIATED → PENDING_PAYMENT → PAID → PROCESSING → COMPLETED
        ```

        **Отмена:**
        ```
        PENDING_PAYMENT → CANCELLED
        ```
      enum:
        - INITIATED
        - PENDING_PAYMENT
        - PAID
        - PROCESSING
        - COMPLETED
        - FAILED
        - CANCELLED
      example: PENDING_PAYMENT

    CurrencyPair:
      type: object
      description: Поддерживаемая пара фиат/крипто с лимитами и доступностью
      properties:
        fiat:
          type: string
          description: Код фиатной валюты по ISO 4217
          example: "KGS"
        crypto:
          type: string
          description: Тикер криптовалюты
          example: "USDT"
        networks:
          type: array
          items:
            type: string
          description: Поддерживаемые блокчейн-сети для данной пары
          example: ["TRC-20", "ERC-20"]
        min_fiat_amount:
          type: string
          description: Минимальная сумма в фиате (строка-десятичное число)
          example: "500.00"
        max_fiat_amount:
          type: string
          description: Максимальная сумма в фиате (строка-десятичное число)
          example: "500000.00"
        min_crypto_amount:
          type: string
          description: Минимальная сумма в крипто (строка-десятичное число)
          example: "5.00"
        max_crypto_amount:
          type: string
          description: Максимальная сумма в крипто (строка-десятичное число)
          example: "5000.00"
        on_ramp:
          type: boolean
          description: Доступен ли ON_RAMP (фиат → крипто) для данной пары
          example: true
        off_ramp:
          type: boolean
          description: Доступен ли OFF_RAMP (крипто → фиат) для данной пары
          example: true
        status:
          type: string
          enum: [active, inactive, maintenance]
          description: Текущий статус доступности пары
          example: "active"

    CurrenciesResponse:
      type: object
      properties:
        currencies:
          type: array
          items:
            $ref: '#/components/schemas/CurrencyPair'

    PaymentMethod:
      type: object
      description: Метод приёма или выплаты фиата
      properties:
        id:
          type: string
          description: Машиночитаемый идентификатор метода оплаты
          enum: [elqr, bank_transfer]
          example: "elqr"
        name:
          type: string
          description: Название метода оплаты для отображения пользователю
          example: "ELQR"
        type:
          type: string
          description: Технический тип метода оплаты
          enum: [qr_code, bank_transfer]
          example: "qr_code"
        currencies:
          type: array
          items:
            type: string
          description: Поддерживаемые фиатные валюты
          example: ["KGS"]
        direction:
          type: array
          items:
            type: string
            enum: [ON_RAMP, OFF_RAMP]
          description: Поддерживаемые направления для данного метода
          example: ["ON_RAMP"]
        min_amount:
          type: string
          description: Минимальная сумма в фиате (строка-десятичное число)
          example: "100.00"
        max_amount:
          type: string
          description: Максимальная сумма в фиате (строка-десятичное число)
          example: "500000.00"
        processing_time:
          type: string
          description: Ожидаемое время обработки
          example: "instant"
        status:
          type: string
          enum: [active, inactive, maintenance]
          example: "active"

    PaymentMethodsResponse:
      type: object
      properties:
        payment_methods:
          type: array
          items:
            $ref: '#/components/schemas/PaymentMethod'

    CreateTransactionRequest:
      type: object
      required:
        - direction
        - fiat_amount
        - fiat_currency
        - crypto_currency
        - flow
      properties:
        direction:
          type: string
          description: |
            Направление транзакции.
            - `ON_RAMP` — фиат на вход, крипто на выход (пользователь платит KGS, получает USDT)
            - `OFF_RAMP` — крипто на вход, фиат на выход (пользователь отправляет USDT, получает KGS на карту)
          enum: [ON_RAMP, OFF_RAMP]
          example: ON_RAMP
        fiat_amount:
          type: string
          description: |
            Сумма в фиате в виде строки-десятичного числа. Должна быть положительной.
            Максимум: `9999999999.99999999`.
          example: "5000.00"
        fiat_currency:
          type: string
          description: |
            Код фиатной валюты по ISO 4217.
            Фаза 1: только `KGS`. Фаза 2: `KZT`, `UZS`.
          enum: [KGS, KZT, UZS]
          example: KGS
        crypto_currency:
          type: string
          description: |
            Тикер криптовалюты.
            Фаза 1: `USDT`, `USDC`. Фаза 2: `BTC`, `ETH`, `TON`.
          enum: [USDT, USDC, BTC, ETH, TON]
          example: USDT
        network:
          type: string
          description: |
            Блокчейн-сеть для доставки или получения криптовалюты.
            **Обязательно для ON_RAMP.** Необязательно для OFF_RAMP (по умолчанию TRC-20 для USDT/USDC).

            Доступность сетей по криптовалюте:
            - `USDT`: TRC-20, ERC-20
            - `USDC`: TRC-20, ERC-20
            - `TON`: TON
            - `BTC`: (Фаза 2)
            - `ETH`: ERC-20 (Фаза 2)
          enum: ["TRC-20", "ERC-20", "TON"]
          example: "TRC-20"
        crypto_address:
          type: string
          description: |
            Адрес криптокошелька для доставки при ON_RAMP.
            **Обязателен для направления ON_RAMP.**

            Формат адреса по сети:
            - `TRC-20`: 34 символа, начинается с `T`, base58. Пример: `TRx7KzjN4GqLhU3k9pF2mW5vB8dY6cE1n`
            - `ERC-20`: 42 символа, начинается с `0x`, hex. Пример: `0xAbCd1234...`
            - `TON`: 48 символов, base64url. Пример: `EQBv...`
          example: "TRx7KzjN4GqLhU3k9pF2mW5vB8dY6cE1n"
        payment_method:
          type: string
          description: |
            Метод приёма или выплаты фиата.
            - `elqr` — ELQR QR-платёж. По умолчанию. Доступен для ON_RAMP и OFF_RAMP.
            - `bank_transfer` — Банковский перевод. Фаза 2.
          enum: [elqr, bank_transfer]
          default: "elqr"
          example: "elqr"
        flow:
          type: string
          description: |
            Внутренний маршрут обработки.
            - `KGS_FLOW` — маршрутизация через обменники KGS (DantePay)
            - `RUB_FLOW` — маршрутизация через обменники RUB (Фаза 2)
          enum: [KGS_FLOW, RUB_FLOW]
          example: KGS_FLOW
        kyc_share_token:
          type: string
          description: |
            Токен Sumsub для прохождения KYC.

            Это `applicantId` или share-токен Sumsub, выданный вашим KYC-потоком.
            Формат: `ssn_XXXX` (share-токен) или UUID (applicant ID).
            Пример: `ssn_applicant_abc123xyz`

            Twin Bridge передаёт этот токен обменнику as-is для верификации личности.
            Обменник выполняет KYC-проверки напрямую через Sumsub.

            **Когда требуется:** для транзакций выше регуляторного порога KYC
            (в настоящее время 30 000 KGS на одну транзакцию или 100 000 KGS
            в совокупности за месяц). Для сумм ниже порога поле необязательно,
            но рекомендуется.

            Результат KYC доставляется через вебхук `payment.kyc_verified`
            или `payment.kyc_rejected`.
          example: "ssn_applicant_abc123xyz"
        sumsub_applicant_id:
          type: string
          description: |
            Applicant ID в Sumsub для прямого запроса KYC.
            Дополнительное поле к `kyc_share_token`.
            Используйте, когда applicant ID хранится отдельно от share-токена
            и вы хотите разрешить обменнику делать прямые запросы к Sumsub API.
          example: "6234a2f8b3c4d5e6f7a8b9c0"
        callback_url:
          type: string
          format: uri
          description: |
            URL вебхука для конкретной транзакции (переопределяет партнёрский URL).
            Если указан, Twin Bridge будет отправлять все события вебхука для этой
            транзакции на данный URL вместо вашего URL по умолчанию.
            Должен быть валидным HTTPS-адресом.
          example: "https://yourapp.com/webhooks/twinbridge/tx-specific"
        metadata:
          type: object
          description: |
            Произвольный JSON-объект для отслеживания на стороне партнёра (макс. 1 КБ).
            Объект хранится как есть и возвращается во всех событиях вебхука
            для данной транзакции. Используйте для номеров заказов, ID пользователей
            или любых внутренних данных.
          additionalProperties: true
          example:
            order_id: "ORD-2026-001"
            user_id: "usr_12345"

    FeeInfo:
      type: object
      description: Детализация комиссий по транзакции
      properties:
        vasp_fee:
          type: string
          description: Комиссия обменника (строка-десятичное число)
          example: "25.00"
        vasp_fee_currency:
          type: string
          description: Валюта комиссии обменника
          example: "KGS"
        network_fee:
          type: string
          description: Сетевая комиссия блокчейна (строка-десятичное число)
          example: "1.00"
        network_fee_currency:
          type: string
          description: Валюта сетевой комиссии (обычно криптовалюта транзакции)
          example: "USDT"
        total_fee_fiat:
          type: string
          description: Итоговая комиссия в фиатном эквиваленте (строка-десятичное число)
          example: "113.42"
        total_fee_currency:
          type: string
          description: Валюта итоговой комиссии в фиате
          example: "KGS"

    TransactionResponse:
      type: object
      properties:
        tx_id:
          type: string
          format: uuid
          description: Уникальный идентификатор транзакции в системе Twin Bridge
          example: "550e8400-e29b-41d4-a716-446655440000"
        status:
          $ref: '#/components/schemas/TransactionStatus'
        direction:
          type: string
          enum: [ON_RAMP, OFF_RAMP]
          example: ON_RAMP
        fiat_amount:
          type: string
          description: Сумма в фиате (строка-десятичное число)
          example: "5000.00"
        fiat_currency:
          type: string
          example: KGS
        crypto_amount:
          type: string
          description: Фактическая доставленная сумма крипто (устанавливается после COMPLETED)
          example: "56.55"
        crypto_currency:
          type: string
          example: USDT
        network:
          type: string
          description: Блокчейн-сеть, используемая для данной транзакции
          example: "TRC-20"
        crypto_address:
          type: string
          description: Адрес криптокошелька из запроса (эхо-возврат для ON_RAMP)
          example: "TRx7KzjN4GqLhU3k9pF2mW5vB8dY6cE1n"
        crypto_tx_hash:
          type: string
          description: |
            Хэш транзакции в блокчейне. Устанавливается после перехода статуса в COMPLETED.
            Можно использовать для независимой верификации on-chain перевода.
          example: "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
        deposit_address:
          type: string
          description: |
            Только для OFF_RAMP: крипто-адрес обменника, на который пользователь
            должен отправить криптовалюту. Появляется после создания транзакции.
          example: "TExchangerWalletAddressXXXXXXXXXXXX"
        deposit_memo:
          type: string
          description: |
            Опциональный memo/tag, требуемый некоторыми сетями (например, TON, XRP)
            при отправке на deposit_address. Всегда включайте если указан.
          example: "1234567890"
        rate:
          type: string
          description: Зафиксированный курс обмена на момент создания транзакции
          example: "88.42"
        vasp_id:
          type: string
          description: Идентификатор обменника, исполнившего транзакцию (для прозрачности)
          example: "dantepay-01"
        fee:
          $ref: '#/components/schemas/FeeInfo'
        estimated_completion:
          type: string
          format: date-time
          description: Ожидаемое время завершения (ISO 8601 UTC). Может обновляться в процессе.
          example: "2026-04-03T12:05:00Z"
        payment:
          $ref: '#/components/schemas/PaymentInfo'
        failure_reason:
          type: string
          description: Причина ошибки в читаемом виде (устанавливается при статусах FAILED или CANCELLED)
          example: "qr_expired"
        created_at:
          type: string
          format: date-time
          example: "2026-03-31T10:00:00Z"
        paid_at:
          type: string
          format: date-time
          description: Время подтверждения фиатного платежа
          example: "2026-03-31T10:05:00Z"
        completed_at:
          type: string
          format: date-time
          description: Время доставки криптовалюты (ON_RAMP) или выплаты фиата (OFF_RAMP)
          example: "2026-03-31T10:08:00Z"

    PaymentInfo:
      type: object
      description: Данные QR-кода для оплаты. Присутствует когда статус `PENDING_PAYMENT`.
      properties:
        qr_data:
          type: string
          description: |
            Строка с данными QR-кода. Отрисуйте её как QR-изображение для конечного пользователя.
            Формат соответствует киргизскому национальному стандарту платёжного QR (совместимо с EMVCo).
          example: "00020101021226580014kg.elqr.payment..."
        qr_url:
          type: string
          format: uri
          description: Опциональная прямая ссылка для оплаты (диплинк в банковское приложение)
          example: "https://elqr.kg/qr/abc123"
        expires_at:
          type: string
          format: date-time
          description: Время истечения QR-кода. После этого отправляется вебхук `payment.qr_expired`.
          example: "2026-03-31T11:30:00Z"
        amount:
          type: string
          description: Сумма к оплате конечным пользователем
          example: "5000.00"
        currency:
          type: string
          description: Валюта платежа
          example: KGS

    ListTransactionsResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/TransactionResponse'
        total:
          type: integer
          description: Общее количество транзакций, соответствующих фильтру
          example: 42
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20

    CancelRequest:
      type: object
      properties:
        reason:
          type: string
          description: Опциональная причина отмены в читаемом виде
          example: "Отмена по запросу клиента"

    RateResponse:
      type: object
      properties:
        pair:
          type: string
          description: Валютная пара (ФИАТ/КРИПТО)
          example: KGS/USDT
        rate:
          type: string
          description: Текущий курс обмена (1 КРИПТО = rate ФИАТ)
          example: "88.42"
        spread:
          type: string
          description: Спред обменника, применяемый к базовому курсу (строка-десятичное число)
          example: "0.50"
        min_amount:
          type: string
          description: Минимальная сумма в фиате для данной пары (строка-десятичное число)
          example: "500.00"
        max_amount:
          type: string
          description: Максимальная сумма в фиате для данной пары (строка-десятичное число)
          example: "500000.00"
        available_networks:
          type: array
          items:
            type: string
          description: Блокчейн-сети, доступные в данный момент для данной пары
          example: ["TRC-20", "ERC-20"]
        updated_at:
          type: string
          format: date-time
          description: Время последнего обновления курса
          example: "2026-03-31T11:00:00Z"

    HealthResponse:
      type: object
      properties:
        status:
          type: string
          enum: [ok, degraded]
          example: ok
        db:
          type: string
          enum: [ok, error]
          example: ok
        redis:
          type: string
          enum: [ok, error]
          example: ok
        vasp:
          type: string
          enum: [ok, error]
          example: ok

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Сообщение об ошибке в читаемом виде
          example: "invalid transaction id"
        code:
          type: string
          description: Машиночитаемый код ошибки
          example: INVALID_ID
        tx_id:
          type: string
          format: uuid
          description: Идентификатор транзакции (когда применимо)
          example: "550e8400-e29b-41d4-a716-446655440000"

    WebhookEvent:
      type: object
      required: [event_type, tx_id, status, timestamp]
      properties:
        event_type:
          type: string
          description: Тип события
          enum:
            - payment.created
            - payment.qr_scanned
            - payment.qr_expired
            - payment.completed
            - payment.cancelled
            - payment.partial
            - payment.late
            - payment.kyc_verified
            - payment.kyc_rejected
            - kyc.verified
            - kyc.rejected
          example: payment.completed
        tx_id:
          type: string
          format: uuid
          description: Идентификатор транзакции в системе Twin Bridge
          example: "550e8400-e29b-41d4-a716-446655440000"
        status:
          $ref: '#/components/schemas/TransactionStatus'
        timestamp:
          type: integer
          format: int64
          description: Unix timestamp события
          example: 1743415200
        payload:
          type: object
          description: |
            Данные, специфичные для типа события. Содержимое зависит от event_type.

            **payment.completed (ON_RAMP):**
            - `crypto_amount`, `crypto_currency`, `crypto_tx_hash`, `network`, `rate`, `fee`, `metadata`

            **payment.completed (OFF_RAMP):**
            - `fiat_amount`, `fiat_currency`, `payout_method`, `payout_reference`, `rate`, `metadata`
          additionalProperties: true

  responses:
    Unauthorized:
      description: Ошибка аутентификации
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            missing_headers:
              summary: Отсутствуют заголовки аутентификации
              value:
                error: "missing authentication headers"
                code: "AUTH_MISSING"
            invalid_signature:
              summary: Несоответствие подписи
              value:
                error: "invalid signature"
                code: "AUTH_INVALID_SIGNATURE"
            expired_timestamp:
              summary: Timestamp устарел
              value:
                error: "invalid or expired timestamp"
                code: "AUTH_TIMESTAMP"

    NotFound:
      description: Транзакция не найдена (или принадлежит другому партнёру)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "transaction not found"
            code: "NOT_FOUND"

    RateLimitExceeded:
      description: Превышен лимит запросов — 100 запросов в минуту на API-ключ
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "rate limit exceeded: 100 requests per minute"
            code: "RATE_LIMITED"
      headers:
        Retry-After:
          description: Количество секунд до следующей попытки
          schema:
            type: integer
            example: 30
