openapi: 3.1.0
info:
  title: PayVAN Payments API
  version: "1.5.0"
  summary: Card payments, two-step authorizations, refunds, subscriptions, customers and payouts.
  description: |
    Signed JSON API for the PayVAN payment platform.

    **Read the [integration guide](./) first.** It explains how the pieces fit
    together: request signing, idempotency, hosted checkout vs. direct API,
    3-D Secure, the amount rule for follow-up operations, and webhooks. This
    reference lists every endpoint and field.

    ## Authentication

    Every request carries four headers:

    | Header | Value |
    |---|---|
    | `X-SHOP-ID` | Your Shop ID |
    | `X-REQUEST-ID` | Unique ID of this request (UUID v4); also the idempotency key |
    | `X-REQUEST-TIMESTAMP` | Unix seconds; must be within 5 minutes of our clock |
    | `X-REQUEST-SIGNATURE` | `v1=` + lowercase hex HMAC-SHA256 of the string below |

    ```
    v1\n{METHOD}\n{PATH_WITH_QUERY}\n{SHOP_ID}\n{REQUEST_ID}\n{TIMESTAMP}\n{hex(SHA-256(BODY))}
    ```

    The signature depends on the request body and time, so the interactive
    "try it" consoles of generic OpenAPI tools can't produce it on their own.
    Use the helpers from the guide (Python, Node.js, PHP, shell) or a
    pre-request script in Postman.

    ## Conventions

    * Amounts are decimal strings in major units: `"49"` or `"49.00"`.
    * Currency is ISO 4217 and must equal your shop's currency.
    * Timestamps in responses and webhooks are RFC 3339 in UTC.
    * Field names are case-sensitive and `PascalCase`.
    * Resources: a payment is `/v1/payments/{id}`; capture, cancel and refund
      are actions on it. Reads are `GET` with filters in the query string;
      lists come as `{Data, HasMore, NextCursor}`.
    * `201`/`200` from a payment endpoint means *accepted*, not *paid*: the
      outcome comes by webhook or by reading the payment.
  contact:
    name: PayVAN support
    url: https://payvan.net
servers:
  - url: https://api.payvan.net
    description: Live
  - url: https://api.sandbox.payvan.net
    description: Sandbox
security:
  - ShopId: []
    RequestId: []
    RequestTimestamp: []
    RequestSignature: []
tags:
  - name: Checkout
    description: Hosted payment page. Runs on `pay.payvan.net` / `pay.sandbox.payvan.net`.
  - name: Payments
    description: |
      Direct (host-to-host) payments. Requires PCI DSS certification when you
      send card data. A payment is a resource; capture, cancel and refund are
      actions on it (`Amount` optional — empty means the whole remaining
      balance). Every capture, release and refund is an operation of the
      payment, with its own `TransactionId`, amount, state and time — failed
      attempts included.
  - name: 3-D Secure
    description: Report completion of the 3DS step named in the payment's `StateDetails.ThreeDS`.
  - name: Subscriptions
    description: Scheduled charges on a saved card.
  - name: Customers
    description: Customer records and merchant-declared KYC.
  - name: Payouts
    description: Payouts to cards and the payout balance.

paths:
  /v1/checkout/sessions:
    servers:
      - url: https://pay.payvan.net
        description: Live checkout
      - url: https://pay.sandbox.payvan.net
        description: Sandbox checkout
    post:
      tags: [Checkout]
      operationId: createCheckoutSession
      summary: Create a checkout session
      description: |
        Creates a hosted payment session and returns the URL to send the payer to.
        The body is the same flat body as `POST /v1/payments`, without card
        data: `PaymentDetails` is rejected with `CARD_DATA_NOT_ALLOWED`.

        The payer's return to your URL is **not** a payment confirmation:
        fulfil orders on the webhook or by reading the payment.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CheckoutSessionRequest" }
            examples:
              pay:
                summary: One-step payment
                value:
                  PaymentType: Pay
                  Amount: "49.00"
                  Currency: USD
                  OrderId: order-1043
                  Description: Pro plan, November
                  ExtraData:
                    SuccessUrl: https://shop.example/paid
                    DeclineUrl: https://shop.example/failed
                    CancelUrl: https://shop.example/cart
                    ReturnUrl: https://shop.example/return
                  CustomerInfo: { Language: en, Email: payer@example.com }
              subscription:
                summary: First payment of a subscription
                value:
                  Amount: "9.99"
                  Currency: USD
                  OrderId: order-2001
                  Description: Basic plan
                  RebillFlag: true
                  Subscription:
                    Description: Basic plan
                    Interval: { Unit: month, Count: 1 }
                    MaxCycles: 12
                    OfferUrl: https://shop.example/terms
                  ExtraData: { ReturnUrl: https://shop.example/return }
      responses:
        "201":
          description: Session created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CheckoutSession" }
              example:
                SessionId: 6d0f3c1e-2a4b-4c1d-9e5f-0a7b8c9d0e1f
                CheckoutUrl: https://pay.payvan.net/pay/6d0f3c1e-2a4b-4c1d-9e5f-0a7b8c9d0e1f
                ExpiresAt: "2026-09-01T12:40:00Z"
        "400":
          description: |
            Invalid request. `ErrorCode` is one of `INVALID_JSON`, `INVALID_ORDER_ID`,
            `INVALID_AMOUNT`, `INVALID_CURRENCY`, `INVALID_DESCRIPTION`,
            `INVALID_PAYMENT_TYPE`, `INVALID_PAYMENT_METHODS`, `INVALID_RETURN_URL`, `CARD_DATA_NOT_ALLOWED`,
            `SUBSCRIPTION_REQUIRES_REBILL_FLAG`, `INVALID_INTERVAL`,
            `INVALID_OFFER_URL`, `INVALID_START_AT`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: card data must not be sent to the hosted checkout, ErrorCode: CARD_DATA_NOT_ALLOWED }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/payments:
    post:
      tags: [Payments]
      operationId: createPayment
      summary: Create a payment
      description: |
        Creates a payment. The body says how to run it:

        * `PaymentType: Pay` (or no `PaymentType`) charges right away;
        * `PaymentType: Block` puts the amount on hold — capture it later
          with `capture` or release it with `cancel`;
        * `RebillId` instead of `PaymentDetails` charges a card saved earlier
          with `RebillFlag: true` (a merchant-initiated charge). `PaymentType`
          works here too.

        Send card details in `PaymentDetails`, or a wallet token in
        `PaymentDetails.Token` with `PaymentMethod` set to `ApplePay` or
        `GooglePay`. Add `RebillFlag: true` to save the card, and a
        `Subscription` block to start a subscription with this payment.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaymentRequest" }
            examples:
              card:
                summary: Card, charge now
                value:
                  PaymentType: Pay
                  Amount: "49.00"
                  Currency: USD
                  OrderId: order-1043
                  Description: Pro plan, November
                  PaymentMethod: Card
                  RebillFlag: false
                  CustomerInfo: { Email: payer@example.com, IP: 203.0.113.10, Country: US, Language: en }
                  PaymentDetails:
                    CardholderName: JANE DOE
                    CardNumber: "4111111111111111"
                    ExpMonth: "12"
                    ExpYear: "2030"
                    CVC: "123"
              block:
                summary: Card, put on hold
                value:
                  PaymentType: Block
                  Amount: "49.00"
                  Currency: USD
                  OrderId: order-1045
                  Description: Hotel deposit
                  PaymentDetails: { CardholderName: JANE DOE, CardNumber: "4111111111111111", ExpMonth: "12", ExpYear: "2030", CVC: "123" }
              googlePay:
                summary: Google Pay token
                value:
                  Amount: "49.00"
                  Currency: USD
                  OrderId: order-1044
                  Description: Pro plan, November
                  PaymentMethod: GooglePay
                  PaymentDetails:
                    Token: "{\"protocolVersion\":\"ECv2\",\"signature\":\"…\",\"signedMessage\":\"…\"}"
              savedCard:
                summary: Saved card (rebill)
                value: { RebillId: "3554", OrderId: order-1107, Amount: "49.00", Currency: USD, Description: "Pro plan, December" }
              withSubscription:
                summary: First payment of a subscription
                value:
                  Amount: "9.99"
                  Currency: USD
                  OrderId: order-2001
                  Description: Basic plan
                  RebillFlag: true
                  CustomerInfo: { Email: payer@example.com, IP: 203.0.113.10 }
                  PaymentDetails: { CardholderName: JANE DOE, CardNumber: "4111111111111111", ExpMonth: "12", ExpYear: "2030", CVC: "123" }
                  Subscription:
                    OrderId: sub-2001
                    AccountId: user-77
                    Description: Basic plan
                    Interval: { Unit: month, Count: 1 }
                    MaxCycles: 12
                    OfferUrl: https://shop.example/terms
      responses:
        "201":
          description: Accepted. Not yet paid. Read the payment or wait for the webhook.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
              example: { TransactionId: "3554", OrderId: order-1043, Amount: "49.00", Currency: USD }
        "400": { $ref: "#/components/responses/PaymentValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404":
          description: "`SAVED_CARD_NOT_FOUND`: no saved card on this `RebillId`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: |
            `SAVED_CARD_INACTIVE`: the saved card was removed or is not active;
            or `REQUEST_IN_PROGRESS`: a request with the same `X-REQUEST-ID` is
            still being processed — retry shortly with the same ID.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "422": { $ref: "#/components/responses/KeyReused" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Payments]
      operationId: listPayments
      summary: List payments
      description: |
        Your payments, newest first, without their operations (read one
        payment for those). Captures, releases and refunds are operations of a
        payment and do not appear here.

        All filters are optional. A page holds up to `Limit` payments; when
        `HasMore` is true, pass `NextCursor` as `Cursor` for the next one.
      parameters:
        - { name: OrderId, in: query, schema: { type: string }, description: "Your order reference. Not unique: a retried order has several payments." }
        - { name: From, in: query, schema: { type: string, format: date-time }, description: "Created at or after. RFC 3339; without an offset it is read in `TimeZone`." }
        - { name: To, in: query, schema: { type: string, format: date-time }, description: Created at or before. }
        - { name: TimeZone, in: query, schema: { type: string, default: UTC }, description: IANA zone for dates without an offset. }
        - name: States
          in: query
          description: Comma-separated states, e.g. `success,declined`. Empty means all.
          schema:
            type: array
            items: { $ref: "#/components/schemas/TransactionState" }
          style: form
          explode: false
        - { name: Limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
        - { name: Cursor, in: query, schema: { type: string }, description: "`NextCursor` of the previous page." }
      responses:
        "200":
          description: One page, possibly empty.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentsPage" }
              example:
                Data:
                  - { TransactionId: "3554", PaymentId: "3554", Type: Pay, OrderId: order-1043, TransactionState: success, StateDetails: { Amount: "49.00", Currency: USD, RemainingAmount: "19.00", DateTime: "2026-09-01T11:22:05Z" } }
                HasMore: true
                NextCursor: "3554"
        "400":
          description: "`INVALID_REQUEST`: unknown parameter, unparseable date, unknown state or time zone, `To` before `From`, bad `Cursor` or `Limit`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/payments/{id}:
    parameters:
      - $ref: "#/components/parameters/PaymentId"
    get:
      tags: [Payments]
      operationId: getPayment
      summary: Get a payment
      description: |
        The payment with all its operations: captures, releases and refunds,
        failed attempts included, in the order they were made. The payment's own `TransactionState` says
        where it stands; `StateDetails.RemainingAmount` is what can still be
        captured, voided or refunded.

        The ID of a capture, a release, a refund or a payout is not a payment
        ID: use the operation's `PaymentId`, or `GET /v1/operations/{id}`.
      responses:
        "200":
          description: The payment.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payment" }
              example:
                TransactionId: "3554"
                PaymentId: "3554"
                Type: Pay
                OrderId: order-1043
                TransactionState: success
                StateDetails: { Amount: "49.00", Currency: USD, RemainingAmount: "19.00", CardMasked: "411111******1111", RRN: "123456789012", RebillId: "3554", DateTime: "2026-09-01T11:22:05Z" }
                Operations:
                  - { TransactionId: "3556", PaymentId: "3554", Type: Refund, OrderId: order-1043, TransactionState: success, StateDetails: { Amount: "30.00", Currency: USD, DateTime: "2026-09-02T08:10:00Z" } }
        "404": { $ref: "#/components/responses/PaymentNotFound" }

  /v1/payments/{id}/confirm:
    parameters:
      - $ref: "#/components/parameters/PaymentId"
    post:
      tags: [3-D Secure]
      operationId: confirmThreeDS
      summary: Report a completed 3DS step
      description: |
        Call after running the step from `StateDetails.ThreeDS`
        (state `wait_for_3ds`), then read the payment again for the next step
        or the outcome.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ConfirmationRequest" }
            examples:
              method:
                summary: Method step finished
                value: { Step: method, Status: completed }
              challenge2:
                summary: 3DS 2 challenge result
                value: { Step: challenge, CRes: eyJ0aHJlZURTU2VydmVyVHJhbnNJRCI6Ij… }
              challenge1:
                summary: 3DS 1.0 result
                value: { PaRes: eJzNWFmTo7gS…, MD: 3554-a1 }
      responses:
        "200":
          description: Step accepted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
        "409":
          description: "`BAD_STATUS_FOR_CONFIRM`: the payment is not waiting for 3DS."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404": { $ref: "#/components/responses/TRANSACTION_NOT_FOUND" }

  /v1/payments/{id}/payer-data:
    parameters:
      - $ref: "#/components/parameters/PaymentId"
    post:
      tags: [Payments]
      operationId: sendPayerData
      summary: Send the payer details the provider asked for
      description: |
        Some providers will not start a payment without details of the payer:
        a name, an email or a phone, a date of birth. The payment then waits in
        `waiting_action` with `StateDetails.Collect`: `Fields` are all
        required, and of each `AnyOf` group at least one is. Send what the
        payer entered here, flat, in the same spelling as `CustomerInfo`.

        Details sent earlier — in `CustomerInfo` with the payment or in a
        previous call — count: send only what is missing. Not enough is
        `400 PAYER_DATA_INCOMPLETE` naming what is missing; a value in the
        wrong format is `400 INVALID_PAYER_DATA`. Once accepted, the payment
        goes on to the provider, and the outcome comes in a webhook.

        Rather not draw the form yourself? Send the payer to
        `StateDetails.PayerUrl`: our page asks the same fields.

        To skip the step altogether, send `FirstName`, `LastName`, `Email`
        and `Phone` in `CustomerInfo` when you create the payment.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PayerDataRequest" }
            example: { FirstName: Remi, LastName: Terr, Email: remi@example.com }
      responses:
        "200":
          description: Details accepted; the payment goes on to the provider.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
        "400":
          description: "`PAYER_DATA_INCOMPLETE` or `INVALID_PAYER_DATA`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "`BAD_STATUS_FOR_PAYER_DATA`: the payment does not wait for payer details (they were already sent, or the step expired)."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404": { $ref: "#/components/responses/TRANSACTION_NOT_FOUND" }

  /v1/payments/{id}/capture:
    parameters:
      - $ref: "#/components/parameters/PaymentId"
    post:
      tags: [Payments]
      operationId: capture
      summary: Capture a hold
      description: |
        Captures a `preauthorized` payment. Without `Amount`, the whole
        remaining hold is captured. A partial capture closes the
        authorization: the rest is released. A capture is an operation of the
        payment (`Type: Capture`) with its own `TransactionId`; its outcome
        comes as a `Capture` webhook, or `Fail` if the bank refuses — then the
        payment stays `preauthorized`. One capture or release of a payment
        runs at a time.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FollowUpRequest" }
            example: { Amount: "30.00", Currency: USD }
      responses:
        "201":
          description: Accepted. A new operation; `Amount` is the amount being captured.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
              example: { TransactionId: "3551", OrderId: order-1043, Amount: "30.00", Currency: USD }
        "400":
          description: "`AMOUNT_MISMATCH`: more than the hold (see `Details.AvailableAmount`)."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: Amount is bigger than the charged amount, ErrorCode: AMOUNT_MISMATCH, Details: { Amount: "60.00", AvailableAmount: "49.00" } }
        "404": { $ref: "#/components/responses/ActionNotFound" }
        "409":
          description: "`BAD_STATUS_FOR_CHARGE`: the payment is not on hold; `Details.Status` is its status. `OPERATION_IN_PROGRESS`: another capture or release of this payment has no outcome yet."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "503": { $ref: "#/components/responses/NotStarted" }

  /v1/payments/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/PaymentId"
    post:
      tags: [Payments]
      operationId: cancel
      summary: Cancel a payment
      description: |
        Stops the payment, whatever state it is in:

        * waiting for the payer (`waiting_action`) — the payment becomes
          `declined` with `DeclineCode: CANCELLED_BY_USER`, and a `Fail`
          webhook follows;
        * on hold (`preauthorized`) — the hold is released, fully or partly.
          A release is an operation of the payment (`Type: Void`) with its own
          `TransactionId` (`201`). After a partial one (`Amount` below the
          hold) the payment stays `preauthorized` for the rest; a refused
          release is `declined` itself and leaves the hold as it was.

        Anything else — a charged or finished payment — is
        `409 BAD_STATUS_FOR_CANCEL`; a charged payment is returned with a refund.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FollowUpRequest" }
            example: { Amount: "20.00", Currency: USD }
      responses:
        "200":
          description: Cancelled while waiting for the payer — the payment itself.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
              example: { TransactionId: "3552", OrderId: order-1043, Amount: "49.00", Currency: USD }
        "201":
          description: Release — a new operation; its number is in the body.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
              example: { TransactionId: "3553", OrderId: order-1043, Amount: "20.00", Currency: USD }
        "400":
          description: "`AMOUNT_MISMATCH`: more than the hold."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404": { $ref: "#/components/responses/ActionNotFound" }
        "409":
          description: "`BAD_STATUS_FOR_CANCEL`: neither on hold nor waiting for the payer; `Details.Status` is its status. `OPERATION_IN_PROGRESS`: another capture or release of this payment has no outcome yet."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "503": { $ref: "#/components/responses/NotStarted" }

  /v1/payments/{id}/refunds:
    parameters:
      - $ref: "#/components/parameters/PaymentId"
    post:
      tags: [Payments]
      operationId: refund
      summary: Refund a captured payment
      description: |
        Refunds a captured payment, fully or in parts. Every refund is a
        separate operation with its own `TransactionId` and the payment's
        `PaymentId`. Without `Amount`, the remaining refundable balance is
        refunded.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FollowUpRequest" }
            example: { Currency: USD }
      responses:
        "201":
          description: Accepted. A refund is always a new operation; `Amount` is the amount actually refunded.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
              example: { TransactionId: "3556", OrderId: order-1043, Amount: "19.00", Currency: USD }
        "400":
          description: "`AMOUNT_MISMATCH`: more than the refundable balance."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404": { $ref: "#/components/responses/ActionNotFound" }
        "409":
          description: |
            `BAD_STATUS_FOR_REFUND`: the payment exists but is not captured, or
            it is a payout (`Details.Status`, and `Details.Type: Payout`);
            or `REQUEST_IN_PROGRESS`: a request with the same `X-REQUEST-ID`
            is still being processed — retry shortly with the same ID.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example: { error: Operation REFUND requires status CHARGED, ErrorCode: BAD_STATUS_FOR_REFUND, Details: { Status: BLOCKED } }

  /v1/operations/{id}:
    get:
      tags: [Payments]
      operationId: getOperation
      summary: Get an operation
      description: |
        One operation of any kind — a payment, a capture, a release, a refund
        or a payout — by the `TransactionId` from a response or a webhook.
        `PaymentId` points to the payment it belongs to.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string }, example: "3556" }
      responses:
        "200":
          description: Current state.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionStatus" }
              examples:
                success:
                  summary: Captured, partly refunded
                  value:
                    TransactionId: "3554"
                    PaymentId: "3554"
                    Type: Pay
                    OrderId: order-1043
                    TransactionState: success
                    StateDetails: { Amount: "49.00", Currency: USD, RemainingAmount: "19.00", CardMasked: "411111******1111", RRN: "123456789012", RebillId: "3554", DateTime: "2026-09-01T11:22:05Z" }
                threeDS:
                  summary: Waiting for a 3DS step
                  value:
                    TransactionId: "3554"
                    PaymentId: "3554"
                    Type: Pay
                    OrderId: order-1043
                    TransactionState: wait_for_3ds
                    StateDetails:
                      ThreeDS: { Version: 2.2.0, Step: method, MethodUrl: "https://acs.bank.example/method", MethodData: eyJ0aHJlZURTU2VydmVyVHJhbnNJRCI6…, Timeout: 10 }
                declined:
                  summary: Declined
                  value:
                    TransactionId: "3554"
                    PaymentId: "3554"
                    Type: Pay
                    OrderId: order-1043
                    TransactionState: declined
                    StateDetails: { DeclineCode: "51", DeclineReason: Insufficient funds, CardMasked: "411111******1111" }
        "404": { $ref: "#/components/responses/StatusNotFound" }

  /v1/rebills/{id}:
    delete:
      tags: [Payments]
      operationId: disableSavedCard
      summary: Disable a saved card
      description: The card saved on this `RebillId` can no longer be charged. Later payments with it are rejected.
      parameters:
        - { name: id, in: path, required: true, description: "`RebillId`: the payment the card was saved on.", schema: { type: string }, example: "3554" }
      responses:
        "200":
          description: Card disabled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  RebillId: { type: string, description: The saved card that was disabled. }
              example: { RebillId: "3554" }
        "404":
          description: "`TRANSACTION_NOT_FOUND` or `SAVED_CARD_NOT_FOUND`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "`SAVED_CARD_INACTIVE`: the saved card is not active."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /v1/subscriptions:
    post:
      tags: [Subscriptions]
      operationId: createSubscription
      summary: Create a subscription on a saved card
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SubscriptionCreateRequest" }
            example:
              RebillId: "3554"
              OrderId: sub-1044
              AccountId: user-77
              Amount: "9.99"
              Currency: USD
              Description: Basic plan
              Interval: { Unit: month, Count: 1 }
              StartAt: "2026-10-01T09:00:00Z"
              MaxCycles: 12
              TimeZone: America/New_York
              WebhookUrl: https://shop.example/hooks/subs
              Payer: { Email: payer@example.com }
      responses:
        "201":
          description: Subscription created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Subscription" }
        "400": { $ref: "#/components/responses/SubscriptionError" }
        "404": { $ref: "#/components/responses/SubscriptionError" }
        "409": { $ref: "#/components/responses/SubscriptionError" }
    get:
      tags: [Subscriptions]
      operationId: findSubscriptions
      summary: Find subscriptions
      description: |
        Subscriptions matching the filter, up to 100. `HasMore` means more
        matched: narrow the filter.
      parameters:
        - { name: OrderId, in: query, schema: { type: string } }
        - { name: AccountId, in: query, schema: { type: string } }
        - { name: RebillId, in: query, schema: { type: string } }
      responses:
        "200":
          description: Matching subscriptions.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubscriptionsPage" }
        "400":
          description: "`INVALID_REQUEST`: unknown parameter."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/subscriptions/{id}:
    parameters:
      - $ref: "#/components/parameters/SubscriptionId"
    get:
      tags: [Subscriptions]
      operationId: getSubscription
      summary: Get a subscription
      responses:
        "200":
          description: The subscription.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Subscription" }
        "404": { $ref: "#/components/responses/SubscriptionError" }
    patch:
      tags: [Subscriptions]
      operationId: changeSubscription
      summary: Change terms
      description: |
        Send only what changes. A new amount applies from the next period. A
        new `Interval` or `StartAt` rebuilds the schedule from the next period.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                Amount: { $ref: "#/components/schemas/Amount" }
                Description: { type: string }
                Interval: { $ref: "#/components/schemas/Interval" }
                StartAt: { type: string, format: date-time }
                MaxCycles: { type: integer, minimum: 1 }
                EndsAt: { type: string, format: date-time }
            example: { Amount: "12.99" }
      responses:
        "200":
          description: Updated subscription.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Subscription" }
        "400": { $ref: "#/components/responses/SubscriptionError" }
        "404": { $ref: "#/components/responses/SubscriptionError" }
        "409": { $ref: "#/components/responses/SubscriptionError" }

  /v1/subscriptions/{id}/pause:
    parameters:
      - $ref: "#/components/parameters/SubscriptionId"
    post:
      tags: [Subscriptions]
      operationId: pauseSubscription
      summary: Pause
      responses:
        "200":
          description: Paused.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Subscription" }
        "404": { $ref: "#/components/responses/SubscriptionError" }
        "409": { $ref: "#/components/responses/SubscriptionError" }

  /v1/subscriptions/{id}/resume:
    parameters:
      - $ref: "#/components/parameters/SubscriptionId"
    post:
      tags: [Subscriptions]
      operationId: resumeSubscription
      summary: Resume
      description: Without `ResumeAt`, charging continues on the next date of the original schedule. Paused periods are not charged.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                ResumeAt: { type: string, format: date-time }
            example: { ResumeAt: "2026-11-01T09:00:00Z" }
      responses:
        "200":
          description: Resumed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Subscription" }
        "404": { $ref: "#/components/responses/SubscriptionError" }
        "409": { $ref: "#/components/responses/SubscriptionError" }

  /v1/subscriptions/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/SubscriptionId"
    post:
      tags: [Subscriptions]
      operationId: cancelSubscription
      summary: Cancel
      description: Always possible, including during a charge.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                Reason:
                  type: string
                  enum: [merchant, payer]
                  default: merchant
                  description: "`payer` if the customer asked; the payer gets a different email."
            example: { Reason: payer }
      responses:
        "200":
          description: Cancelled.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Subscription" }
        "404": { $ref: "#/components/responses/SubscriptionError" }

  /v1/customers:
    get:
      tags: [Customers]
      operationId: listCustomers
      summary: List customers
      parameters:
        - { name: KycVerified, in: query, schema: { type: boolean }, description: "`false` finds customers you declared unverified, not everyone else." }
        - { name: RoutingGroup, in: query, schema: { type: string }, description: "An empty value (`RoutingGroup=`) finds customers without a group." }
        - { name: FirstDepositFrom, in: query, schema: { type: string, format: date } }
        - { name: FirstDepositTo, in: query, schema: { type: string, format: date } }
        - { name: OnlyThisShop, in: query, schema: { type: boolean } }
        - { name: Limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
        - { name: Cursor, in: query, schema: { type: string }, description: "`NextCursor` of the previous page." }
      responses:
        "200":
          description: One page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomersPage" }
        "400":
          description: "`INVALID_REQUEST` (unknown parameter) or `INVALID_CURSOR`, `INVALID_FIRST_DEPOSIT_FROM`, `INVALID_FIRST_DEPOSIT_TO`, `INVALID_ROUTING_GROUP`, `INVALID_LIMIT`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/customers/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: "Your `CustomerId`. URL-encode it: `team/u-1` becomes `team%2Fu-1`."
        schema: { type: string }
        example: u-81723
    get:
      tags: [Customers]
      operationId: getCustomer
      summary: Get a customer
      responses:
        "200":
          description: The customer.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        "404":
          description: "`CUSTOMER_NOT_FOUND`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    patch:
      tags: [Customers]
      operationId: changeCustomer
      summary: Declare KYC or routing group
      description: Applied in full or not at all.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                KycVerified: { type: boolean }
                RoutingGroup: { type: string }
                Cards:
                  type: array
                  maxItems: 20
                  items:
                    type: object
                    required: [CardMasked, CardKycVerified]
                    properties:
                      CardMasked: { type: string, description: "First six and last four digits with asterisks between, e.g. `400000***0002`." }
                      CardKycVerified: { type: boolean }
            example: { KycVerified: true, Cards: [ { CardMasked: "400000***0002", CardKycVerified: true } ] }
      responses:
        "200":
          description: The customer after the change.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        "400":
          description: "`NOTHING_TO_CHANGE`, `INVALID_CARD_MASKED`, `INVALID_CARD_KYC_VERIFIED`, `INVALID_ROUTING_GROUP`, `TOO_MANY_CARDS`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "`CUSTOMER_NOT_FOUND` or `CARD_NOT_FOUND`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "`CARD_AMBIGUOUS`: two cards share this mask."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /v1/payouts:
    post:
      tags: [Payouts]
      operationId: createPayout
      summary: Pay out to a card
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PayoutRequest" }
            example:
              Amount: "120.00"
              Currency: USD
              OrderId: payout-77
              Description: Seller payout, October
              PayoutMethod: Card
              CustomerInfo: { Email: seller@example.com, Country: US, IP: 203.0.113.20 }
              PayoutDetails: { CardholderName: JOHN ROE, CardNumber: "4111111111111111", ExpMonth: "12", ExpYear: "2030", BankName: EXAMPLE BANK }
      responses:
        "201":
          description: |
            Accepted, not paid out yet. The outcome arrives as a `Payout` or `Fail`
            webhook. A payout above the available balance is rejected right away
            with `400 INSUFFICIENT_PAYOUT_BALANCE`; one that fits the balance can
            still be declined after acceptance (our fee or a concurrent payout
            took the rest): `Fail` with `LIMITS_REACHED`, "Payout limits reached".
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OperationResponse" }
        "400":
          description: |
            Validation error (`INVALID_*`, including `INVALID_PAYOUT_METHOD`), or
            `INSUFFICIENT_PAYOUT_BALANCE`: the amount is bigger than the available
            payout balance. No payout is created; `Details` carries the requested
            `Amount`, the `Balance` and its `Currency`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              examples:
                balance:
                  value: { error: Payout amount is bigger than the available payout balance, ErrorCode: INSUFFICIENT_PAYOUT_BALANCE, Details: { Amount: "120.00", Balance: "75.40", Currency: USD } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/InProgress" }
        "422": { $ref: "#/components/responses/KeyReused" }

  /v1/payouts/{id}:
    get:
      tags: [Payouts]
      operationId: getPayout
      summary: Get a payout
      parameters:
        - { name: id, in: path, required: true, schema: { type: string }, example: "4120" }
      responses:
        "200":
          description: Current state of the payout.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionStatus" }
              example: { TransactionId: "4120", PaymentId: "4120", Type: Payout, OrderId: payout-77, TransactionState: success, StateDetails: { Amount: "120.00", Currency: USD, CardMasked: "411111******1111", DateTime: "2026-10-01T10:00:00Z" } }
        "404":
          description: "`PAYOUT_NOT_FOUND`: no such payout for your shop (a payment ID is not a payout)."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /v1/balance:
    get:
      tags: [Payouts]
      operationId: getBalance
      summary: Payout balance
      description: Signed request with an empty body. May be restricted to your server IPs.
      responses:
        "200": { $ref: "#/components/responses/Balance" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409":
          description: "`BALANCE_UNAVAILABLE`: the balance could not be read."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

webhooks:
  operationEvent:
    post:
      operationId: operationWebhook
      summary: Operation webhook
      description: |
        Sent to your shop's webhook URL when an operation changes state.

        Verify `X-WEBHOOK-SIGNATURE` before trusting the body:

        ```
        v1-webhook\nPOST\n{FULL_URL}\n{SHOP_ID}\n{WEBHOOK_ID}\n{TIMESTAMP}\n{hex(SHA-256(BODY))}
        ```

        The header may hold several comma-separated `v1-webhook=<hex>` values
        (one per valid key during a secret rotation). Accept if any matches.

        Reply `2xx` within 30 seconds. Anything else is retried: 30 s, then
        doubling up to 6 h, 12 attempts in total. Deduplicate on `X-WEBHOOK-ID`.
      parameters:
        - { name: X-SHOP-ID, in: header, required: true, schema: { type: string } }
        - { name: X-WEBHOOK-ID, in: header, required: true, description: Same on every delivery attempt of one notification., schema: { type: string } }
        - { name: X-WEBHOOK-TIMESTAMP, in: header, required: true, description: Unix seconds of this attempt., schema: { type: string } }
        - { name: X-WEBHOOK-SIGNATURE, in: header, required: true, schema: { type: string }, example: v1-webhook=8d1f0f9a2207241e640bd28aef00de33aca990e7087b06df38a09917d995b4b6 }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OperationWebhook" }
            example:
              Event: Payment
              TransactionId: "3554"
              PaymentId: "3554"
              OrderId: order-1043
              ShopId: "1001"
              Amount: "49.00"
              Currency: USD
              DateTime: "2026-09-01T11:22:05Z"
              CardMasked: "411111******1111"
              ExpirationDate: 12/30
              RRN: "123456789012"
              Email: payer@example.com
              ExtraData: { plan: pro }
              IsTest: false
      responses:
        "200":
          description: Received. Any 2xx stops retries.
  subscriptionEvent:
    post:
      operationId: subscriptionWebhook
      summary: Subscription webhook
      description: |
        Sent to the subscription's `WebhookUrl`, or to your shop's webhook URL.
        Same headers, signature and retries as the operation webhook.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SubscriptionWebhook" }
      responses:
        "200":
          description: Received.

components:
  securitySchemes:
    ShopId:
      type: apiKey
      in: header
      name: X-SHOP-ID
      description: Your Shop ID.
    RequestId:
      type: apiKey
      in: header
      name: X-REQUEST-ID
      description: |
        Unique request ID (UUID v4). Reuse it only to retry the same operation:
        responses are kept for 24 hours, and money never moves twice for the
        same ID, with no time limit. A payment, authorization or payout is not
        created twice for the same ID and OrderId, a capture, release or refund
        is not created twice for the same ID and payment: the retry returns the
        existing operation. The same ID with a different method, path or body
        gets 422 IDEMPOTENCY_KEY_REUSED.
    RequestTimestamp:
      type: apiKey
      in: header
      name: X-REQUEST-TIMESTAMP
      description: Unix seconds. Accepted within ±5 minutes of server time.
    RequestSignature:
      type: apiKey
      in: header
      name: X-REQUEST-SIGNATURE
      description: |
        `v1=` + lowercase hex HMAC-SHA256(secret,
        `v1\nMETHOD\nPATH_WITH_QUERY\nSHOP_ID\nREQUEST_ID\nTIMESTAMP\nhex(SHA-256(BODY))`).

  parameters:
    PaymentId:
      name: id
      in: path
      required: true
      description: The payment's `TransactionId` (its `PaymentId`).
      schema: { type: string }
      example: "3554"
    SubscriptionId:
      name: id
      in: path
      required: true
      schema: { type: string }
      example: "8812"

  responses:
    Unauthorized:
      description: Missing headers, invalid signature, unknown shop, or timestamp outside the window (then `server_time` is included).
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
              ErrorCode:
                type: string
                enum: [MISSING_HEADERS, UNKNOWN_SHOP, INVALID_TIMESTAMP, INVALID_SIGNATURE, STALE_TIMESTAMP]
              server_time: { type: integer, description: "Our Unix time, present only for a stale timestamp." }
          examples:
            signature: { value: { error: invalid signature, ErrorCode: INVALID_SIGNATURE } }
            clock: { value: { error: request timestamp outside allowed window, ErrorCode: STALE_TIMESTAMP, server_time: 1789733105 } }
    InProgress:
      description: |
        A request with the same `X-REQUEST-ID` is still being processed (at
        most 90 seconds if it died without an answer). Retry shortly with the
        same ID.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: duplicate request in progress, ErrorCode: REQUEST_IN_PROGRESS }
    NotStarted:
      description: |
        `OPERATION_NOT_STARTED`: the operation was accepted but could not be
        started, and nothing went to the bank. Retry with the same
        `X-REQUEST-ID`: the retry returns the same operation and starts it.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "operation was not started, retry the request with the same X-REQUEST-ID", ErrorCode: OPERATION_NOT_STARTED }
    KeyReused:
      description: This `X-REQUEST-ID` was already used for a different request (method, path or body).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: X-REQUEST-ID was already used for a different request, ErrorCode: IDEMPOTENCY_KEY_REUSED }
    TooLarge:
      description: Body larger than 1 MB.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Too many requests for this Shop ID. Back off and retry with the same `X-REQUEST-ID`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PaymentValidationError:
      description: |
        Rejected before a payment was created. `ErrorCode` is one of
        `INVALID_JSON`, `INVALID_REQUEST` (a field of the wrong JSON type, or
        `RebillId` together with `PaymentDetails`), `INVALID_PAYMENT_TYPE`,
        `INVALID_ORDER_ID`, `INVALID_AMOUNT`, `INVALID_CURRENCY`,
        `INVALID_CARD_NUMBER`, `INVALID_CARD_DATE`, `INVALID_CARDHOLDER`,
        `INVALID_CVC`, `INVALID_PAYMENT_DETAILS`, `INVALID_WALLET_TOKEN`.
        For a saved card: `CARD_EXPIRED`, and with saved-card limits enabled
        for the shop `SAVED_CARD_LIMIT_REACHED` (no charges or amount left) or
        `SAVED_CARD_PERMISSION_EXPIRED` (the permission to charge the card has
        run out; not the card's own expiry).
        Customer block errors come as `BusinessError` (`CUSTOMER_ID_REQUIRED`,
        `INVALID_CUSTOMER_ID`, …), and an expired card with `ErrorCode: CARD_EXPIRED`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Amount must be a positive decimal, ErrorCode: INVALID_AMOUNT }
    TRANSACTION_NOT_FOUND:
      description: No such operation for your shop.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Transaction not found, ErrorCode: TRANSACTION_NOT_FOUND }
    ActionNotFound:
      description: |
        `TRANSACTION_NOT_FOUND`: no such operation for your shop.
        `PAYMENT_NOT_FOUND`: the ID is an operation of a payment (a capture, a
        release, a refund), not the payment itself — act by its `PaymentId`.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: payment not found, ErrorCode: PAYMENT_NOT_FOUND }
    PaymentNotFound:
      description: No such payment for your shop. The ID of a capture, a release, a refund or a payout is not a payment ID.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: payment not found, ErrorCode: PAYMENT_NOT_FOUND }
    StatusNotFound:
      description: No such operation for your shop.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: transaction not found, ErrorCode: TRANSACTION_NOT_FOUND }
    SubscriptionError:
      description: |
        `SUBSCRIPTION_NOT_FOUND` (404), `SUBSCRIPTION_EXISTS` (409),
        `SUBSCRIPTION_BAD_STATUS` (409), `SUBSCRIPTION_ATTEMPT_IN_FLIGHT` (409),
        `SAVED_CARD_NOT_FOUND` (404), `SAVED_CARD_INACTIVE` (409),
        `SUBSCRIPTION_REQUIRES_REBILL_FLAG`, `INVALID_INTERVAL`, `INVALID_START_AT`,
        `INVALID_ENDS_AT`, `INVALID_MAX_CYCLES`, `INVALID_TIME_ZONE` (400).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Balance:
      description: Available payout balance.
      content:
        application/json:
          schema:
            type: object
            properties:
              Balance: { $ref: "#/components/schemas/Amount" }
              Currency: { $ref: "#/components/schemas/Currency" }
          example: { Balance: "12540.00", Currency: USD }

  schemas:
    Amount:
      type: string
      pattern: '^\d{1,16}(\.\d{2})?$'
      description: Positive decimal in major units, no decimals or exactly two.
      examples: ["49.00", "1200"]
    Currency:
      type: string
      pattern: '^[A-Za-z]{3}$'
      description: ISO 4217 code; must be your shop's currency.
      examples: [USD]
    OrderId:
      type: string
      minLength: 1
      maxLength: 100
      description: Your reference, printable characters.

    Error:
      type: object
      description: |
        Every error has this shape. `ErrorCode` is a stable `UPPER_SNAKE` code
        to branch on; `error` is a message for people; `Details` carries extra
        data for some codes (e.g. `AvailableAmount` for `AMOUNT_MISMATCH`).
      required: [error, ErrorCode]
      properties:
        error: { type: string }
        ErrorCode: { type: string, examples: [AMOUNT_MISMATCH] }
        Details:
          type: object
          additionalProperties: true

    CustomerInfo:
      type: object
      description: Payer details. Customer fields (`CustomerId` and below) build a customer profile; see the guide.
      properties:
        Email: { type: string, format: email }
        Phone: { type: string }
        IP: { type: string, description: Payer's IP address. Required by some banks. }
        Country: { type: string, description: ISO 3166-1 alpha-2. }
        Language: { type: string, description: "Payer's language, e.g. `en`." }
        Address: { type: string }
        Town: { type: string }
        ZIP: { type: string }
        CustomerId: { type: string, minLength: 1, maxLength: 64, description: "Your ID for the payer, shared across your shops." }
        FirstName: { type: string }
        LastName: { type: string }
        BirthDate: { type: string, format: date }
        Citizenship: { type: string, description: ISO 3166-1 alpha-2. }
        KycVerified: { type: boolean, description: You verified the customer. Persists until revoked with `false`. Requires `CustomerId`. }
        CardKycVerified: { type: boolean, description: You verified that this payment's card belongs to the customer. Requires `CustomerId`. }
        RoutingGroup: { type: string, maxLength: 64, description: Segment label for routing rules; `""` removes it. Requires `CustomerId`. }
        Document:
          type: object
          properties:
            Type: { type: string }
            Number: { type: string }
        BankAccount:
          type: object
          properties:
            Number: { type: string }
            Name: { type: string }
            Type: { type: string, enum: [SAVINGS, CHECKING] }
            Bank: { type: string }
            BankCode: { type: string }
            Branch: { type: string }
            BranchCode: { type: string }
        History:
          type: object
          description: The customer's history with you before PayVAN. Passed to the bank, not used in routing.
          properties:
            FirstDepositAt: { type: string, format: date }
            DepositsCount: { type: integer }
            DepositsAmount: { type: string }
            PayoutsCount: { type: integer }
            PayoutsAmount: { type: string }
            Currency: { type: string }

    CardDetails:
      type: object
      properties:
        CardNumber: { type: string, pattern: '^\d{13,19}$', description: Must pass the Luhn check. }
        ExpMonth: { type: string, description: "`1`–`12`." }
        ExpYear: { type: string, description: "`30` or `2030`." }
        CVC: { type: string, pattern: '^\d{3,4}$' }
        CardholderName: { type: string, pattern: '^[a-zA-Z .,-]{3,30}$' }
    WalletDetails:
      type: object
      required: [Token]
      properties:
        Token:
          description: The wallet token exactly as returned by the wallet; an object for Apple Pay, a string for Google Pay. Max 16 KB.
          oneOf:
            - type: object
            - type: string

    PaymentRequest:
      type: object
      description: |
        A new payment. For a charge on a saved card send `RebillId` instead of
        `PaymentDetails`; `PaymentMethod`, `RebillFlag`, `CustomerInfo` and
        `Subscription` don't apply then.
      required: [Amount, Currency, OrderId]
      properties:
        PaymentType:
          type: string
          enum: [Pay, Block]
          default: Pay
          description: "`Block` puts the amount on hold instead of charging it."
        RebillId: { type: string, description: "Charge the card saved on this payment (`RebillFlag: true`). Excludes `PaymentDetails`." }
        Amount: { $ref: "#/components/schemas/Amount" }
        Currency: { $ref: "#/components/schemas/Currency" }
        OrderId: { $ref: "#/components/schemas/OrderId" }
        Description: { type: string }
        PaymentMethod:
          type: string
          description: "`Card`, `ApplePay`, `GooglePay`, or another method enabled for your shop."
          examples: [Card]
        RebillFlag: { type: boolean, default: false, description: Save the card for later charges. }
        CustomerInfo: { $ref: "#/components/schemas/CustomerInfo" }
        PaymentDetails:
          oneOf:
            - $ref: "#/components/schemas/CardDetails"
            - $ref: "#/components/schemas/WalletDetails"
        ExtraData:
          type: object
          additionalProperties: { type: string }
          description: Your own flat key/value data. Returned in webhooks as `ExtraData`.
        WebhookUrl:
          type: string
          format: uri
          description: Send this payment's webhooks (and of its captures, releases and refunds) here instead of your shop's webhook URL. A bad URL gets `INVALID_WEBHOOK_URL`.
        Subscription: { $ref: "#/components/schemas/SubscriptionTerms" }

    FollowUpRequest:
      type: object
      description: The payment is in the path. The body may be empty.
      properties:
        Amount:
          $ref: "#/components/schemas/Amount"
          description: Optional. Empty means the whole remaining balance.
        Currency:
          $ref: "#/components/schemas/Currency"

    ConfirmationRequest:
      type: object
      properties:
        Step: { type: string, enum: [method, challenge] }
        Status:
          type: string
          enum: [completed, timeout, error]
          default: completed
          description: For the `method` step.
        CRes: { type: string, description: 3DS 2 challenge result. }
        PaRes: { type: string, description: 3DS 1.0 result. }
        MD: { type: string, description: 3DS 1.0 merchant data. }

    OperationResponse:
      type: object
      properties:
        TransactionId: { type: string }
        OrderId: { type: string }
        Amount: { $ref: "#/components/schemas/Amount" }
        Currency: { $ref: "#/components/schemas/Currency" }
        SubscriptionId: { type: string, description: Present when a subscription was created with the payment. }

    TransactionState:
      type: string
      enum: [success, preauthorized, voided, pending, wait_for_3ds, redirect, waiting_action, declined, cancelled_by_user, abandoned]
      description: |
        Final: `success`, `preauthorized` (until captured or voided), `voided`,
        `declined`, `cancelled_by_user`, `abandoned`. In progress: `pending`,
        `wait_for_3ds`, `redirect`, `waiting_action`. `pending` is never a decline.

    TransactionStatus:
      type: object
      description: One operation.
      properties:
        TransactionId: { type: string }
        PaymentId: { type: string, description: "The payment this operation belongs to: its own ID for a payment or a payout, the payment's for a capture, a release or a refund." }
        Type:
          type: string
          enum: [Pay, Block, Capture, Void, Refund, Payout]
          description: "What the operation is. `Capture` and `Void` are a capture and a release of a hold, `Refund` a refund; each is its own operation of the payment."
        OrderId: { type: string }
        TransactionState: { $ref: "#/components/schemas/TransactionState" }
        StateDetails: { $ref: "#/components/schemas/StateDetails" }

    Payment:
      allOf:
        - $ref: "#/components/schemas/TransactionStatus"
        - type: object
          properties:
            Operations:
              type: array
              description: "Captures, releases and refunds of the payment, failed attempts included, oldest first. Empty if none."
              items: { $ref: "#/components/schemas/TransactionStatus" }

    PaymentsPage:
      type: object
      properties:
        Data:
          type: array
          items: { $ref: "#/components/schemas/TransactionStatus" }
        HasMore: { type: boolean }
        NextCursor: { type: string, description: "Pass as `Cursor` for the next page. Present when `HasMore` is true." }

    SubscriptionsPage:
      type: object
      properties:
        Data:
          type: array
          items: { $ref: "#/components/schemas/Subscription" }
        HasMore: { type: boolean, description: More than 100 matched; narrow the filter. }

    CustomersPage:
      type: object
      properties:
        Data:
          type: array
          items: { $ref: "#/components/schemas/Customer" }
        HasMore: { type: boolean }
        NextCursor: { type: string, description: "Pass as `Cursor` for the next page. Present when `HasMore` is true." }

    StateDetails:
      type: object
      description: Fields depend on the state; empty values are omitted.
      properties:
        Amount: { $ref: "#/components/schemas/Amount" }
        Currency: { $ref: "#/components/schemas/Currency" }
        ProcessingAmount: { type: string, description: "Amount in the bank's processing currency, if different." }
        ProcessingCurrency: { type: string }
        RemainingAmount: { type: string, description: "What can still be captured, voided or refunded." }
        CardMasked: { type: string, examples: ["411111******1111"] }
        RRN: { type: string }
        RebillId: { type: string, description: Present when the card was saved. }
        DateTime: { type: string, format: date-time, description: "Creation time, RFC 3339 in UTC.", examples: ["2026-09-01T11:22:05Z"] }
        ReceiptLink: { type: string, format: uri }
        DeclineCode: { type: string, description: "Decline code. In `declined`, and in `preauthorized` after the bank refused a capture or a full release." }
        DeclineReason: { type: string, description: "Decline reason you can show the payer. In `declined`, and in `preauthorized` after the bank refused a capture or a full release." }
        ThreeDS: { $ref: "#/components/schemas/ThreeDSStep" }
        ThreeDSResult: { $ref: "#/components/schemas/ThreeDSResult" }
        AcsUrl: { type: string, format: uri, description: 3DS 1.0 ACS URL. }
        PaReq: { type: string }
        MD: { type: string }
        RedirectUrl: { type: string, format: uri, description: "Address of the bank page, as a plain URL." }
        RedirectMethod: { type: string, enum: [GET, POST] }
        RedirectParams: { type: object, additionalProperties: true, description: Form fields to post when `RedirectMethod` is POST. }
        QrCodeString: { type: string }
        Display: { $ref: "#/components/schemas/Display" }
        Collect: { $ref: "#/components/schemas/Collect" }
        PayerUrl: { type: string, format: uri, description: "Our page that shows the `Display` step or asks the `Collect` fields: send the payer there if you do not draw the screen yourself. In `waiting_action`." }

    Display:
      type: object
      description: >-
        What to show the payer in `waiting_action`: a QR code, an address and
        an exact amount to transfer, or a link that opens the payer's app.
        Show values as text. The final state comes in a webhook, not by the
        timer.
      properties:
        Kind: { type: string, enum: [qr, credentials, instructions] }
        Payload:
          type: object
          properties:
            Qr: { type: string, description: Text to encode as a QR code. }
            Link: { type: string, format: uri, description: HTTPS link that opens the payer's app. }
            Address: { type: string, description: Where to transfer. }
            Memo: { type: string, description: Tag the transfer must carry. }
            Amount: { type: string, description: "Exact amount to transfer, up to 18 decimals.", examples: ["0.14002249"] }
            AmountCurrency: { type: string, examples: [BNB, USDT] }
            Network: { type: string, description: Network to transfer on., examples: [TRC20, BSC] }
            Text: { type: string, description: Instructions in words. }
        ExpiresAt: { type: string, format: date-time, description: "Until when the step is valid, RFC 3339 in UTC." }

    Collect:
      type: object
      description: >-
        Payer details the provider needs before it starts the payment, in
        `waiting_action`. Send them with `POST /v1/payments/{id}/payer-data`.
        No money has moved yet. Without the details in time the payment is
        declined with `DeclineCode: PAYER_DATA_TIMEOUT`.
      properties:
        Fields:
          type: array
          description: All of these are required.
          items: { $ref: "#/components/schemas/PayerField" }
        AnyOf:
          type: array
          description: At least one field of each group is required.
          items:
            type: array
            items: { $ref: "#/components/schemas/PayerField" }
      example: { Fields: [FirstName, LastName], AnyOf: [[Email, Phone]] }

    PayerField:
      type: string
      enum: [FirstName, LastName, Email, Phone, BirthDate, Country]

    PayerDataRequest:
      type: object
      description: Payer details for a `Collect` step. Empty fields are ignored.
      properties:
        FirstName: { type: string, maxLength: 100 }
        LastName: { type: string, maxLength: 100 }
        Email: { type: string, format: email }
        Phone: { type: string, description: "International format; spaces, dashes and brackets are dropped.", examples: ["+44 7700 900123"] }
        BirthDate: { type: string, format: date }
        Country: { type: string, description: "ISO 3166-1 alpha-2 country of residence.", examples: [GB] }

    ThreeDSStep:
      type: object
      properties:
        Version: { type: string, examples: [2.2.0] }
        Step: { type: string, enum: [method, challenge] }
        MethodUrl: { type: string, format: uri }
        MethodData: { type: string, description: Value for the `threeDSMethodData` form field. }
        Timeout: { type: integer, description: "Seconds to wait for the step. For `method`: for the 3DS Method Notification message; none by then is `timeout`." }
        AcsUrl: { type: string, format: uri }
        CReq: { type: string, description: Value for the `creq` form field. }
        PaReq: { type: string }
        MD: { type: string }

    ThreeDSResult:
      type: object
      properties:
        Version: { type: string }
        TransStatus: { type: string, enum: [Y, A, N, R, U, I] }
        ECI: { type: string }
        DsTransId: { type: string }
        AcsTransId: { type: string }

    CheckoutSessionRequest:
      type: object
      description: |
        The same flat body as `POST /v1/payments`, without card details: the
        hosted page collects them. `PaymentDetails` is rejected with
        `CARD_DATA_NOT_ALLOWED`.
      required: [Amount, Currency, OrderId, Description]
      properties:
        PaymentType:
          type: string
          enum: [Pay, Block]
          default: Pay
          description: "`Block` puts the amount on hold instead of charging it."
        PaymentMethods:
          type: array
          items: { type: string }
          description: Offer only these of your shop's methods on the page, e.g. `["Card", "SBP"]`. Empty means all available.
        Amount: { $ref: "#/components/schemas/Amount" }
        Currency: { $ref: "#/components/schemas/Currency" }
        OrderId: { $ref: "#/components/schemas/OrderId" }
        Description: { type: string, description: Shown to the payer. }
        RebillFlag: { type: boolean, default: false }
        WebhookUrl: { type: string, format: uri, description: Send this payment's webhooks here instead of your shop's webhook URL. }
        Subscription: { $ref: "#/components/schemas/SubscriptionTerms" }
        ExtraData:
          type: object
          properties:
            SuccessUrl: { type: string, format: uri }
            DeclineUrl: { type: string, format: uri }
            CancelUrl: { type: string, format: uri }
            ReturnUrl: { type: string, format: uri, description: Fallback for any of the three above. }
          additionalProperties: { type: string }
        CustomerInfo:
          type: object
          properties:
            Language: { type: string, enum: [en, es, id, ru] }
            Email: { type: string, format: email }
            Phone: { type: string }

    CheckoutSession:
      type: object
      properties:
        SessionId: { type: string }
        CheckoutUrl: { type: string, format: uri }
        ExpiresAt: { type: string, format: date-time }

    Interval:
      type: object
      required: [Unit, Count]
      properties:
        Unit: { type: string, enum: [day, week, month, year] }
        Count: { type: integer, minimum: 1, maximum: 366 }

    SubscriptionTerms:
      type: object
      description: "Requires `RebillFlag: true`. Amount and Currency default to the first payment's."
      required: [Interval]
      properties:
        OrderId: { type: string }
        AccountId: { type: string, description: Your customer account ID. }
        Amount: { $ref: "#/components/schemas/Amount" }
        Currency: { $ref: "#/components/schemas/Currency" }
        Description: { type: string }
        Interval: { $ref: "#/components/schemas/Interval" }
        StartAt: { type: string, format: date-time, description: Second charge time; must be in the future. }
        EndsAt: { type: string, format: date-time }
        MaxCycles: { type: integer, minimum: 1 }
        OfferUrl: { type: string, format: uri, description: Terms the payer accepts. Required on hosted checkout. }
        TimeZone: { type: string, default: UTC }
        WebhookUrl: { type: string, format: uri }

    SubscriptionCreateRequest:
      allOf:
        - $ref: "#/components/schemas/SubscriptionTerms"
        - type: object
          required: [RebillId, OrderId, Amount, Currency, StartAt]
          properties:
            RebillId: { type: string }
            Payer:
              type: object
              properties:
                Email: { type: string, format: email }

    Subscription:
      type: object
      properties:
        SubscriptionId: { type: string }
        OrderId: { type: string }
        AccountId: { type: string }
        RebillId: { type: string }
        Status: { type: string, enum: [pending, active, past_due, paused, completed, cancelled] }
        Amount: { $ref: "#/components/schemas/Amount" }
        Currency: { $ref: "#/components/schemas/Currency" }
        Description: { type: string }
        Interval: { $ref: "#/components/schemas/Interval" }
        TimeZone: { type: string }
        PaidCycles: { type: integer }
        MaxCycles: { type: [integer, "null"] }
        NextChargeAt: { type: [string, "null"], format: date-time }
        EndsAt: { type: [string, "null"], format: date-time }
        CurrentCycle:
          type: [object, "null"]
          description: Present while a period is unpaid.
          additionalProperties: true
        CancelReason:
          type: [string, "null"]
          enum: [merchant, payer, operator, mandate_revoked, first_payment_failed, unpaid, card_unusable, null]
        CreatedAt: { type: string, format: date-time }
      example:
        SubscriptionId: "8812"
        OrderId: sub-1044
        RebillId: "3554"
        Status: active
        Amount: "9.99"
        Currency: USD
        Interval: { Unit: month, Count: 1 }
        TimeZone: America/New_York
        PaidCycles: 0
        MaxCycles: 12
        NextChargeAt: "2026-10-01T09:00:00Z"
        CreatedAt: "2026-09-15T12:00:00Z"

    Customer:
      type: object
      properties:
        CustomerId: { type: string }
        FirstName: { type: string }
        LastName: { type: string }
        RoutingGroup: { type: string }
        KycVerified: { type: [boolean, "null"] }
        KycChangedAt: { type: [string, "null"], format: date-time }
        FirstSeenAt: { type: string, format: date-time }
        LastSeenAt: { type: string, format: date-time }
        History:
          type: object
          properties:
            FirstDepositAt: { type: [string, "null"], format: date-time }
            DepositsCount: { type: integer }
            PayoutsCount: { type: integer }
            Amounts:
              type: array
              items:
                type: object
                properties:
                  Kind: { type: string, enum: [deposit, payout] }
                  Currency: { type: string }
                  Amount: { type: string }
            Shop:
              type: object
              description: The same counters for the requesting shop only.
              additionalProperties: true
        Cards:
          type: array
          items:
            type: object
            properties:
              CardMasked: { type: string }
              ExpMonth: { type: integer }
              ExpYear: { type: integer }
              CardKycVerified: { type: [boolean, "null"] }
              DepositsCount: { type: integer }
              FirstDepositAt: { type: [string, "null"], format: date-time }

    PayoutRequest:
      type: object
      required: [Amount, Currency, OrderId, PayoutDetails]
      properties:
        Amount: { $ref: "#/components/schemas/Amount" }
        Currency: { $ref: "#/components/schemas/Currency" }
        OrderId: { $ref: "#/components/schemas/OrderId" }
        Description: { type: string }
        PayoutMethod: { type: string, enum: [Card], default: Card }
        CustomerInfo: { $ref: "#/components/schemas/CustomerInfo" }
        PayoutDetails:
          type: object
          description: The recipient's card.
          required: [CardNumber, ExpMonth, ExpYear]
          properties:
            CardNumber: { type: string }
            ExpMonth: { type: string }
            ExpYear: { type: string }
            CardholderName: { type: string, description: Optional; must be well-formed if sent. }
            BankName: { type: string }
            BankCode: { type: string }

    OperationWebhook:
      type: object
      required: [Event, TransactionId, OrderId, Amount, Currency]
      properties:
        Event: { type: string, enum: [Payment, Block, Capture, Refund, Void, Payout, Fail, Abandoned, 3DS, 3DSRedirect, WaitingAction] }
        TransactionId: { type: string }
        PaymentId: { type: string, description: "The payment the operation belongs to: `GET /v1/payments/{PaymentId}` shows it with all its operations. Equals `TransactionId` for the payment itself and for a payout. `Fail` with a different `PaymentId` is a refused capture or release: the payment stays on hold." }
        OrderId: { type: string }
        ShopId: { type: string, description: Your Shop ID. }
        Amount: { type: string, description: "For Capture, Refund and Void, the amount of that operation: the captured, refunded or released part." }
        Currency: { type: string }
        DateTime: { type: string, format: date-time, description: "Creation time, RFC 3339 in UTC.", examples: ["2026-09-01T11:22:05Z"] }
        IsTest: { type: boolean }
        Description: { type: string }
        Email: { type: string }
        Phone: { type: string }
        CardMasked: { type: string }
        ExpirationDate: { type: string, description: "`MM/YY`." }
        CardholderName: { type: string }
        RRN: { type: string }
        ExtraData:
          type: object
          additionalProperties: { type: string }
          description: The ExtraData you sent with the payment.
        PaylinkCode: { type: string, description: "The payment link, for payments made through one." }
        RemainingAmount: { type: string, description: "Refund and Void only: what can still be refunded, or is still on hold." }
        PayoutToken: { type: string }
        DeclineCode: { type: string, description: "`Fail` only, when known. The same code as `StateDetails.DeclineCode`." }
        DeclineReason: { type: string, description: "`Fail` only. A reason you can show the payer." }
        Display: { $ref: "#/components/schemas/Display" }
        Collect: { $ref: "#/components/schemas/Collect" }
        PayerUrl: { type: string, format: uri, description: "`WaitingAction` only: our page that shows the step to the payer." }
        AcsUrl: { type: string, description: "`3DS` only, direct API." }
        PaReq: { type: string }
        MD: { type: string }
        RedirectUrl: { type: string, description: "`3DSRedirect` only, direct API." }
        RedirectMethod: { type: string }

    SubscriptionWebhook:
      allOf:
        - $ref: "#/components/schemas/Subscription"
        - type: object
          properties:
            Event: { type: string, const: Subscription }
            EventId: { type: string }
            ShopId: { type: string, description: Your Shop ID. }
            DateTime: { type: string, format: date-time, description: "When the event happened, RFC 3339 UTC." }
            Type:
              type: string
              enum: [created, activated, cycle_paid, past_due, awaiting_card, recovered, attempt_failed, cycle_skipped, changed, paused, resumed, cancelled, completed]
            Details:
              type: object
              additionalProperties: true
              description: Event specifics, e.g. `CycleNo`, `TransactionId`, `DeclineCategory`, `NextAttemptAt`.
