openapi: 3.1.0
info:
  title: Payment Platform Client API
  version: 1.2.0
  description: Provider-neutral API for hosted, direct and recurring payments.
servers:
  - url: https://sandbox-api.example.com/api/v1
    description: Sandbox
  - url: https://api.example.com/api/v1
    description: Production after compliance approval
security:
  - HMAC: []
tags:
  - name: Health
  - name: Payments
  - name: Subscriptions
  - name: Card verifications
  - name: Payouts
paths:
  /health:
    get:
      tags: [Health]
      operationId: health
      security: []
      responses:
        '200':
          description: API process is available; no internal dependency details are returned
          content:
            application/json:
              schema:
                type: object
                required: [status, service, version, environment]
                properties:
                  status: {type: string, const: ok}
                  service: {type: string}
                  version: {type: string}
                  environment: {type: string, enum: [sandbox, production]}
  /payment-sessions:
    post:
      tags: [Payments]
      operationId: createPaymentSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreatePayment'}
      responses:
        '201':
          description: Hosted session created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Payment'}
        default: {$ref: '#/components/responses/Error'}
  /payment-sessions/{id}:
    get:
      tags: [Payments]
      operationId: getPaymentSession
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Safe normalized state
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Payment'}
        default: {$ref: '#/components/responses/Error'}
  /payments/direct:
    post:
      tags: [Payments]
      operationId: createDirectPayment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/DirectPayment'}
      responses:
        '201':
          description: Payment accepted for processing
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Payment'}
        default: {$ref: '#/components/responses/Error'}
  /payments/{id}:
    get:
      tags: [Payments]
      operationId: getPayment
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Safe normalized payment
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Payment'}
        default: {$ref: '#/components/responses/Error'}
  /payments/{id}/confirm:
    post:
      tags: [Payments]
      operationId: confirmPayment
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                value: {type: string, maxLength: 128}
      responses:
        '200':
          description: Confirm operation queued; poll the payment or consume webhooks for the provider result
          content: {application/json: {schema: {$ref: '#/components/schemas/Payment'}}}
        default: {$ref: '#/components/responses/Error'}
  /payments/{id}/capture:
    post:
      tags: [Payments]
      operationId: capturePayment
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Amount'}
      responses:
        '200':
          description: Capture operation queued; poll the payment or consume webhooks for the provider result
          content: {application/json: {schema: {$ref: '#/components/schemas/Payment'}}}
        default: {$ref: '#/components/responses/Error'}
  /payments/{id}/cancel:
    post:
      tags: [Payments]
      operationId: cancelPayment
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Cancel operation queued; poll the payment or consume webhooks for the provider result
          content: {application/json: {schema: {$ref: '#/components/schemas/Payment'}}}
        default: {$ref: '#/components/responses/Error'}
  /payments/{id}/refunds:
    get:
      tags: [Payments]
      operationId: listRefunds
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Refund operations
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: {$ref: '#/components/schemas/Refund'}
        default: {$ref: '#/components/responses/Error'}
    post:
      tags: [Payments]
      operationId: createRefund
      description: |
        **The first provider has no refund method**, so on it this endpoint always answers
        `422 capability_not_supported` and the statuses `refunded` / `partially_refunded`
        never occur. That is not a defect in this platform: `api_doc_ru.md` describes no
        refund at all, and an endpoint that appeared to work would be worse than one that
        refuses.

        To return money to a payer today, send a payout to their card — see `/payouts`. It
        is a different operation with different limits, not a refund wearing another name,
        and it settles as a transfer rather than as a reversal of the original charge.

        The path stays here because refund is a platform capability gated per channel: a
        second provider that supports it turns this endpoint on without a contract change.
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Amount'}
      responses:
        '200':
          description: Refund operation queued when supported; the bundled first provider currently returns capability_not_supported before queueing
          content: {application/json: {schema: {$ref: '#/components/schemas/Payment'}}}
        default: {$ref: '#/components/responses/Error'}
  /subscriptions:
    post:
      tags: [Subscriptions]
      operationId: createSubscription
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateSubscription'}
      responses:
        '201':
          description: Subscription created from an existing mandate
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Subscription'}
        default: {$ref: '#/components/responses/Error'}
  /subscriptions/{id}:
    get:
      tags: [Subscriptions]
      operationId: getSubscription
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Subscription
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Subscription'}
        default: {$ref: '#/components/responses/Error'}
  /subscriptions/{id}/pause:
    post:
      tags: [Subscriptions]
      operationId: pauseSubscription
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': {$ref: '#/components/responses/Subscription'}
        default: {$ref: '#/components/responses/Error'}
  /subscriptions/{id}/resume:
    post:
      tags: [Subscriptions]
      operationId: resumeSubscription
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': {$ref: '#/components/responses/Subscription'}
        default: {$ref: '#/components/responses/Error'}
  /subscriptions/{id}/cancel:
    post:
      tags: [Subscriptions]
      operationId: cancelSubscription
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': {$ref: '#/components/responses/Subscription'}
        default: {$ref: '#/components/responses/Error'}
  /payment-channels:
    get:
      operationId: listPaymentChannels
      responses:
        '200':
          description: Provider-neutral capabilities assigned to this key
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: {type: array, items: {$ref: '#/components/schemas/PaymentChannel'}}
        default: {$ref: '#/components/responses/Error'}
  /cards/information:
    get:
      operationId: getCardInformation
      summary: Issuer metadata for a card BIN
      description: |
        Resolves the issuer behind a card BIN. Answers from the platform BIN base first and
        falls back to the payment provider only for BINs the active BIN version does not
        cover; `source` says which one answered.

        Accepts a 6- or 8-digit BIN only. A full card number is rejected with `invalid_bin`:
        the value travels in the query string, and query strings reach access logs, proxy
        logs and browser history. Remember that the query string is part of the signed
        canonical path, so `?bin=` must be included when computing X-Signature.
      parameters:
        - name: bin
          in: query
          required: true
          description: First 6 or 8 digits of the card number.
          schema: {type: string, pattern: '^([0-9]{6}|[0-9]{8})$'}
      responses:
        '200':
          description: Issuer metadata
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CardInformation'}
        '404':
          description: Neither the BIN base nor the provider knows this BIN
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorEnvelope'}
        default: {$ref: '#/components/responses/Error'}
  /card-verifications:
    post:
      tags: [Card verifications]
      operationId: createCardVerification
      summary: Verify that the payer controls a card
      description: |
        Starts a LookUp: the provider briefly blocks a sum on the card, the payer reads the
        code off their bank statement or SMS, and the provider reverses the block itself.

        There is no `amount_minor` — the provider chooses what it blocks, so the platform has
        nothing to bill and nothing to reserve against your limits. A verification is
        therefore not a payment: it never appears in your payment list, your exports or your
        dashboard totals, has no `payment_id`, and cannot be captured, refunded or cancelled.
        `currency` is still required, because the provider blocks real money in it.

        The call is asynchronous. Poll `GET /card-verifications/{id}` (or wait for
        `status: code_sent`) before submitting a code — see the `verify` endpoint.

        Requires a provider with the `lookup` capability; otherwise `422
        capability_not_supported`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateCardVerification'}
      responses:
        '201':
          description: Verification accepted; the LookUp is queued
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CardVerification'}
        default: {$ref: '#/components/responses/Error'}
  /card-verifications/{id}:
    get:
      tags: [Card verifications]
      operationId: getCardVerification
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Current verification state
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CardVerification'}
        default: {$ref: '#/components/responses/Error'}
  /card-verifications/{id}/verify:
    post:
      tags: [Card verifications]
      operationId: verifyCardVerification
      summary: Submit the code the payer read back
      description: |
        Confirms a LookUp with the code the payer found on their statement.

        Only accepted while the verification is in `code_sent`; anything else is `422
        verification_not_awaiting_code`. That includes the window right after creation, before
        the provider has sent a code — submitting then would address a LookUp that does not
        exist yet and consume one of the provider's own attempts to be told so.

        The provider limits how many codes it will accept. Once that limit is reached it
        answers with a final `failed` and `decline_code: authentication_failed`;
        `verify_attempts` on the response says how many were submitted.
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/VerifyCardVerification'}
      responses:
        '200':
          description: Code accepted for checking; the result is asynchronous
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CardVerification'}
        default: {$ref: '#/components/responses/Error'}
  /payouts:
    post:
      tags: [Payouts]
      operationId: createPayout
      summary: Send money to a card or wallet
      description: |
        Credits a card, or an Apple/Google/Samsung Pay instrument, with money moving out of
        your account.

        A payout is not a payment and not a refund. It lives in its own resource: it never
        appears in `GET /payments`, your payment exports or your dashboard totals — where it
        would otherwise be added to money *collected* rather than subtracted from it — and
        `/payments/{id}/capture|refund|cancel` will not accept a payout id. It also draws on
        its own limit: your payout allowance is separate from your collection allowance, and
        exceeding it returns `422 limit_exceeded` without touching the other.

        **This is how you return money to a payer.** The first provider has no refund method
        at all, so `POST /payments/{id}/refunds` answers `422 capability_not_supported`. To
        give money back, send a payout to the payer's card — most conveniently with
        `payout_method.saved_card_id`, if the original payment stored one.

        The call is asynchronous. Poll `GET /payouts/{id}` or subscribe to `payout.*`
        webhooks. A payout can be 3DS-challenged like a charge: if it reaches
        `requires_action`, either send the payer to `next_action.url`, or pass the OTP they
        read back to `POST /payouts/{id}/confirm`.

        Requires a provider with the `debit` capability; otherwise `422
        capability_not_supported`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreatePayout'}
      responses:
        '201':
          description: Payout accepted; the transfer is queued
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Payout'}
        default: {$ref: '#/components/responses/Error'}
  /payouts/{id}:
    get:
      tags: [Payouts]
      operationId: getPayout
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Current payout state
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Payout'}
        default: {$ref: '#/components/responses/Error'}
  /payouts/{id}/confirm:
    post:
      tags: [Payouts]
      operationId: confirmPayout
      summary: Answer a 3DS challenge on a payout
      description: |
        Submits the one-time code the payer received for a challenged transfer.

        Only accepted while the payout is in `requires_action`; anything else is `409
        invalid_payout_state`. A payout that finished on its own never needs this call.
      parameters:
        - $ref: '#/components/parameters/ResourceID'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ConfirmPayout'}
      responses:
        '200':
          description: Code accepted for checking; the result is asynchronous
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Payout'}
        default: {$ref: '#/components/responses/Error'}
components:
  securitySchemes:
    HMAC:
      type: apiKey
      in: header
      name: X-Access-Key
      description: Requires X-Timestamp, X-Nonce and X-Signature. Mutations also require Idempotency-Key.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: {type: string, minLength: 8, maxLength: 200}
    ResourceID:
      name: id
      in: path
      required: true
      schema: {type: string, format: uuid}
  responses:
    Payment:
      description: Safe normalized payment
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Payment'}
    Subscription:
      description: Subscription
      content:
        application/json:
          schema: {$ref: '#/components/schemas/Subscription'}
    Error:
      description: Error envelope
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorEnvelope'}
  schemas:
    Amount:
      type: object
      additionalProperties: false
      properties:
        amount_minor: {type: integer, format: int64, minimum: 1}
    Customer:
      type: object
      additionalProperties: false
      description: >
        Card-holder data. For a Mastercard PAN email is required. For a Visa PAN
        first_name, last_name, email, billing_address.country and
        billing_address.line1 are required. country is accepted as a current
        ISO 3166-1 alpha-2 code and converted to PSP numeric-3 by the backend.
      properties:
        first_name: {type: string, maxLength: 100}
        last_name: {type: string, maxLength: 100}
        email: {type: string, format: email}
        phone: {type: string, maxLength: 32}
        billing_address:
          type: object
          additionalProperties: false
          properties:
            country:
              type: string
              pattern: '^[A-Za-z]{2}$'
              description: Current assigned ISO 3166-1 alpha-2 country code.
            postal_code: {type: string, maxLength: 32}
            region: {type: string, maxLength: 100}
            city: {type: string, maxLength: 100}
            line1: {type: string, maxLength: 200}
            line2: {type: [string, 'null'], maxLength: 200}
            line3:
              type: [string, 'null']
              maxLength: 200
              description: >
                Third address line, forwarded to the PSP as address_line_3. Optional
                everywhere; no card scheme requires it.
    DeviceSnapshot:
      type: object
      additionalProperties: false
      description: Neutral browser snapshot. The backend derives authoritative accept_header
        and user_agent values from HTTP request headers and merges payer IP only into the
        provider DTO. Forwarded IP headers are accepted only from socket peers in the
        configured trusted-proxy CIDRs.
      properties:
        accept_header: {type: string, maxLength: 512}
        color_depth: {type: number}
        screen_width: {type: number}
        screen_height: {type: number}
        viewport_width: {type: number}
        viewport_height: {type: number}
        device_pixel_ratio: {type: number}
        language: {type: string, maxLength: 32}
        languages:
          type: array
          maxItems: 10
          items: {type: string, maxLength: 32}
        timezone: {type: string, maxLength: 128}
        utc_offset: {type: number}
        user_agent: {type: string, maxLength: 512}
        platform: {type: string, maxLength: 128}
        javascript_enabled: {type: boolean}
        touch_enabled: {type: boolean}
        max_touch_points: {type: number}
        cookie_enabled: {type: boolean}
        online: {type: boolean}
        referrer_origin: {type: string, maxLength: 128}
        session_started_at: {type: string, maxLength: 128}
        client_timestamp: {type: string, maxLength: 128}
    SubscriptionTerms:
      type: object
      additionalProperties: false
      required: [interval_unit, interval_count, first_charge_at, disclosure_version]
      properties:
        interval_unit: {type: string, enum: [day, week, month, year]}
        interval_count: {type: integer, minimum: 1}
        first_charge_at: {type: string, format: date-time}
        disclosure_version: {type: string, minLength: 1}
        consent: {type: boolean, description: Checkout collects the payer's separate consent again.}
    CreatePayment:
      type: object
      additionalProperties: false
      required: [order_id, amount_minor, currency]
      properties:
        order_id: {type: string, minLength: 1, maxLength: 200}
        amount_minor: {type: integer, format: int64, minimum: 1}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        description: {type: string, maxLength: 500}
        customer: {$ref: '#/components/schemas/Customer'}
        customer_reference:
          type: string
          maxLength: 128
          description: >
            Opaque, stable identifier of the payer in the merchant's own system. It is
            the account a stored card is bound to: the same reference on a later payment
            offers that customer their saved cards on the hosted page, and scopes
            GET /cabinet/v1/payment-method-tokens.
        return_url: {type: string, format: uri}
        locale:
          type: string
          enum: [ru, en, ua]
          description: >
            Forwarded to the provider as the envelope `lang`, which chooses the language of
            the pages the provider renders itself - its hosted checkout and the issuer's 3DS
            step. Defaults to `ru`.

            It does not translate the platform's own hosted payment page, which is
            English-only. If your payers need the platform page in another language, that is
            a separate piece of work and this field will not produce it.
        expires_in_seconds: {type: integer, minimum: 300, maximum: 7200}
        save_payment_method:
          type: boolean
          description: >
            Store the card for later payments by this customer. Requires
            customer_reference; without one there would be no account to bind the card
            to and it could never be listed, reused or deleted, so the request is
            rejected with customer_reference_required.
        subscription: {oneOf: [{$ref: '#/components/schemas/SubscriptionTerms'}, {type: 'null'}]}
        metadata: {type: object, maxProperties: 20}
    DirectPayment:
      allOf:
        - $ref: '#/components/schemas/CreatePayment'
        - type: object
          description: >
            For card payments, customer fields are conditionally required by the
            PAN scheme: Mastercard requires email; Visa requires first_name,
            last_name, email, billing_address.country and billing_address.line1.
          required: [payment_method]
          properties:
            operation_mode: {type: string, enum: [charge, authorize], default: charge}
            payment_method:
              type: object
              required: [type]
              properties:
                type: {type: string, enum: [card, apple_pay, google_pay, samsung_pay, mobile]}
                card:
                  type: object
                  properties:
                    number: {type: string, minLength: 12, maxLength: 19, writeOnly: true}
                    expire_month: {type: integer, minimum: 1, maximum: 12, writeOnly: true}
                    expire_year: {type: integer, writeOnly: true}
                    cvv: {type: string, minLength: 3, maxLength: 4, writeOnly: true}
                    cardholder: {type: string, maxLength: 100, writeOnly: true}
                wallet: {type: object, writeOnly: true}
                payload_mode:
                  type: string
                  enum: [token, direct]
                  default: token
                  description: Selects the documented token or direct PSP wallet method. Wallet authorize is supported when the channel capability allows it.
                mobile:
                  type: object
                  additionalProperties: false
                  required: [phone]
                  description: >
                    Required when type is mobile: the mobile account the operator debits.
                    Rejected with 422 capability_not_supported together with
                    operation_mode=authorize - the operator settles immediately and offers
                    no hold to capture later.
                  properties:
                    phone:
                      type: string
                      writeOnly: true
                      pattern: '^\+[1-9][0-9]{7,14}$'
                      description: >
                        E.164, leading plus required. Spaces, dashes and brackets are
                        stripped before validation. The plus is not assumed: a national
                        number carries no country code, and reading its first digits as one
                        would debit a different subscriber.
            device: {$ref: '#/components/schemas/DeviceSnapshot'}
    Payment:
      type: object
      required: [payment_id, status, final, created_at, updated_at]
      properties:
        payment_id: {type: string, format: uuid}
        session_id: {type: string, format: uuid}
        status:
          type: string
          enum: [created, awaiting_payment_data, processing, requires_action, authorized, succeeded, declined, failed, canceled, expired, manual_review, partially_refunded, refunded]
        final: {type: boolean}
        checkout_url: {type: string, format: uri}
        expires_at: {type: string, format: date-time}
        authorization_expires_at: {type: string, format: date-time}
        masked_card: {type: string}
        customer:
          type: object
          additionalProperties: false
          properties:
            first_name: {type: string}
            last_name: {type: string}
        device: {$ref: '#/components/schemas/DeviceSnapshot'}
        payer_ip: {type: string}
        location: {type: object}
        decline:
          type: object
          properties:
            code: {type: string}
            message: {type: string}
        next_action:
          type: object
          properties:
            type: {type: string, enum: [redirect, three_ds, otp, wait, none]}
            url: {type: string, format: uri}
        bank_reference: {$ref: '#/components/schemas/BankReference'}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    BankReference:
      type: object
      additionalProperties: false
      description: |
        What the issuing bank calls this operation, as reported by the PSP
        (api_doc_ru.md §17: `bank_payment_id`, `bank_name`).

        This is not the payment id and not the PSP's id — the bank has never heard of
        either of those. It is the only identifier that means anything in a conversation
        with the bank, which is the one situation it exists for: the PSP says money moved
        and the bank says it did not.

        Absent until the provider reports one, and for most declines that is never. Do not
        key anything on it and do not treat its absence as a problem.
      properties:
        payment_id: {type: string, description: The operation's number inside the issuing bank.}
        name: {type: string, description: The issuing bank's name as the PSP spells it.}
    PaymentChannel:
      type: object
      additionalProperties: false
      required: [id, alias, payment_methods, currencies, checkout_mode]
      properties:
        id: {type: string, format: uuid}
        alias: {type: string}
        payment_methods:
          type: array
          items: {type: string, enum: [card, apple_pay, google_pay, samsung_pay, mobile]}
        currencies:
          type: array
          items: {type: string, pattern: '^[A-Z]{3}$'}
        checkout_mode: {type: string, enum: [platform, provider_redirect]}
    CardInformation:
      type: object
      additionalProperties: false
      required: [bin, source]
      description: |
        Issuer metadata for a BIN. Only `bin` and `source` are guaranteed: neither source
        fills every column, so treat every other field as optional.
      properties:
        bin: {type: string, pattern: '^([0-9]{6}|[0-9]{8})$'}
        scheme: {type: string, description: 'Card network, e.g. visa or mastercard.'}
        type: {type: string, description: 'Card type, e.g. debit or credit.'}
        level: {type: string, description: 'Card level, e.g. traditional or platinum.'}
        issuer: {type: string, description: Issuing bank name.}
        country: {type: string, description: Issuing bank country name.}
        country_code: {type: string, pattern: '^[A-Z]{2}$'}
        issuer_website: {type: string}
        issuer_phone: {type: string}
        source:
          type: string
          enum: [bin_base, provider]
          description: |
            Which source answered. `bin_base` is the platform's own versioned BIN base and
            is authoritative - it is the same data that classifies the card at payment time.
            `provider` means the active BIN version did not cover this BIN and the payment
            provider was asked instead.
    CreateCardVerification:
      type: object
      additionalProperties: false
      required: [order_id, currency, card]
      description: |
        Mirrors what a LookUp actually carries. There is no `customer` block and no
        `cardholder`: the provider's send_lookup takes only the card, the currency and your
        references, so any cardholder data supplied here could never leave the platform — and
        holding a payer's name for a call that never sends it is retention without a purpose.
      properties:
        order_id: {type: string, minLength: 1, maxLength: 128, description: Your own reference. Unique per API key.}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        card:
          type: object
          additionalProperties: false
          required: [number, expire_month, expire_year, cvv]
          properties:
            number: {type: string, minLength: 12, maxLength: 19, writeOnly: true}
            expire_month: {type: integer, minimum: 1, maximum: 12, writeOnly: true}
            expire_year: {type: integer, writeOnly: true}
            cvv: {type: string, minLength: 3, maxLength: 4, writeOnly: true}
        description: {type: string, maxLength: 512}
        metadata: {type: object, additionalProperties: true}
    VerifyCardVerification:
      type: object
      additionalProperties: false
      required: [code]
      properties:
        code:
          type: string
          pattern: '^[0-9]{1,32}$'
          writeOnly: true
          description: |
            The LookUp code exactly as the payer read it back. Digits only. Stored encrypted
            until the provider has seen it, then discarded.
    CardVerification:
      type: object
      additionalProperties: false
      required: [verification_id, order_id, status, final, verify_attempts, created_at, updated_at]
      properties:
        verification_id: {type: string, format: uuid}
        order_id: {type: string}
        status:
          type: string
          enum: [pending, code_sent, verified, failed, manual_review]
          description: |
            `pending` — a LookUp call is queued or in flight.
            `code_sent` — the provider sent a code and is waiting for it back; submit it to
            `/verify`.
            `verified` — the payer proved control of the card. Final.
            `failed` — declined, or the provider stopped accepting codes. Final.
            `manual_review` — the provider's answer could not be read as either outcome. Not
            final and not a failure: never treat it as a bad card.
        final: {type: boolean, description: 'True only for verified and failed.'}
        card_mask: {type: string, description: 'Masked PAN, e.g. 411111******1111.'}
        decline_code: {type: string}
        client_description: {type: string, description: Neutral, payer-safe summary. Never the provider's own text.}
        next_action:
          type: object
          additionalProperties: true
          description: |
            Present while `code_sent`, with `type: lookup` — meaning the payer has to supply
            the code. No URL is involved; there is nothing to redirect to.
        verify_attempts: {type: integer, description: How many codes have been submitted so far.}
        bank_reference: {$ref: '#/components/schemas/BankReference'}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    CreatePayout:
      type: object
      additionalProperties: false
      required: [order_id, amount_minor, currency, account, payout_method]
      description: |
        There is no `operation_mode` and no `save_payment_method`: a transfer settles in one
        step, and there is nothing worth storing about a card the platform is only crediting.
      properties:
        order_id: {type: string, minLength: 1, maxLength: 128, description: Your own reference. Unique per API key.}
        amount_minor: {type: integer, format: int64, minimum: 1}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        account:
          type: string
          minLength: 1
          maxLength: 64
          description: |
            The account the transfer is drawn against, as agreed with the provider. Required —
            it is not the destination card, and it is safe to display in the cabinet.
        payout_method:
          type: object
          additionalProperties: false
          required: [type]
          description: |
            Exactly one of `card`, `saved_card_id` or `wallet`, matching `type`. Sending both
            a card and a saved card is `400 card_and_saved_card_are_exclusive`.
          properties:
            type: {type: string, enum: [card, apple_pay, google_pay, samsung_pay]}
            card:
              type: object
              additionalProperties: false
              required: [number, expire_month, expire_year]
              description: |
                The destination card. `cvv` is optional here and required nowhere: a CVV
                proves the payer is holding the card, which is worth asking when taking money
                and means nothing when giving it back.
              properties:
                number: {type: string, minLength: 12, maxLength: 19, writeOnly: true}
                expire_month: {type: integer, minimum: 1, maximum: 12, writeOnly: true}
                expire_year: {type: integer, writeOnly: true}
                cvv: {type: string, minLength: 3, maxLength: 4, writeOnly: true}
                cardholder: {type: string, maxLength: 100, writeOnly: true}
            saved_card_id:
              type: string
              format: uuid
              description: |
                A card stored earlier, addressed by the id from `GET
                /cabinet/v1/payment-method-tokens`. Must belong to you and to the same
                provider, otherwise `422 saved_card_not_found`.
            wallet: {type: object, additionalProperties: true, writeOnly: true, description: Wallet payment token, as issued by the wallet SDK.}
            payload_mode: {type: string, enum: [token, direct], default: token}
        description: {type: string, maxLength: 512}
        return_url: {type: string, format: uri, description: Where to send the payer back after a 3DS challenge.}
        customer: {$ref: '#/components/schemas/Customer'}
        device: {type: object, additionalProperties: true, description: Browser data forwarded to the issuer for 3DS.}
        metadata: {type: object, additionalProperties: true}
    ConfirmPayout:
      type: object
      additionalProperties: false
      properties:
        value:
          type: string
          writeOnly: true
          description: The one-time code the payer received. Stored encrypted until the provider has seen it, then discarded.
    Payout:
      type: object
      additionalProperties: false
      required: [payout_id, order_id, status, final, amount_minor, currency, account, payout_method, created_at, updated_at]
      properties:
        payout_id: {type: string, format: uuid}
        order_id: {type: string}
        status:
          type: string
          enum: [processing, requires_action, succeeded, declined, failed, manual_review]
          description: |
            `processing` — the transfer is queued or in flight.
            `requires_action` — the payer must pass a 3DS challenge; see `next_action` or
            `POST /payouts/{id}/confirm`.
            `succeeded` — the money has been sent. Final.
            `declined` — the issuer refused the credit. Final.
            `failed` — the transfer did not happen. Final.
            `manual_review` — the provider's answer could not be read as either outcome. Not
            final and not a failure: never treat it as money not sent, and never reissue the
            payout on the strength of it — the platform keeps polling until it resolves.

            There is no `authorized`, `captured` or `refunded`: §18 has no two-step form, and
            money that has left cannot be partially unsent.
        final: {type: boolean, description: 'True only for succeeded, declined and failed.'}
        amount_minor: {type: integer, format: int64}
        currency: {type: string}
        account: {type: string}
        payout_method: {type: string, enum: [card, apple_pay, google_pay, samsung_pay]}
        card_mask: {type: string, description: 'Masked destination PAN, e.g. 411111******1111.'}
        decline_code: {type: string}
        client_description: {type: string, description: Neutral, payer-safe summary. Never the provider's own text.}
        next_action: {type: object, additionalProperties: true, description: 'Present while requires_action; carries the 3DS URL to send the payer to.'}
        bank_reference:
          allOf: [{$ref: '#/components/schemas/BankReference'}]
          description: >
            The receiving bank's reference for the transfer. Quote it when a payer says the
            money never arrived — it is the only identifier their bank can look up.
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    Refund:
      type: object
      properties:
        id: {type: string, format: uuid}
        amount_minor: {type: integer, format: int64}
        status: {type: string}
        created_at: {type: string, format: date-time}
        completed_at: {type: string, format: date-time}
    CreateSubscription:
      type: object
      additionalProperties: false
      required: [mandate_id, amount_minor, currency, interval_unit, interval_count]
      properties:
        mandate_id: {type: string, format: uuid}
        amount_minor: {type: integer, format: int64, minimum: 1}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        interval_unit: {type: string, enum: [day, week, month, year]}
        interval_count: {type: integer, minimum: 1}
        first_run_at: {type: string, format: date-time}
    Subscription:
      type: object
      required: [id, mandate_id, amount_minor, currency, interval_unit, interval_count, status]
      properties:
        id: {type: string, format: uuid}
        mandate_id: {type: string, format: uuid}
        amount_minor: {type: integer, format: int64}
        currency: {type: string}
        interval_unit: {type: string, enum: [day, week, month, year]}
        interval_count: {type: integer}
        next_run_at: {type: string, format: date-time}
        status: {type: string, enum: [incomplete, active, past_due, paused, canceled, completed]}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id, retryable, details]
          properties:
            code: {type: string}
            message: {type: string}
            field: {oneOf: [{type: string}, {type: 'null'}]}
            request_id: {type: string, format: uuid}
            retryable: {type: boolean}
            details: {type: object}
