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

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

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

paths:
  /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'

  /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'

  /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' }

  /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' }

  /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' }

  /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'

  /v1/public/rates:
    get:
      tags: [Reports]
      summary: Действующие ставки магазина
      description: |
        Ставки по методам для магазина, к которому привязан ключ. Нужны, чтобы
        показать комиссию до первого платежа смены.

        Ставка справочная: сумму комиссии берите из платежа (`fee`) - ставку
        могут изменить в середине дня, и задним числом она не применяется.

        Требуется scope `api:reports:read`.
      operationId: listRates
      responses:
        '200':
          description: ОК
          content:
            application/json:
              schema:
                type: object
                properties:
                  rates:
                    type: array
                    items:
                      type: object
                      properties:
                        method:
                          type: string
                          enum: [sbp, cards, tpay, sberpay, recurrent]
                        rate_bps:
                          type: integer
                          description: 'Сотые доли процента: 200 = 2,00%'
                          example: 200

  /v1/public/ledger:
    get:
      tags: [Reports]
      summary: Журнал движений
      description: |
        Движения по магазину ключа - то же, что в кабинете. Закрытие смены
        сводится суммой движений за период: сюда попадают и комиссии, и
        возвраты, и корректировки, которых нет в реестре платежей.

        Комиссия ссылается на свой платёж через `payment_id`.

        `format=csv` отдаёт ту же выгрузку файлом.

        Требуется scope `api:reports:read`.
      operationId: listLedger
      parameters:
        - in: query
          name: type
          schema:
            type: string
            enum: [payment, fee, refund, payout, payout_fee, settlement, adjustment]
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: cursor
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, default: 50, maximum: 200 }
        - in: query
          name: format
          schema: { type: string, enum: [csv] }
      responses:
        '200':
          description: ОК
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/LedgerEntry' }
                  next_cursor: { type: string }
            text/csv:
              schema: { type: string }

  /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' }

  /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

  /v1/public/subscriptions:
    get:
      tags: [Subscriptions]
      summary: Список подписок
      description: Все подписки мерчанта, к которому привязан API-ключ.
      operationId: listSubscriptions
      responses:
        '200':
          description: Список
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/Subscription' }
    post:
      tags: [Subscriptions]
      summary: Создать подписку
      description: |
        Создаёт рекуррентную подписку и первичный (bind) платёж. Магазин берётся
        из API-ключа. Списания идут в валюте подписки (RUB) через bank131: первый
        платёж привязывает карту (`payment_options.recurrent=true`), банк выдаёт
        rebill-токен, далее платформа сама списывает по расписанию
        (`period`×`interval_count`).

        **Важно:** в ответе есть `pay_url` - отправьте на него покупателя, чтобы
        он привязал карту в платёжном виджете. До успешной привязки подписка в
        статусе `pending` и списаний не делает. Маршрут (терминал/проект bank131)
        фиксируется при создании: и привязка, и все списания идут через него.
      operationId: createSubscription
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateSubscriptionInput' }
      responses:
        '201':
          description: Подписка создана
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription: { $ref: '#/components/schemas/Subscription' }
                  initial_payment: { $ref: '#/components/schemas/Payment' }
                  pay_url:
                    type: string
                    description: Ссылка для привязки карты покупателем.
                    example: https://zergpay.com/pay/4f0e...
        '422':
          description: Не настроен recurrent-маршрут на магазине
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/public/subscriptions/{id}:
    get:
      tags: [Subscriptions]
      summary: Получить подписку
      description: Возвращает одну подписку мерчанта по id.
      operationId: getSubscription
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Подписка
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Subscription' }
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/public/subscriptions/{id}/schedule:
    get:
      tags: [Subscriptions]
      summary: График списаний подписки
      description: |
        История проведённых платежей подписки (первичный bind + плановые списания)
        и проекция ближайших 12 будущих дат списания (для active/past_due, по
        period×interval_count от next_charge_at).
      operationId: getSubscriptionSchedule
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: График
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscription_id: { type: string, format: uuid }
                  status: { type: string, enum: [pending, active, past_due, paused, cancelled] }
                  amount: { type: integer, format: int64 }
                  currency: { type: string, example: RUB }
                  period: { type: string, enum: [day, week, month, year] }
                  interval_count: { type: integer }
                  next_charge_at: { type: string, format: date-time }
                  charges:
                    type: array
                    description: Проведённые платежи/попытки списаний (по возрастанию даты).
                    items:
                      type: object
                      properties:
                        payment_id: { type: string, format: uuid }
                        kind: { type: string, example: subscription_charge }
                        amount: { type: integer, format: int64 }
                        currency: { type: string, example: RUB }
                        status: { type: string, example: succeeded }
                        created_at: { type: string, format: date-time }
                        captured_at: { type: string, format: date-time }
                  upcoming:
                    type: array
                    description: Ближайшие плановые даты списания (проекция).
                    items: { type: string, format: date-time }
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/public/subscriptions/{id}/cancel:
    post:
      tags: [Subscriptions]
      summary: Отменить подписку
      description: Останавливает будущие списания. Уже проведённые платежи не трогает.
      operationId: cancelSubscription
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, example: по запросу клиента }
      responses:
        '200':
          description: Отменена
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: cancelled }
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/public/tokens:
    get:
      tags: [Tokens]
      summary: Список токенов
      description: Сохранённые карты магазина. Фильтр `?customer_ref=` по клиенту.
      operationId: listCards
      parameters:
        - in: query
          name: customer_ref
          schema: { type: string }
      responses:
        '200':
          description: Список
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/Card' }
    post:
      tags: [Tokens]
      summary: Привязать способ оплаты (рекуррентный токен)
      description: |
        Привязка способа оплаты. Поле `method`: `cards` (по умолчанию), `sbp` или `tpay`.
        Для карты привязка проходит верификацией 1 ₽ без списания (с 3DS). Для СБП и T-Pay
        привязка идёт вместе с первым списанием: укажите `amount`, он спишется и сразу
        выпустит токен. В ответе `pay_url`, отправьте на него плательщика для подтверждения.
        После успеха токен переходит в `active`, по нему можно делать MIT-списания.
        Требует scope `api:recurrent:bind`.
      operationId: tokenizeCard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_ref]
              properties:
                customer_ref: { type: string, example: user_42 }
                method:
                  type: string
                  enum: [cards, sbp, tpay]
                  default: cards
                  description: Метод привязки/списания токена.
                amount:
                  type: integer
                  format: int64
                  description: Сумма первого списания (копейки). Обязательна для sbp/tpay; для cards игнорируется (верификация 1 ₽).
                  example: 199000
                customer_email: { type: string, example: user@example.ru }
      responses:
        '201':
          description: Карта создана
          content:
            application/json:
              schema:
                type: object
                properties:
                  card: { $ref: '#/components/schemas/Card' }
                  pay_url:
                    type: string
                    example: https://zergpay.com/pay/4f0e...
        '403':
          description: Нет scope api:recurrent:bind
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/public/tokens/{id}/charge:
    post:
      tags: [Tokens]
      summary: Списать по токену
      description: |
        MIT-списание по сохранённому токену. Карта должна быть `active`. Требует
        scope `api:recurrent:charge`. Возвращает платёж (succeeded/pending/failed).
      operationId: chargeCard
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: integer, format: int64, minimum: 1, example: 199000 }
                order_id: { type: string, example: order-7781 }
                description: { type: string, example: Pro plan }
      responses:
        '201':
          description: Списание создано
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Payment' }
        '409':
          description: Карта не active (pending/cancelled)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/public/tokens/{id}/cancel:
    post:
      tags: [Tokens]
      summary: Отвязать токен
      description: Деактивирует токен - новые списания невозможны. Проведённые платежи не трогает.
      operationId: cancelCard
      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'

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-PSP-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://zergpay.com/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 }

    LedgerEntry:
      type: object
      properties:
        id: { type: string, format: uuid }
        type:
          type: string
          enum: [payment, fee, refund, payout, payout_fee, settlement, adjustment]
        amount:
          type: integer
          format: int64
          description: 'Со знаком: поступление - плюс, удержание - минус.'
          example: -20
        currency: { type: string, example: RUB }
        description: { type: string, example: 'Комиссия psp 2% - платёж #ord_20260814190335' }
        payment_id:
          type: string
          format: uuid
          nullable: true
          description: Заполнено, если движение относится к платежу (в том числе строка комиссии).
        payout_id:
          type: string
          format: uuid
          nullable: true
        created_at: { type: string, format: date-time }

    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: Сбер }
        fee:
          type: integer
          format: int64
          description: |
            Комиссия платформы по этому платежу, минорные единицы. Приходит
            только у `succeeded` - до успеха комиссия не удержана. Ноль означает,
            что не удержано ничего (тестовый платёж или нулевая ставка).
          example: 20
        fee_rate_bps:
          type: integer
          description: |
            Действующая ставка в сотых долях процента: 200 = 2,00%. Уже с учётом
            всех уровней переопределения.
          example: 200
        net:
          type: integer
          format: int64
          description: К зачислению - `amount` минус `fee`.
          example: 980
        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 }

    CreateSubscriptionInput:
      type: object
      required: [amount, customer_ref]
      properties:
        amount:
          type: integer
          format: int64
          minimum: 1
          description: Сумма списания в минорных единицах (копейки), валюта RUB.
          example: 49900
        customer_ref:
          type: string
          description: Ваш идентификатор клиента (для сопоставления на вашей стороне).
          example: user-42
        customer_email: { type: string, example: client@example.com }
        customer_phone: { type: string, example: '+79991234567' }
        plan_name: { type: string, example: pro-monthly }
        method:
          type: string
          enum: [cards, sbp, tpay]
          default: cards
          description: >-
            Метод рекуррента. cards - карточный. sbp - рекуррент по СБП. tpay - T-Pay
            (первый платёж списывается сразу, выдаёт токен). Пробный период
            (trial_days) доступен для cards и sbp; для tpay - нет.
        period:
          type: string
          enum: [day, week, month, year]
          default: month
        interval_count:
          type: integer
          minimum: 1
          default: 1
          description: Списывать каждые N period (напр. period=month, interval_count=3 → раз в квартал).
        trial_days:
          type: integer
          minimum: 0
          default: 0
          description: >-
            Пробный период в днях перед первым списанием полной суммы (method=cards
            и sbp). Привязка без списания (карта - верификация 1 ₽, СБП - 1 ₽ binding);
            плановое списание полной суммы откладывается на now + trial_days. Для
            method=tpay недоступен. 0 - без пробного периода.

    Subscription:
      type: object
      properties:
        id: { type: string, format: uuid }
        merchant_id: { type: string, format: uuid }
        shop_id: { type: string, format: uuid }
        customer_ref: { type: string }
        plan_name: { type: string }
        amount: { type: integer, format: int64 }
        currency: { type: string, example: RUB }
        period:
          type: string
          enum: [day, week, month, year]
        interval_count: { type: integer }
        trial_days: { type: integer }
        status:
          type: string
          enum: [pending, active, past_due, paused, cancelled]
          description: |
            `pending` - карта ещё не привязана (отправьте клиента на pay_url);
            `active` - привязана, списания идут по расписанию;
            `past_due` - последнее списание не прошло (идут ретраи);
            `cancelled` - остановлена.
        next_charge_at: { type: string, format: date-time }
        last_charge_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    Card:
      type: object
      description: Сохранённая карта (токенизация). PAN/CVV не хранятся - только токен и маска.
      properties:
        id: { type: string, format: uuid }
        merchant_id: { type: string, format: uuid }
        shop_id: { type: string, format: uuid }
        customer_ref: { type: string }
        method:
          type: string
          enum: [cards, sbp, tpay]
          description: Метод привязки/списания токена.
        status:
          type: string
          enum: [pending, active, cancelled]
          description: |
            `pending` - карта ещё не привязана (отправьте клиента на pay_url);
            `active` - токен выпущен, можно делать charge;
            `cancelled` - токен отозван.
        card_brand: { type: string, example: Visa }
        card_mask: { type: string, example: '4242' }
        card_holder: { type: string }
        last_charged_at: { type: string, format: date-time }
        created_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' }
