openapi: 3.0.3
info:
  title: freefin Public API
  version: "2026-05-06"
  description: |
    Публичное REST-API freefin. Авторизация по Bearer-ключу терминала
    (`sk_test_…` для симулятора, `sk_live_…` для боевого банка). Все ответы и
    тела запросов — JSON в UTF-8. Денежные суммы — целое число в минорных
    единицах валюты (копейки для RUB).

    Дополнительно: опциональная HMAC-подпись запроса (`X-Freefin-Signature`),
    идемпотентность (`Idempotency-Key`), per-merchant rate-limit, webhook'и
    с подписью HMAC-SHA256.
  contact:
    name: freefin support
    email: support@freefin.ru
    url: https://freefin.ru/contacts
  license:
    name: Proprietary
servers:
  - url: https://api.freefin.ru
    description: Production
  - url: http://localhost:8080
    description: Local development
security:
  - BearerKey: []

tags:
  - name: Payments
    description: Создание, статус, отмена, возврат платежей
  - name: Payouts
    description: Исходящие выплаты по СБП
  - name: Banks
    description: Справочник участников НСПК
  - name: Events
    description: Realtime-стрим событий (SSE)

paths:
  /api/v1/public/payments:
    post:
      tags: [Payments]
      summary: Создать платёж
      description: |
        Возвращает объект платежа и `payment_url`, на который надо отправить
        покупателя. Терминал берётся из API-ключа.
      operationId: createPayment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/Signature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentInput'
            examples:
              sbp:
                summary: СБП на 1500 руб
                value:
                  amount: 150000
                  currency: RUB
                  method: sbp
                  order_id: ORDER-1042
                  customer_id: user-7821
                  return_url: https://example.com/order/1042/done
              multiform:
                summary: Покупатель сам выберет метод
                value:
                  amount: 150000
                  order_id: ORDER-1043
                  description: Подписка Pro · 1 месяц
      responses:
        '201':
          description: Платёж создан
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatePaymentResponse'
        '200':
          description: Idempotent replay (тот же ключ Idempotency-Key)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatePaymentResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Конфликт идемпотентности (тот же ключ, другое тело)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api/v1/public/payments/{id}:
    get:
      tags: [Payments]
      summary: Получить платёж
      description: |
        Возвращает полный объект платежа с актуальным статусом, списком
        возвратов и customer-инфо.
      operationId: getPayment
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: ОК
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Payment' }
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/public/payments/{id}/cancel:
    post:
      tags: [Payments]
      summary: Отменить платёж
      description: Отменяет платёж в статусе `pending`. Шлёт webhook `payment.cancelled`.
      operationId: cancelPayment
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Отменён
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: cancelled }
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Платёж не в статусе pending
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/public/payments/{id}/refund:
    post:
      tags: [Payments]
      summary: Создать возврат
      description: |
        Полный или частичный возврат на платёж в `succeeded`. Сумма всех
        успешных возвратов не может превысить amount платежа. На каждый
        возврат прилетает webhook `payment.refunded`.
      operationId: refundPayment
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount:
                  type: integer
                  format: int64
                  description: Минорные единицы. 0 или отсутствует → возврат остатка.
                  example: 50000
                reason:
                  type: string
                  example: частичный возврат по запросу клиента
      responses:
        '201':
          description: Возврат создан
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Refund' }
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Платёж не в succeeded или уже полностью возвращён
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422':
          description: Сумма больше доступного остатка
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/public/payouts:
    post:
      tags: [Payouts]
      summary: Создать выплату по СБП
      operationId: createPayout
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreatePayoutInput' }
      responses:
        '201':
          description: Выплата создана
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreatePayoutResponse' }
        '200':
          description: Idempotent replay
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreatePayoutResponse' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          description: Маршрут не настроен или метод не поддерживается банком
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /api/v1/public/payouts/{id}:
    get:
      tags: [Payouts]
      summary: Получить выплату
      operationId: getPayout
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: ОК
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Payout' }
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/public/banks/sbp:
    get:
      tags: [Banks]
      summary: Справочник банков СБП
      description: Топ-30 участников НСПК с member_id для подстановки в `bank_id` /payouts.
      operationId: listSBPBanks
      responses:
        '200':
          description: ОК
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/SBPBank' }

  /api/v1/public/events:
    get:
      tags: [Events]
      summary: Server-Sent Events стрим
      description: |
        Long-lived SSE-стрим событий мерчанта в реальном времени.
        Формат — стандартный SSE: строки `event: <kind>` и `data: <json>`,
        разделённые пустой строкой. Heartbeat (`: ping`) каждые 25 секунд.
      operationId: streamEvents
      responses:
        '200':
          description: SSE-стрим
          content:
            text/event-stream:
              schema:
                type: string
              example: |
                : connected

                event: payment.status_changed
                data: {"kind":"payment.status_changed","payload":{"id":"…","status":"succeeded"}}

                : ping

components:
  securitySchemes:
    BearerKey:
      type: http
      scheme: bearer
      bearerFormat: sk_test_<6>_<secret> | sk_live_<6>_<secret>
      description: |
        API-ключ терминала. `sk_test_…` ходит в симулятор, `sk_live_…`
        в боевой банк. Получить — в ЛК `/cabinet/integration` → карточка
        терминала.

  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      schema:
        type: string
        maxLength: 64
      description: |
        Защита от дублей. При повторном запросе с тем же ключом и тем же телом
        вернём ранее созданный объект (HTTP 200 вместо 201). При повторе с тем
        же ключом, но другим телом — 409 idempotent_conflict.
      example: 7f2a-pay-1042

    Signature:
      in: header
      name: X-Freefin-Signature
      schema:
        type: string
        pattern: '^sha256=[a-f0-9]{64}$'
      description: |
        HMAC-SHA256 от raw body запроса, секрет — `signing_secret` терминала.
        Обязательно, если на терминале включён флаг "Требовать подпись".

  schemas:
    CreatePaymentInput:
      type: object
      required: [amount]
      properties:
        amount:
          type: integer
          format: int64
          minimum: 1
          description: Минорные единицы (копейки для RUB)
          example: 150000
        currency:
          type: string
          default: RUB
          example: RUB
        method:
          type: string
          enum: [sbp, cards, tpay, sberpay, recurrent, any]
          default: any
        order_id: { type: string, example: ORDER-1042 }
        customer_id: { type: string, example: user-7821 }
        return_url: { type: string, format: uri, example: 'https://example.com/order/1042/done' }
        description: { type: string, example: 'Подписка Pro · 1 месяц' }
        webhook_url:
          type: string
          format: uri
          description: Per-payment URL для webhook'а; перебивает endpoint из ЛК.
        metadata:
          type: object
          additionalProperties: { type: string }
          description: Произвольные пары ключ-значение (строки).

    CreatePaymentResponse:
      type: object
      required: [payment, payment_url, mode]
      properties:
        payment: { $ref: '#/components/schemas/Payment' }
        payment_url:
          type: string
          format: uri
          example: https://pay.freefin.ru/pay/5a331a39-…
        mode:
          type: string
          enum: [test, live]
        idempotent:
          type: boolean
          description: true, если это replay по Idempotency-Key (не было создания).
        simulator:
          type: string
          description: Только в test-режиме. Запланированный исход симулятора.
        bank: { type: string, description: Только в live-режиме. Имя банка-эквайера. }
        bank_ref: { type: string, description: Только в live-режиме. ID операции в банке. }
        failure_code:
          type: string
          description: При синхронном отказе. См. раздел "Коды отказов" в доке.
        failure_message: { type: string }

    Payment:
      type: object
      properties:
        id: { type: string, format: uuid }
        merchant_id: { type: string, format: uuid }
        terminal_id: { type: string, format: uuid }
        terminal_name: { type: string }
        order_id: { type: string }
        amount: { type: integer, format: int64 }
        amount_refunded: { type: integer, format: int64 }
        amount_usdt: { type: number, format: double }
        currency: { type: string }
        method:
          type: string
          enum: [sbp, cards, tpay, sberpay, recurrent, any]
        status:
          type: string
          enum: [pending, processing, succeeded, failed, expired, cancelled, refunded]
        rrn: { type: string }
        bank_name: { type: string }
        return_url: { type: string }
        created_at: { type: string, format: date-time }
        captured_at: { type: string, format: date-time }
        finalized_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        processing_until: { type: string, format: date-time }
        metadata:
          type: object
          additionalProperties: { type: string }
        customer_card_brand: { type: string, example: visa }
        customer_card_mask: { type: string, example: '411111******1111' }
        customer_card_holder: { type: string }
        customer_phone_mask: { type: string, example: '+7•••••••12-34' }
        customer_payer_bank: { type: string, example: Сбер }
        refunds:
          type: array
          items: { $ref: '#/components/schemas/Refund' }

    CreatePayoutInput:
      type: object
      required: [amount, phone, bank_id]
      properties:
        amount:
          type: integer
          format: int64
          minimum: 1
          description: Минорные единицы (копейки)
          example: 250000
        currency:
          type: string
          default: RUB
        method:
          type: string
          enum: [payouts_sbp]
          default: payouts_sbp
        phone:
          type: string
          pattern: '^\+7\d{10}$'
          example: '+79001234567'
        bank_id:
          type: string
          pattern: '^\d{12}$'
          description: member_id банка-получателя в НСПК. См. GET /banks/sbp.
          example: '100000000111'
        full_name:
          type: string
          example: Иванов Иван Иванович

    CreatePayoutResponse:
      type: object
      properties:
        payout: { $ref: '#/components/schemas/Payout' }
        mode: { type: string, enum: [test, live] }
        idempotent: { type: boolean }
        simulator: { type: string }
        bank: { type: string }
        bank_ref: { type: string }
        failure_reason: { type: string }

    Payout:
      type: object
      properties:
        id: { type: string, format: uuid }
        merchant_id: { type: string, format: uuid }
        terminal_id: { type: string, format: uuid }
        amount: { type: integer, format: int64 }
        currency: { type: string }
        method: { type: string }
        status:
          type: string
          enum: [pending, succeeded, failed]
        fail_reason: { type: string }
        recipient:
          type: object
          properties:
            phone: { type: string }
            bank_id: { type: string }
            full_name: { type: string }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }

    Refund:
      type: object
      properties:
        id: { type: string, format: uuid }
        payment_id: { type: string, format: uuid }
        merchant_id: { type: string, format: uuid }
        amount: { type: integer, format: int64 }
        reason: { type: string }
        status:
          type: string
          enum: [pending, succeeded, failed]
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }

    SBPBank:
      type: object
      properties:
        member_id:
          type: string
          pattern: '^\d{12}$'
          example: '100000000111'
        name:
          type: string
          example: Сбербанк
        name_en:
          type: string
          example: Sberbank

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: not_pending
            message:
              type: string
              example: 'отменить можно только платёж в статусе pending'

  responses:
    BadRequest:
      description: Невалидный запрос (bad_request, signature_required, invalid_signature)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Авторизация не прошла
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Доступ запрещён (live_not_enabled, ip_not_allowed, aml_blocked, forbidden_purpose)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Объект не найден
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Превышен лимит запросов мерчанта
      headers:
        Retry-After:
          schema: { type: integer, example: 1 }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
