openapi: 3.0.3

info:
  title: Bet-Flow Merchant API
  version: "2.0.0"
  description: |
    Публичный Merchant API Bet-Flow для приёма платежей (PayIn) и выплат (PayOut).

    **Base URL:** `https://api.bet-flow.com/v1`

    Полное руководство по подключению: [Integration Guide](/integration.html)

    ---

    ## Безопасность

    | Требование | Описание |
    |------------|----------|
    | HTTPS | Все запросы только по TLS 1.2+. HTTP не принимается. |
    | Подпись запроса | Каждый запрос подписывается **HMAC-SHA256** секретным ключом. |
    | Срок жизни | Заголовок `Expires` — Unix timestamp (сек). Запрос отклоняется после истечения (`408`). |
    | Anti-replay | Заголовок `X-Nonce` обязателен. Одно значение nonce нельзя использовать повторно в течение 5 минут. |
    | Rate limit | До **120 запросов в минуту** на один `X-API-Key`. При превышении — `429`. |
    | IP whitelist | Если список задан в [ЛК](https://app.bet-flow.com/integration), запросы принимаются **только** с этих IP/CIDR. Пустой список = все IP разрешены. |
    | Секретный ключ | Храните только на сервере. При компрометации — немедленная ротация в ЛК. |
    | Callback | Проверяйте заголовок `X-Signature` (HMAC-SHA256 от **сырого** JSON-тела). |

    ---

    ## Аутентификация запросов (HMAC-SHA256)

    ### Обязательные заголовки

    | Header | Описание |
    |--------|----------|
    | `Content-Type` | `application/json` (для POST) |
    | `X-API-Key` | Публичный API-ключ из личного кабинета |
    | `Expires` | Unix timestamp (сек), до которого запрос действителен. Рекомендуется **now + 300…600 сек**. |
    | `X-API-Sign` | HMAC-SHA256 подпись (hex, lowercase) |
    | `X-Nonce` | Уникальная строка (UUID v4). **Обязательна.** |

    ### Формирование подписи

    ```
    message = Expires + payload
    X-API-Sign = hex(HMAC-SHA256(secret_key, message))
    ```

    **GET-запросы:** `payload` = query string как в URL (параметры отсортированы Go `url.Values.Encode()`).

    Пример: `GET /v1/payin?id=abc123` при `Expires=1760635100`:
    ```
    message = "1760635100id=abc123"
    ```

    **POST-запросы:** тело парсится как JSON → map → сериализуется обратно в JSON (компактно). `payload` = эта JSON-строка.

    Пример:
    ```json
    {"amount":"1000","currency":"RUB","externalID":"order-42"}
    ```
    ```
    message = "1760635100{\"amount\":\"1000\",\"currency\":\"RUB\",\"externalID\":\"order-42\"}"
    ```

    > **Важно:** подписывайте JSON **после** финальной сериализации тела, которое отправляете в запросе.

    ---

    ## Webhook (callback от Bet-Flow к мерчанту)

    При смене статуса операции Bet-Flow отправляет **POST** на `callbackURL`:
    - из настроек в [личном кабинете](https://app.bet-flow.com/integration), или
    - переданный в поле `callbackURL` при создании операции (приоритет над глобальным).

    ### Тело webhook

    ```json
    {
      "order_id": "0199ee07-dffa-754c-ba2e-c28c2b2ea15a",
      "merchant_order_id": "order-42",
      "status": "success",
      "amount": "1000.00",
      "currency": "RUB",
      "timestamp": 1760635122
    }
    ```

    | Поле | Описание |
    |------|----------|
    | `order_id` | Внутренний ID операции Bet-Flow |
    | `merchant_order_id` | Ваш `externalID` |
    | `status` | Статус в **нижнем регистре** |
    | `amount` | Фактическая сумма |
    | `currency` | Валюта операции |
    | `timestamp` | Unix timestamp отправки |

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

    ```
    X-Signature = hex(HMAC-SHA256(secret_key, raw_request_body))
    ```

    Сравнивайте подпись **constant-time**. Ответьте HTTP `200` при успешной обработке.
    Bet-Flow повторяет доставку при ошибках сети или non-2xx ответах.

    ---

    ## Ошибки

    ```json
    {
      "success": false,
      "error": {
        "cause": "invalid X-API-SIGN"
      }
    }
    ```

    | HTTP | cause | Описание |
    |------|-------|----------|
    | 400 | missing X-API-KEY | Нет заголовка X-API-KEY |
    | 400 | missing X-API-SIGN | Нет подписи |
    | 400 | missing EXPIRES | Нет Expires |
    | 401 | invalid X-API-KEY | Неверный ключ |
    | 401 | invalid X-API-SIGN | Неверная подпись |
      | 403 | forbidden | Нет X-Nonce, replay nonce |
      | 403 | source IP is not whitelisted | IP не в whitelist (если список задан в ЛК) |
    | 408 | request has expired | Истёк Expires |
    | 409 | external id already exists | Дубликат externalID |
    | 409 | resource conflict | Конфликт (в т.ч. idempotency) |
    | 422 | cannot parse request body | Невалидный JSON |
    | 429 | too many requests | Rate limit |
    | 500 | internal server error | Ошибка сервера |

servers:
  - url: https://api.bet-flow.com/v1
    description: Production

tags:
  - name: System
    description: Баланс и системные методы
  - name: Dictionaries
    description: Справочники
  - name: PayIn
    description: Приём платежей
  - name: PayOut
    description: Выплаты
  - name: Webhook
    description: Уведомления на стороне мерчанта (исходящие от Bet-Flow)

security:
  - MerchantHmac: []

components:
  securitySchemes:
    MerchantHmac:
      type: apiKey
      in: header
      name: X-API-Sign
      description: |
        HMAC-SHA256 подпись. Также обязательны заголовки X-API-Key, Expires, X-Nonce.

  parameters:
    ExpiresHeader:
      name: Expires
      in: header
      required: true
      schema:
        type: string
      example: "1760635100"
    ApiKeyHeader:
      name: X-API-Key
      in: header
      required: true
      schema:
        type: string
    ApiSignHeader:
      name: X-API-Sign
      in: header
      required: true
      schema:
        type: string
    NonceHeader:
      name: X-Nonce
      in: header
      required: true
      schema:
        type: string
      example: "550e8400-e29b-41d4-a716-446655440000"

  schemas:
    SuccessEnvelope:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object

    ErrorResponse:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required: [cause]
          properties:
            cause:
              type: string
              example: "invalid X-API-SIGN"

    StatusEnum:
      type: string
      enum:
        - created
        - pending
        - waiting_payment
        - success
        - success_recalc
        - error
        - expired
        - dispute
        - wrong_amount
        - wrong_bank
        - manual_check
        - refunded
      description: Статус операции (нижний регистр)

    CurrencyEnum:
      type: string
      enum: [RUB, UZS, KGS, KZT, USD, EUR, INR, EGP, TJS, TRY, AZN]
      example: RUB

    PayTypeEnum:
      type: string
      enum: [card, sbp, nspk, account, payform]
      example: card
      description: |
        - `card` — перевод на карту
        - `sbp` — СБП
        - `nspk` — НСПК / QR
        - `account` — номер счёта
        - `payform` — платёжная форма

    BalanceItem:
      type: object
      required: [type, available, frozen]
      properties:
        type:
          type: string
          enum: [in, out]
          description: "`in` — приём, `out` — выплаты"
        available:
          type: string
          example: "12457.89"
        frozen:
          type: string
          example: "0.00"

    CurrencyBalance:
      type: object
      required: [currency, items]
      properties:
        currency:
          type: string
          example: RUB
        items:
          type: array
          items:
            $ref: '#/components/schemas/BalanceItem'

    BankItem:
      type: object
      properties:
        name:
          type: string
          example: SBER
          description: Код банка
        currency:
          $ref: '#/components/schemas/CurrencyEnum'

    CommissionItem:
      type: object
      properties:
        bank:
          type: string
          example: SBER
        currency:
          $ref: '#/components/schemas/CurrencyEnum'
        type:
          type: string
          enum: [in, out]
          description: "`in` — PayIn, `out` — PayOut"
        percent:
          type: string
          example: "1.50"
        minAmount:
          type: string
          example: "100.00"
        maxAmount:
          type: string
          example: "150000.00"

    Card:
      type: object
      properties:
        expMonth:
          type: string
          example: "12"
        expYear:
          type: string
          example: "30"

    Requisite:
      type: object
      description: Реквизиты для оплаты (PayIn)
      properties:
        method:
          type: string
        bank:
          type: string
        bank_code:
          type: string
        cardNumber:
          type: string
          nullable: true
        card_number:
          type: string
          nullable: true
        accountNumber:
          type: string
          nullable: true
        nspkURL:
          type: string
          nullable: true
        phoneNumber:
          type: string
          nullable: true
        holder:
          type: string
          nullable: true
        holder_name:
          type: string
          nullable: true
        paymentURL:
          type: string
          nullable: true
        qrURL:
          type: string
          nullable: true
        country:
          type: string
        country_code:
          type: string
        expected_local_amount:
          type: integer
          format: int64
        local_currency:
          type: string

    PayInCreateRequest:
      type: object
      required: [externalID, currency, amount]
      properties:
        externalID:
          type: string
          description: Ваш уникальный ID заказа
          example: "order-42"
        currency:
          $ref: '#/components/schemas/CurrencyEnum'
        amount:
          type: string
          example: "1000.00"
        bank:
          type: string
          example: SBER
          description: Код банка или `any` для любого доступного
        type:
          $ref: '#/components/schemas/PayTypeEnum'
        callbackURL:
          type: string
          format: uri
          example: "https://merchant.example/webhook/betflow"
        merchantUserID:
          type: string
          example: "user-7683"

    PayInCreateResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                id:
                  type: string
                  description: Внутренний ID Bet-Flow
                externalID:
                  type: string
                currency:
                  type: string
                type:
                  $ref: '#/components/schemas/PayTypeEnum'
                amount:
                  type: string
                country:
                  type: string
                  example: ru
                status:
                  $ref: '#/components/schemas/StatusEnum'
                requisite:
                  $ref: '#/components/schemas/Requisite'
                createdAt:
                  type: string
                  format: date-time
                expires_at:
                  type: string
                  format: date-time

    PayInGetResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                id:
                  type: string
                externalID:
                  type: string
                currency:
                  type: string
                status:
                  $ref: '#/components/schemas/StatusEnum'
                holder:
                  type: string
                requisite:
                  $ref: '#/components/schemas/Requisite'
                createdAt:
                  type: string
                  format: date-time
                updatedAt:
                  type: string
                  format: date-time
                amount:
                  type: string
                commission:
                  type: string
                expires_at:
                  type: string
                  format: date-time

    PayOutCreateRequest:
      type: object
      required: [externalID, type, currency, amount, recipient]
      properties:
        externalID:
          type: string
          example: "payout-99"
        bank:
          type: string
          example: SBER
        type:
          $ref: '#/components/schemas/PayTypeEnum'
        currency:
          $ref: '#/components/schemas/CurrencyEnum'
        amount:
          type: string
          example: "5000.00"
        recipient:
          type: string
          description: Карта, телефон (СБП) или другой реквизит получателя
          example: "4000000000000000"
        callbackURL:
          type: string
          format: uri
        holder:
          type: string
        iban:
          type: string
        ifscCode:
          type: string
        tcID:
          type: string
        card:
          $ref: '#/components/schemas/Card'

    PayOutCreateResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                id:
                  type: string
                externalID:
                  type: string
                bank:
                  type: string
                currency:
                  type: string
                type:
                  $ref: '#/components/schemas/PayTypeEnum'
                status:
                  $ref: '#/components/schemas/StatusEnum'
                recipient:
                  type: string
                amount:
                  type: string
                commission:
                  type: string
                createdAt:
                  type: string
                  format: date-time

    PayOutGetResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                id:
                  type: string
                externalID:
                  type: string
                currency:
                  type: string
                status:
                  $ref: '#/components/schemas/StatusEnum'
                holder:
                  type: string
                recipient:
                  type: string
                createdAt:
                  type: string
                  format: date-time
                updatedAt:
                  type: string
                  format: date-time
                amount:
                  type: string
                commission:
                  type: string

    WebhookPayload:
      type: object
      required: [order_id, status, amount, timestamp]
      properties:
        order_id:
          type: string
          description: Внутренний ID Bet-Flow
        merchant_order_id:
          type: string
          description: Ваш externalID
        status:
          $ref: '#/components/schemas/StatusEnum'
        amount:
          type: string
        currency:
          type: string
        timestamp:
          type: integer
          format: int64
          description: Unix timestamp отправки

paths:
  /balances:
    get:
      tags: [System]
      summary: Баланс мерчанта
      description: Доступные и замороженные средства по валютам (типы `in` / `out`).
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          balances:
                            type: array
                            items:
                              $ref: '#/components/schemas/CurrencyBalance'
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /dictionaries/banks:
    get:
      tags: [Dictionaries]
      summary: Список банков
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/BankItem'
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /dictionaries/currencies:
    get:
      tags: [Dictionaries]
      summary: Список валют
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          type: string
                          example: RUB
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /dictionaries/commissions:
    get:
      tags: [Dictionaries]
      summary: Комиссии и лимиты
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/CommissionItem'
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /payin:
    post:
      tags: [PayIn]
      summary: Создать PayIn
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayInCreateRequest'
      responses:
        '200':
          description: Заявка создана
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayInCreateResponse'
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      tags: [PayIn]
      summary: Получить PayIn по ID
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
        - name: id
          in: query
          required: true
          schema:
            type: string
          description: Внутренний ID операции (`order_id` / `id` из ответа создания)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayInGetResponse'
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /payout:
    post:
      tags: [PayOut]
      summary: Создать PayOut
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayOutCreateRequest'
      responses:
        '200':
          description: Заявка создана
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayOutCreateResponse'
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      tags: [PayOut]
      summary: Получить PayOut по ID
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/ExpiresHeader'
        - $ref: '#/components/parameters/ApiSignHeader'
        - $ref: '#/components/parameters/NonceHeader'
        - name: id
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayOutGetResponse'
        default:
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /webhook/payment:
    post:
      tags: [Webhook]
      summary: Webhook на стороне мерчанта (исходящий от Bet-Flow)
      description: |
        **Этот endpoint реализуется на вашей стороне.** Bet-Flow отправляет POST при смене статуса.

        Проверяйте `X-Signature` = HMAC-SHA256(secret, raw_body).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookPayload'
      responses:
        '200':
          description: Webhook принят и обработан
