openapi: 3.1.0
info:
  title: Payinto Business API
  version: v1
  summary: Server-to-server APIs for collections, payouts, customers, and bill payments.
  description: |
    The Payinto Business API lets your backend manage customers, issue virtual accounts,
    collect payments, send transfers, and pay bills.

    **Authentication.** Send your secret key on every request:
    `Authorization: Bearer sk_test_...`. Secret keys belong on your server only.

    **Environments.** Sandbox and production share one host. The key prefix selects the
    environment: `sk_test_` is sandbox and `sk_live_` is production.

    **Amounts.** All monetary values are integers in the lowest currency unit. For `NGN`,
    send kobo: `150000` is ₦1,500.00.
  contact:
    name: Payinto Developer Support
    url: https://docs.payinto.co/docs/support
  license:
    name: Proprietary
    url: https://payinto.co
servers:
  - url: https://api.payinto.co/api/v1/business
    description: Sandbox and production. Your API key prefix selects the environment.
security:
  - BusinessBearerAuth: []
tags:
  - name: Meta
    description: Confirm connectivity and inspect the identity behind your API key.
  - name: Accounts
    description: Read settlement accounts, balances, and account transaction history. Requires the `accounts` module.
  - name: Customers
    description: Create and maintain the customers you issue virtual accounts to. Requires the `customers` module.
  - name: Identity Verification
    description: Verify a customer's BVN or NIN, with or without an OTP challenge. Requires the `verification` module.
  - name: Virtual Accounts
    description: Issue static and dynamic virtual accounts and read the payments they receive.
  - name: Transactions
    description: Query the final status of a credit or debit transaction.
  - name: Transfers
    description: Resolve beneficiaries, quote fees, and send bank transfers. Requires the `transfers` module.
  - name: Bulk Transfers
    description: Create a draft, add and resolve recipients, then submit them together. Requires the `bulk_transfers` module.
  - name: Scheduled Bulk Transfers
    description: Run a resolved bulk transfer once or on a recurring schedule. Requires the `bulk_transfers` module.
  - name: VAS Catalog
    description: Discover value-added services, billers, and products. Requires the `vas` module.
  - name: VAS Payments
    description: Validate biller accounts and pay for airtime, data, cable TV, electricity, and betting. Requires the `vas` module.
  - name: OTP
    description: Send and validate one-time passwords. Requires the `otp` module.
  - name: Credit Check
    description: Retrieve credit bureau reports and their fees. Requires the `credit_check` module.
  - name: BVN Verification
    description: Look up a Bank Verification Number. Requires the `verification` module.
x-tagGroups:
  - name: Getting started
    tags: [Meta]
  - name: Accounts and customers
    tags: [Accounts, Customers, Identity Verification]
  - name: Collections
    tags: [Virtual Accounts, Transactions]
  - name: Payouts
    tags: [Transfers, Bulk Transfers, Scheduled Bulk Transfers]
  - name: Bill payments
    tags: [VAS Catalog, VAS Payments]
  - name: Add-ons
    tags: [OTP, Credit Check, BVN Verification]
paths:
  /:
    get:
      operationId: getBusinessApiStatus
      tags: [Meta]
      summary: Check API status
      description: Confirms the Business API is reachable and your secret key is accepted.
      responses:
        '200':
          description: The API is reachable.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          service: { type: string, examples: [payinto-api/business] }
                          version: { type: string, examples: ['1.0'] }
                          your-ip: { type: string, description: "The IP address Payinto sees. Add it to your key's allowlist." }
              example:
                status: success
                message: This endpoint is for Business API usage only.
                timestamp: 1793404800000
                data:
                  service: payinto-api/business
                  version: '1.0'
                  your-ip: 203.0.113.10
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /whoami:
    get:
      operationId: whoAmI
      tags: [Meta]
      summary: Identify the current key
      description: Returns the business and environment your API key resolves to. Use it to confirm you are on the environment you expect.
      responses:
        '200':
          description: The credentials were resolved.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          business_id: { type: string, format: uuid }
                          business_name: { type: string }
                          trade_name: { type: [string, 'null'] }
                          environment: { type: string, enum: [test, live] }
              example:
                status: success
                message: Business API credentials resolved successfully.
                timestamp: 1793404800000
                data:
                  business_id: 01964316-57cb-7284-8651-c53d3fcee694
                  business_name: Example Merchant Ltd
                  trade_name: Example Merchant
                  environment: test
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /transactions/tsq/{reference}:
    get:
      operationId: queryTransactionStatus
      tags: [Transactions]
      summary: Query transaction status
      description: |
        Looks up a credit or debit transaction by Payinto reference or merchant reference. Use this to confirm a final status.

        `amount` is the gross transaction amount and `settled_amount` is the net amount after deductions. For Checkout payments, use `GET /api/v1/checkout/tsq/{reference}` instead.
      parameters:
        - $ref: '#/components/parameters/ReferencePath'
      responses:
        '200':
          description: The transaction was found.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TransactionStatus' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /accounts:
    get:
      operationId: listAccounts
      tags: [Accounts]
      summary: List accounts
      description: Returns your accounts, excluding closed and blacklisted ones. This list is not paginated.
      responses:
        '200':
          description: Your accounts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Account' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /accounts/main:
    get:
      operationId: getMainAccount
      tags: [Accounts]
      summary: Get the main account
      description: Returns the account Payinto uses when you omit `debit_account_id` or `settlement_account_id`.
      responses:
        '200': { $ref: '#/components/responses/AccountResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /accounts/main/transactions:
    get:
      operationId: listMainAccountTransactions
      tags: [Accounts]
      summary: List main account transactions
      parameters:
        - $ref: '#/components/parameters/TransactionType'
        - $ref: '#/components/parameters/TransactionStatusFilter'
        - $ref: '#/components/parameters/FromDate'
        - $ref: '#/components/parameters/ToDate'
        - $ref: '#/components/parameters/PaginationType'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200': { $ref: '#/components/responses/TransactionPage' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /accounts/{id}:
    get:
      operationId: getAccount
      tags: [Accounts]
      summary: Get an account
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/AccountResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /accounts/{id}/transactions:
    get:
      operationId: listAccountTransactions
      tags: [Accounts]
      summary: List account transactions
      parameters:
        - $ref: '#/components/parameters/IdPath'
        - $ref: '#/components/parameters/TransactionType'
        - $ref: '#/components/parameters/TransactionStatusFilter'
        - $ref: '#/components/parameters/FromDate'
        - $ref: '#/components/parameters/ToDate'
        - $ref: '#/components/parameters/PaginationType'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200': { $ref: '#/components/responses/TransactionPage' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers:
    get:
      operationId: listCustomers
      tags: [Customers]
      summary: List customers
      description: Returns your customers, newest first. This list is not paginated.
      responses:
        '200':
          description: Your customers.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Customer' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers/individual:
    post:
      operationId: createIndividualCustomer
      tags: [Customers]
      summary: Create an individual customer
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateIndividualCustomerRequest' }
            example:
              first_name: Ada
              last_name: Example
              gender: female
              date_of_birth: '1990-05-14'
              email: ada@example.com
              phone_number: '08012345678'
              country_code: NG
              bvn: '22222222222'
              address:
                address_line1: 12 Example Street
                city: Ikeja
                state: Lagos
                country: NG
      responses:
        '200': { $ref: '#/components/responses/CustomerResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers/business:
    post:
      operationId: createBusinessCustomer
      tags: [Customers]
      summary: Create a business customer
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateBusinessCustomerRequest' }
            example:
              business_name: Example Stores Ltd
              business_registration_type: limited
              business_registration_number: RC123456
              business_registration_date: '2018-03-01'
              first_name: Ada
              last_name: Example
              gender: female
              date_of_birth: '1990-05-14'
              email: ada@example.com
              phone_number: '08012345678'
              country_code: NG
              bvn: '22222222222'
              address:
                address_line1: 12 Example Street
                city: Ikeja
                state: Lagos
                country: NG
      responses:
        '200': { $ref: '#/components/responses/CustomerResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers/{id}:
    get:
      operationId: getCustomer
      tags: [Customers]
      summary: Get a customer
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/CustomerResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    put:
      operationId: updateCustomer
      tags: [Customers]
      summary: Update a customer
      description: Send only the fields you want to change. To change `phone_number`, also send `country_code`.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateCustomerRequest' }
      responses:
        '200': { $ref: '#/components/responses/CustomerResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers/{id}/photo:
    post:
      operationId: uploadCustomerPhoto
      tags: [Customers]
      summary: Upload a customer photo
      description: Accepts a multipart file, a public image URL, or a base64 data URI. Supported formats are JPEG, PNG, and WebP, up to 10 MB. Image URLs must use the standard HTTP or HTTPS port and resolve to public addresses.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [photo]
              properties:
                photo: { type: string, format: binary }
          application/json:
            schema:
              type: object
              required: [photo]
              properties:
                photo: { type: string, description: "A public image URL or a `data:image/...;base64,` URI." }
      responses:
        '200': { $ref: '#/components/responses/CustomerResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers/{id}/identity/verify:
    post:
      operationId: verifyCustomerIdentity
      tags: [Identity Verification]
      summary: Verify identity without an OTP
      description: Verifies the BVN or NIN already stored on the customer. Use this when you do not need an OTP challenge.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type: { $ref: '#/components/schemas/IdentityType' }
                photo: { type: string, description: "Optional public image URL or `data:image/...;base64,` URI." }
            example:
              type: BVN
      responses:
        '200': { $ref: '#/components/responses/IdentityVerificationResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers/{id}/identity/initiate:
    post:
      operationId: initiateCustomerIdentity
      tags: [Identity Verification]
      summary: Start OTP identity verification
      description: Starts an OTP challenge. Keep the returned `reference` for the validate call.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type: { $ref: '#/components/schemas/IdentityType' }
            example:
              type: NIN
      responses:
        '200':
          description: The challenge was started.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
                        properties:
                          reference: { type: string, format: uuid }
                          type: { $ref: '#/components/schemas/IdentityType' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /customers/{id}/identity/validate:
    post:
      operationId: validateCustomerIdentity
      tags: [Identity Verification]
      summary: Complete OTP identity verification
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, reference, otp]
              properties:
                type: { $ref: '#/components/schemas/IdentityType' }
                reference: { type: string, format: uuid, description: "The reference from the initiate call." }
                otp: { type: string }
                photo: { type: string, description: "Optional public image URL or `data:image/...;base64,` URI." }
      responses:
        '200': { $ref: '#/components/responses/IdentityVerificationResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /virtual-accounts:
    get:
      operationId: listVirtualAccounts
      tags: [Virtual Accounts]
      summary: List virtual accounts
      description: Returns your virtual accounts, newest first. This list is not paginated. Requires the `static_virtual_accounts` or `dynamic_virtual_accounts` module.
      responses:
        '200':
          description: Your virtual accounts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/VirtualAccount' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /virtual-accounts/static:
    post:
      operationId: createStaticVirtualAccount
      tags: [Virtual Accounts]
      summary: Create a static virtual account
      description: |
        Issues a reusable account number for one customer. Requires the `static_virtual_accounts` module.

        A merchant reference is required, and the call is idempotent on it. See [idempotency](/docs/concepts/idempotency).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateStaticVirtualAccountRequest' }
            example:
              customer_id: 0196431a-2f10-7c55-9b20-3d7f6a1e4c11
              merchant_reference: customer-10001-va
              metadata:
                customer_tier: gold
      responses:
        '200': { $ref: '#/components/responses/VirtualAccountResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /virtual-accounts/dynamic:
    post:
      operationId: createDynamicVirtualAccount
      tags: [Virtual Accounts]
      summary: Create a dynamic virtual account
      description: |
        Issues a time-limited account number for one expected payment. Requires the `dynamic_virtual_accounts` module.

        A merchant reference is required, and the call is idempotent on it. See [idempotency](/docs/concepts/idempotency).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateDynamicVirtualAccountRequest' }
            example:
              amount: 150000
              amount_control: Fixed
              valid_for: 1800
              merchant_reference: order-10001
              metadata:
                order_id: '10001'
      responses:
        '200': { $ref: '#/components/responses/VirtualAccountResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /virtual-accounts/{id}:
    get:
      operationId: getVirtualAccount
      tags: [Virtual Accounts]
      summary: Get a virtual account
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/VirtualAccountResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /virtual-accounts/{id}/transactions:
    get:
      operationId: listVirtualAccountTransactions
      tags: [Virtual Accounts]
      summary: List virtual account payments
      description: Returns payments received by the virtual account, newest first. This list is not paginated, so narrow it with the date filters.
      parameters:
        - $ref: '#/components/parameters/IdPath'
        - $ref: '#/components/parameters/TransactionStatusFilter'
        - $ref: '#/components/parameters/FromDate'
        - $ref: '#/components/parameters/ToDate'
      responses:
        '200':
          description: Payments received by the virtual account.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          type: object
                          additionalProperties: true
                          description: A payment inflow record.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /virtual-accounts/transactions/{id}:
    get:
      operationId: getVirtualAccountTransaction
      tags: [Virtual Accounts]
      summary: Get a virtual account payment
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/PaymentInflowResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /virtual-accounts/transactions/reference/{reference}:
    get:
      operationId: getVirtualAccountTransactionByReference
      tags: [Virtual Accounts]
      summary: Get a virtual account payment by reference
      parameters:
        - $ref: '#/components/parameters/ReferencePath'
      responses:
        '200': { $ref: '#/components/responses/PaymentInflowResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /transfers/banks:
    get:
      operationId: listBanks
      tags: [Transfers]
      summary: List banks
      description: Returns the banks you can send transfers to. Use `code` as `bank_code` in other requests.
      responses:
        '200':
          description: Supported banks.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Bank' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /transfers/resolve-account:
    post:
      operationId: resolveBankAccount
      tags: [Transfers]
      summary: Resolve a bank account
      description: Confirms the account name before you send money. Pass the returned `reference` as `resolved_account_reference` when you initiate the transfer.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bank_code, account_number]
              properties:
                debit_account_id: { $ref: '#/components/schemas/DebitAccountId' }
                bank_code: { type: string, examples: ['058'] }
                account_number: { type: string, pattern: '^[0-9]{10}$', examples: ['0123456789'] }
            example:
              bank_code: '058'
              account_number: '0123456789'
      responses:
        '200':
          description: The account was resolved.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Beneficiary' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /transfers/resolve-recipient:
    post:
      operationId: resolvePayintoTag
      tags: [Transfers]
      summary: Resolve a Payinto Tag
      description: |
        Resolves a case-insensitive Payinto Tag to bank details. No money moves.

        Use the returned `resolved_account_reference`, `bank_code`, and `account_number` with `POST /transfers`, and keep the same debit account throughout. Sandbox keys resolve sandbox recipients only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [recipient]
              properties:
                recipient:
                  type: string
                  maxLength: 64
                  description: A tag, with or without `@`. Tags have 3 to 20 letters, digits, or underscores and start with a letter.
                  examples: ['@tag_recipient']
                debit_account_id: { $ref: '#/components/schemas/DebitAccountId' }
      responses:
        '200':
          description: The tag was resolved.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        allOf:
                          - $ref: '#/components/schemas/Beneficiary'
                          - type: object
                            properties:
                              mode: { type: string, enum: [payinto_tag] }
                              resolved_account_reference: { type: string }
                              display_value: { type: string, examples: ['@tag_recipient'] }
                              username: { type: string, examples: [tag_recipient] }
                              display_tag: { type: string, examples: ['@tag_recipient'] }
                              business_name: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /transfers/fee:
    post:
      operationId: quoteTransferFee
      tags: [Transfers]
      summary: Quote a transfer fee
      description: Returns the fee, VAT, stamp duty, and total debit for a proposed transfer.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                debit_account_id: { $ref: '#/components/schemas/DebitAccountId' }
                amount: { $ref: '#/components/schemas/MinorAmount' }
                currency: { type: string, minLength: 3, maxLength: 3, examples: [NGN], description: "Defaults to the debit account's currency." }
                beneficiary_bank_code: { type: string, examples: ['058'] }
            example:
              amount: 250000
              currency: NGN
              beneficiary_bank_code: '058'
      responses:
        '200':
          description: The fee was calculated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TransferFee' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /transfers:
    post:
      operationId: initiateTransfer
      tags: [Transfers]
      summary: Initiate a transfer
      description: |
        Sends money from your account to a resolved beneficiary. A success response means Payinto accepted the request, so confirm the final outcome with [transaction status](#tag/transactions/GET/transactions/tsq/{reference}) or a webhook.

        Send a `merchant_reference` so you can retry safely. See [idempotency](/docs/concepts/idempotency).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InitiateTransferRequest' }
            example:
              resolved_account_reference: 0196431c-6e02-7a44-8d1f-52b1c9e07a33
              beneficiary_bank_code: '058'
              beneficiary_account_number: '0123456789'
              amount: 250000
              narration: Vendor settlement
              merchant_reference: payout-10002
      responses:
        '200':
          description: The transfer was accepted. A replay carries the `X-Idempotency-Replay` header.
          headers:
            X-Idempotency-Replay: { $ref: '#/components/headers/IdempotencyReplay' }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Transfer' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers:
    get:
      operationId: listBulkTransfers
      tags: [Bulk Transfers]
      summary: List bulk transfers
      description: Returns 30 records per page, newest first. The records are at `data.data`.
      parameters:
        - $ref: '#/components/parameters/Page'
        - name: status
          in: query
          description: Filter by status.
          schema: { $ref: '#/components/schemas/BulkTransferStatus' }
        - name: scheduled_payment_id
          in: query
          description: Return only the runs created by this schedule.
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: A page of bulk transfers.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        allOf:
                          - $ref: '#/components/schemas/LaravelPage'
                          - type: object
                            properties:
                              data:
                                type: array
                                items: { $ref: '#/components/schemas/BulkTransfer' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      operationId: createBulkTransfer
      tags: [Bulk Transfers]
      summary: Create a bulk transfer draft
      description: Send a `merchant_reference` so you can retry safely. A repeat returns the original draft with a `200` status. See [idempotency](/docs/concepts/idempotency).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateBulkTransferRequest' }
            example:
              account_id: 01964316-57cb-7284-8651-c53d3fcee694
              name: October vendor payouts
              merchant_reference: payouts-2026-10
      responses:
        '200': { $ref: '#/components/responses/BulkTransferReplay' }
        '201': { $ref: '#/components/responses/BulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/template:
    get:
      operationId: downloadBulkTransferTemplate
      tags: [Bulk Transfers]
      summary: Download the import template
      description: Returns a CSV with the columns the import endpoints accept. Amounts in an import file are in naira, not kobo.
      responses:
        '200':
          description: The CSV template.
          content:
            text/csv:
              schema: { type: string }
              example: |
                account_number,bank_name,amount,narration
                0123456789,GTBank,25000,Example payment
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}:
    get:
      operationId: getBulkTransfer
      tags: [Bulk Transfers]
      summary: Get a bulk transfer
      description: Returns the bulk transfer with each recipient and their payment attempts.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/BulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    patch:
      operationId: updateBulkTransfer
      tags: [Bulk Transfers]
      summary: Update a bulk transfer draft
      description: Only `draft` and `ready` bulk transfers can be edited.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateBulkTransferRequest' }
      responses:
        '200': { $ref: '#/components/responses/BulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      operationId: deleteBulkTransfer
      tags: [Bulk Transfers]
      summary: Delete a bulk transfer draft
      description: Only drafts can be deleted.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/SimpleSuccess' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}/items:
    post:
      operationId: addBulkTransferItem
      tags: [Bulk Transfers]
      summary: Add a recipient
      description: Adds one recipient to a draft. A bank code and account number can appear only once per bulk transfer.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateBulkTransferItemRequest' }
            example:
              bank_code: '058'
              bank_name: Guaranty Trust Bank
              account_number: '0123456789'
              amount: 250000
              narration: October invoice
      responses:
        '201': { $ref: '#/components/responses/BulkTransferItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}/import:
    post:
      operationId: importBulkTransferRecipients
      tags: [Bulk Transfers]
      summary: Import recipients from a file
      description: |
        Adds the valid rows from a CSV or XLSX file of up to 5 MB to a draft, and reports each invalid row.

        **Amounts in the file are in naira, not kobo.** `2500` means ₦2,500.00. Imported recipients still need to be resolved.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: '#/components/schemas/BulkTransferImportFile' }
      responses:
        '200':
          description: The import was processed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          created: { type: integer, description: "The number of recipients added." }
                          duplicates: { type: integer, description: "The number of recipients flagged as duplicates." }
                          errors:
                            type: array
                            items:
                              type: object
                              properties:
                                row: { type: integer, description: "The row number in the file." }
                                message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}/import/preview:
    post:
      operationId: previewBulkTransferImport
      tags: [Bulk Transfers]
      summary: Preview an import
      description: Checks a file without changing the draft. Amounts in the response are in kobo.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: '#/components/schemas/BulkTransferImportFile' }
      responses:
        '200':
          description: The preview.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          valid_count: { type: integer }
                          error_count: { type: integer }
                          rows:
                            type: array
                            items:
                              type: object
                              properties:
                                row: { type: integer }
                                account_number: { type: string }
                                bank_code: { type: [string, 'null'] }
                                bank_name: { type: string }
                                amount: { type: integer }
                                narration: { type: [string, 'null'] }
                                duplicate: { type: boolean }
                                errors:
                                  type: array
                                  items: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}/duplicate:
    post:
      operationId: duplicateBulkTransfer
      tags: [Bulk Transfers]
      summary: Duplicate a bulk transfer
      description: Creates a new draft with the same recipients. Each recipient must be resolved again.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '201': { $ref: '#/components/responses/BulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/items/{itemId}:
    patch:
      operationId: updateBulkTransferItem
      tags: [Bulk Transfers]
      summary: Update a recipient
      description: Only recipients of a draft can change. Changing the bank or account number means the recipient must be resolved again.
      parameters:
        - $ref: '#/components/parameters/ItemIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateBulkTransferItemRequest' }
      responses:
        '200': { $ref: '#/components/responses/BulkTransferItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      operationId: deleteBulkTransferItem
      tags: [Bulk Transfers]
      summary: Remove a recipient
      parameters:
        - $ref: '#/components/parameters/ItemIdPath'
      responses:
        '200': { $ref: '#/components/responses/SimpleSuccess' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/items/{itemId}/resolve:
    post:
      operationId: resolveBulkTransferItem
      tags: [Bulk Transfers]
      summary: Resolve a recipient
      description: Confirms the recipient's account name. Every recipient must be resolved before you submit or schedule.
      parameters:
        - $ref: '#/components/parameters/ItemIdPath'
      responses:
        '200': { $ref: '#/components/responses/BulkTransferItemResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}/submit:
    post:
      operationId: submitBulkTransfer
      tags: [Bulk Transfers]
      summary: Submit a bulk transfer
      description: |
        Queues the bulk transfer for processing. The available balance must cover the requested amount and expected fees.

        The status becomes `queued`, or `awaiting_approval` if your business uses an approval workflow. Submitting again is safe: it returns the current state with the `X-Idempotency-Replay` header and doesn't pay anyone twice.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/BulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}/retry:
    post:
      operationId: retryBulkTransfer
      tags: [Bulk Transfers]
      summary: Retry failed recipients
      description: Queues failed recipients for another attempt. Recipients who were already paid aren't included. The status must be `failed` or `partially_successful`.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/BulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /bulk-transfers/{id}/export:
    get:
      operationId: exportBulkTransfer
      tags: [Bulk Transfers]
      summary: Export recipients as CSV
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: A CSV file with one row per recipient.
          content:
            text/csv:
              schema: { type: string }
              example: |
                bulk_reference,item_reference,bank,account_number,amount,status,failure_reason,attempt_count
                BTF01EXAMPLE,BTF01EXAMPLE-I1,Guaranty Trust Bank,0123456789,250000,successful,,1
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /scheduled-bulk-transfers:
    get:
      operationId: listScheduledBulkTransfers
      tags: [Scheduled Bulk Transfers]
      summary: List schedules
      description: Returns 30 records per page, most recently updated first. The records are at `data.data`.
      parameters:
        - $ref: '#/components/parameters/Page'
        - name: status
          in: query
          description: Filter by status.
          schema: { $ref: '#/components/schemas/ScheduleStatus' }
      responses:
        '200':
          description: A page of schedules.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        allOf:
                          - $ref: '#/components/schemas/LaravelPage'
                          - type: object
                            properties:
                              data:
                                type: array
                                items: { $ref: '#/components/schemas/ScheduledBulkTransfer' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      operationId: createScheduledBulkTransfer
      tags: [Scheduled Bulk Transfers]
      summary: Create a schedule
      description: |
        Copies the recipients of a bulk transfer into a schedule. Every recipient must be resolved, and the first run must be at least 10 minutes away.

        Keep the bulk transfer you schedule from, because live runs use it as a template. The response is `202` with the status `pending_approval` if your business requires approval.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateScheduledBulkTransferRequest' }
            example:
              source_account_id: 01964316-57cb-7284-8651-c53d3fcee694
              bulk_transfer_id: 0196431d-7a10-7e22-9c41-6b2d8f30c955
              merchant_reference: monthly-vendors
              schedule:
                frequency: monthly
                interval: 1
                starts_at: '2026-12-01T09:00:00+01:00'
                timezone: Africa/Lagos
                ends_type: occurrences
                max_occurrences: 12
      responses:
        '200': { $ref: '#/components/responses/ScheduledBulkTransferReplay' }
        '201': { $ref: '#/components/responses/ScheduledBulkTransferResponse' }
        '202': { $ref: '#/components/responses/ScheduledBulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /scheduled-bulk-transfers/{id}:
    get:
      operationId: getScheduledBulkTransfer
      tags: [Scheduled Bulk Transfers]
      summary: Get a schedule
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/ScheduledBulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    patch:
      operationId: updateScheduledBulkTransfer
      tags: [Scheduled Bulk Transfers]
      summary: Update a schedule
      description: Only `active` and `paused` schedules can be edited, and the next run must be in the future. If your business requires approval, an edit can return the schedule to `pending_approval`.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateScheduledBulkTransferRequest' }
      responses:
        '200': { $ref: '#/components/responses/ScheduledBulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /scheduled-bulk-transfers/{id}/executions:
    get:
      operationId: listScheduledBulkTransferExecutions
      tags: [Scheduled Bulk Transfers]
      summary: List a schedule's runs
      description: Returns 30 records per page, newest first. Use `bulk_transfer_id` to read each recipient's outcome.
      parameters:
        - $ref: '#/components/parameters/IdPath'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: A page of runs.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        allOf:
                          - $ref: '#/components/schemas/LaravelPage'
                          - type: object
                            properties:
                              data:
                                type: array
                                items: { $ref: '#/components/schemas/ScheduledBulkTransferExecution' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /scheduled-bulk-transfers/{id}/pause:
    post:
      operationId: pauseScheduledBulkTransfer
      tags: [Scheduled Bulk Transfers]
      summary: Pause a schedule
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/ScheduledBulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /scheduled-bulk-transfers/{id}/resume:
    post:
      operationId: resumeScheduledBulkTransfer
      tags: [Scheduled Bulk Transfers]
      summary: Resume a schedule
      description: Resumes from the next future run. A one-time schedule whose date has passed becomes `completed` instead.
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/ScheduledBulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /scheduled-bulk-transfers/{id}/cancel:
    post:
      operationId: cancelScheduledBulkTransfer
      tags: [Scheduled Bulk Transfers]
      summary: Cancel a schedule
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200': { $ref: '#/components/responses/ScheduledBulkTransferResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/services:
    get:
      operationId: listVasServices
      tags: [VAS Catalog]
      summary: List services
      responses:
        '200':
          description: Available services.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/VasService' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/services/{id}:
    get:
      operationId: getVasService
      tags: [VAS Catalog]
      summary: Get a service
      parameters:
        - $ref: '#/components/parameters/ServiceIdPath'
      responses:
        '200':
          description: The service.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/VasService' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/services/{id}/billers:
    get:
      operationId: listVasServiceBillers
      tags: [VAS Catalog]
      summary: List a service's billers
      parameters:
        - $ref: '#/components/parameters/ServiceIdPath'
      responses:
        '200':
          description: Billers for the service.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/VasBiller' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/billers/{id}:
    get:
      operationId: getVasBiller
      tags: [VAS Catalog]
      summary: Get a biller
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: The biller.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/VasBiller' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/billers/{id}/products:
    get:
      operationId: listVasBillerProducts
      tags: [VAS Catalog]
      summary: List a biller's products
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: Products for the biller.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/VasProduct' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/products/{id}:
    get:
      operationId: getVasProduct
      tags: [VAS Catalog]
      summary: Get a product
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: The product.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/VasProduct' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/verify/cable-tv:
    post:
      operationId: verifyCableTv
      tags: [VAS Payments]
      summary: Validate a smart card
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [product_id, smart_card_number]
              properties:
                product_id: { type: string, format: uuid }
                smart_card_number: { type: string }
      responses:
        '200':
          description: The smart card was validated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          biller: { type: [string, object, 'null'] }
                          smart_card_number: { type: [string, 'null'] }
                          name: { type: [string, 'null'] }
                          status: { type: [string, 'null'] }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/verify/electricity:
    post:
      operationId: verifyElectricity
      tags: [VAS Payments]
      summary: Validate a meter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [product_id, meter_number]
              properties:
                product_id: { type: string, format: uuid }
                meter_number: { type: string }
      responses:
        '200':
          description: The meter was validated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          biller: { type: [string, object, 'null'] }
                          meter_number: { type: [string, 'null'] }
                          name: { type: [string, 'null'] }
                          address: { type: [string, 'null'] }
                          disco_code: { type: [string, 'null'] }
                          meter_type: { type: [string, 'null'], examples: [prepaid] }
                          min_amount: { type: [integer, 'null'], description: "Minimum vend amount in lowest units." }
                          max_amount: { type: [integer, 'null'], description: "Maximum vend amount in lowest units." }
                          debt_repayment: { type: [integer, 'null'] }
                          outstanding: { type: [integer, 'null'] }
                          outstanding_notice: { type: [string, 'null'] }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/pay/airtime:
    post:
      operationId: payAirtime
      tags: [VAS Payments]
      summary: Buy airtime
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/VasPaymentBase'
                - $ref: '#/components/schemas/VasPhoneRecipient'
      responses:
        '200': { $ref: '#/components/responses/VasPaymentResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/pay/data:
    post:
      operationId: payData
      tags: [VAS Payments]
      summary: Buy data
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/VasPaymentBase'
                - $ref: '#/components/schemas/VasPhoneRecipient'
      responses:
        '200': { $ref: '#/components/responses/VasPaymentResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/pay/cable-tv:
    post:
      operationId: payCableTv
      tags: [VAS Payments]
      summary: Pay for cable TV
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/VasPaymentBase'
                - type: object
                  required: [smart_card_number]
                  properties:
                    smart_card_number: { type: string }
      responses:
        '200': { $ref: '#/components/responses/VasPaymentResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/pay/electricity:
    post:
      operationId: payElectricity
      tags: [VAS Payments]
      summary: Pay for electricity
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/VasPaymentBase'
                - type: object
                  required: [meter_number]
                  properties:
                    meter_number: { type: string }
      responses:
        '200': { $ref: '#/components/responses/VasPaymentResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /vas/pay/betting:
    post:
      operationId: payBetting
      tags: [VAS Payments]
      summary: Fund a betting account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/VasPaymentBase'
                - type: object
                  required: [customer_id]
                  properties:
                    customer_id: { type: string, description: "The customer's ID at the betting provider." }
      responses:
        '200': { $ref: '#/components/responses/VasPaymentResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /addons/otps:
    post:
      operationId: createOtp
      tags: [OTP]
      summary: Send an OTP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sender, length, expiry, channel, recipient]
              properties:
                sender: { type: string, maxLength: 11, description: "The sender name shown to the recipient." }
                length: { type: integer, minimum: 4, maximum: 8 }
                expiry: { type: integer, minimum: 5, maximum: 60, description: "How long the OTP stays valid." }
                channel:
                  type: array
                  items: { type: string, enum: [sms, whatsapp, email] }
                recipient:
                  type: object
                  description: Send the phone fields for `sms` or `whatsapp`, and `email` for `email`.
                  properties:
                    phone_number: { type: string }
                    phone_number_country_code: { type: string, examples: [NG] }
                    email: { type: string, format: email }
            example:
              sender: Example
              length: 6
              expiry: 10
              channel: [sms]
              recipient:
                phone_number: '08012345678'
                phone_number_country_code: NG
      responses:
        '200':
          description: The OTP was sent.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
                        properties:
                          reference: { type: string }
                          message: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /addons/otps/{reference}/validate:
    post:
      operationId: validateOtp
      tags: [OTP]
      summary: Validate an OTP
      description: |
        Send the OTP reference in both the path and the body.

        **Known issue:** this endpoint currently works with sandbox keys only. Live validation does not complete. Contact support before building on it in production.
      parameters:
        - $ref: '#/components/parameters/ReferencePath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reference, otp]
              properties:
                reference: { type: string, format: uuid }
                otp: { type: string }
      responses:
        '200':
          description: The OTP was validated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /addons/credit-check:
    post:
      operationId: getCreditCheck
      tags: [Credit Check]
      summary: Get a credit report
      description: Send `bvn` when `type` is `individual`, or `business_registration_number` when `type` is `business`. Quote the fee first with the fee endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, credit_bureau]
              properties:
                type: { type: string, enum: [individual, business] }
                bvn: { type: string, maxLength: 20 }
                business_registration_number: { type: string, maxLength: 20 }
                credit_bureau: { $ref: '#/components/schemas/CreditBureau' }
            example:
              type: individual
              bvn: '22222222222'
              credit_bureau: CRC
      responses:
        '200':
          description: The credit report.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
                        description: The bureau report. Its shape depends on the bureau.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /addons/credit-check/fee:
    post:
      operationId: getCreditCheckFee
      tags: [Credit Check]
      summary: Quote a credit report fee
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, credit_bureau]
              properties:
                type: { type: string, enum: [individual, business] }
                credit_bureau: { $ref: '#/components/schemas/CreditBureau' }
      responses:
        '200':
          description: The fee.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /addons/bvn-verification:
    post:
      operationId: verifyBvn
      tags: [BVN Verification]
      summary: Verify a BVN
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bvn]
              properties:
                bvn: { type: string, pattern: '^[0-9]{11}$', examples: ['22222222222'] }
      responses:
        '200':
          description: The BVN details.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /addons/bvn-verification/fee:
    get:
      operationId: getBvnVerificationFee
      tags: [BVN Verification]
      summary: Get the BVN verification fee
      responses:
        '200':
          description: The fee.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        additionalProperties: true
                        properties:
                          amount: { type: integer }
                          currency: { type: string, examples: [NGN] }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
webhooks:
  checkout.payment.succeeded:
    post:
      operationId: webhookCheckoutPaymentSucceeded
      summary: Checkout payment succeeded
      description: Sent after a verified bank transfer or card checkout payment. Use this event to fulfill checkout orders. Process `data.id` or `data.reference` idempotently.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    data: { $ref: '#/components/schemas/CheckoutPaymentEvent' }
      responses:
        '200': { description: "Return any 2xx status to acknowledge receipt." }
  virtual_account.transfer.received:
    post:
      operationId: webhookVirtualAccountTransferReceived
      summary: Virtual account transfer received
      description: Sent when a virtual account deposit is credited, including checkout bank transfers.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    data: { $ref: '#/components/schemas/PaymentInflow' }
      responses:
        '200': { description: "Return any 2xx status to acknowledge receipt." }
  transaction.credit.succeeded:
    post:
      operationId: webhookTransactionCreditSucceeded
      summary: Transaction credit succeeded
      description: Sent when a credit is recorded on one of your accounts.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    data: { $ref: '#/components/schemas/Transaction' }
      responses:
        '200': { description: "Return any 2xx status to acknowledge receipt." }
  transaction.debit.succeeded:
    post:
      operationId: webhookTransactionDebitSucceeded
      summary: Transaction debit succeeded
      description: Sent when a debit is recorded on one of your accounts. This confirms the debit, not that the external transfer or bill payment completed.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    data: { $ref: '#/components/schemas/Transaction' }
      responses:
        '200': { description: "Return any 2xx status to acknowledge receipt." }
  bulk_transfer.finished:
    post:
      operationId: webhookBulkTransferFinished
      summary: Bulk transfer finished
      description: Sent when every recipient has reached a final status. `data.status` is `completed`, `partially_successful`, or `failed`. A retry sends another event when it finishes. Scheduled runs include `scheduled_payment_id`.
      security: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    data: { $ref: '#/components/schemas/BulkTransferFinishedEvent' }
      responses:
        '200': { description: Return any 2xx status to acknowledge receipt. }
components:
  securitySchemes:
    BusinessBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: Your secret key, such as `sk_test_...`. Use a sandbox key in this playground. Public `pk_` keys are rejected.
  headers:
    IdempotencyReplay:
      description: Present and set to `true` when Payinto returned the original result for a repeated `merchant_reference`.
      schema: { type: string, enum: ['true'] }
  parameters:
    IdPath:
      name: id
      in: path
      required: true
      description: The resource ID.
      schema: { type: string, format: uuid }
    ItemIdPath:
      name: itemId
      in: path
      required: true
      description: The recipient ID.
      schema: { type: string, format: uuid }
    ServiceIdPath:
      name: id
      in: path
      required: true
      description: The service ID or code, such as `airtime` or `cable-tv`.
      schema: { type: string, examples: [airtime] }
    ReferencePath:
      name: reference
      in: path
      required: true
      description: The reference to look up.
      schema: { type: string }
    TransactionType:
      name: type
      in: query
      description: Filter by direction.
      schema: { type: string, enum: [credit, debit] }
    TransactionStatusFilter:
      name: status
      in: query
      description: Filter by status.
      schema: { type: string, enum: [pending, successful, failed, reversed] }
    FromDate:
      name: from_date
      in: query
      description: Include records created on or after this date.
      schema: { type: string, format: date }
    ToDate:
      name: to_date
      in: query
      description: Include records created on or before this date.
      schema: { type: string, format: date }
    PaginationType:
      name: pagination_type
      in: query
      description: Use `cursor` for stable traversal of long histories.
      schema: { type: string, enum: [offset, cursor], default: offset }
    PerPage:
      name: per_page
      in: query
      description: Records per page, up to 1000.
      schema: { type: integer, default: 100, maximum: 1000 }
    Page:
      name: page
      in: query
      description: The page number for offset pagination.
      schema: { type: integer, default: 1, minimum: 1 }
    Cursor:
      name: cursor
      in: query
      description: The `next_cursor` or `prev_cursor` from the previous response.
      schema: { type: string }
    WebhookId:
      name: X-Payinto-Webhook-Id
      in: header
      required: true
      description: The unique webhook delivery ID. It stays the same across retries.
      schema: { type: string }
    WebhookEvent:
      name: X-Payinto-Event
      in: header
      required: true
      description: The event name.
      schema: { type: string }
    WebhookTimestamp:
      name: X-Payinto-Timestamp
      in: header
      required: true
      description: The Unix timestamp in seconds of this delivery attempt.
      schema: { type: integer }
    WebhookSignature:
      name: X-Payinto-Signature
      in: header
      required: true
      description: 'The signature in the format `t={timestamp},v1={hmac_sha256_hex}`. The signed content is `{timestamp}.{raw_request_body}`.'
      schema: { type: string }
  responses:
    SimpleSuccess:
      description: The request succeeded.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
    AccountResponse:
      description: The account.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/Account' }
    TransactionPage:
      description: A page of transactions.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Transaction' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
    CustomerResponse:
      description: The customer.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/Customer' }
    IdentityVerificationResponse:
      description: The verification result.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/IdentityVerification' }
    VirtualAccountResponse:
      description: The virtual account. A replay carries the `X-Idempotency-Replay` header.
      headers:
        X-Idempotency-Replay: { $ref: '#/components/headers/IdempotencyReplay' }
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/VirtualAccount' }
    PaymentInflowResponse:
      description: The payment.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/PaymentInflow' }
    BulkTransferResponse:
      description: The bulk transfer.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/BulkTransfer' }
    BulkTransferReplay:
      description: The original draft, returned for a repeated `merchant_reference`.
      headers:
        X-Idempotency-Replay: { $ref: '#/components/headers/IdempotencyReplay' }
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/BulkTransfer' }
    BulkTransferItemResponse:
      description: The recipient.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/BulkTransferItem' }
    ScheduledBulkTransferResponse:
      description: The schedule.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/ScheduledBulkTransfer' }
    ScheduledBulkTransferReplay:
      description: The original schedule, returned for a repeated `merchant_reference`.
      headers:
        X-Idempotency-Replay: { $ref: '#/components/headers/IdempotencyReplay' }
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/ScheduledBulkTransfer' }
    VasPaymentResponse:
      description: The payment was accepted. A replay carries the `X-Idempotency-Replay` header.
      headers:
        X-Idempotency-Replay: { $ref: '#/components/headers/IdempotencyReplay' }
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data: { $ref: '#/components/schemas/VasPayment' }
    BadRequest:
      description: The request could not be completed. Many lookups also return this status when a record does not exist.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            status: error
            message: Customer not found.
            timestamp: 1793404800000
    Unauthorized:
      description: The key is missing, invalid, or expired.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            status: error
            message: Invalid API Key
            timestamp: 1793404800000
    Forbidden:
      description: The key is valid but not allowed, because of its IP allowlist or a disabled environment or module.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            ipNotAllowed:
              summary: IP address is not on the allowlist
              value: { status: error, message: IP address not allowed, timestamp: 1793404800000 }
            moduleDisabled:
              summary: Module is not enabled
              value: { status: error, message: Transfer API access is not enabled for this business API integration., timestamp: 1793404800000 }
    RateLimited:
      description: You sent more than 60 requests in a minute. Wait for the number of seconds in `Retry-After`.
      headers:
        Retry-After:
          description: Seconds to wait before you retry.
          schema: { type: integer, examples: [42] }
        X-RateLimit-Limit:
          description: Requests allowed per minute.
          schema: { type: integer, examples: [60] }
        X-RateLimit-Remaining:
          description: Requests left in the current window.
          schema: { type: integer, examples: [0] }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            status: error
            message: 'Too many attempts, please try again later.'
            timestamp: 1793404800000
    NotFound:
      description: The record does not exist for this business and environment.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            status: error
            message: Resource item not found.
            timestamp: 1793404800000
    Conflict:
      description: This `merchant_reference` was already used with a different payload.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            status: error
            message: Duplicate merchant reference with a different payload.
            timestamp: 1793404800000
    ValidationError:
      description: One or more fields are invalid.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ValidationError' }
          example:
            status: error
            message: One or more of the given data has an error.
            errors:
              account_number: [The account number field must be 10 digits.]
            timestamp: 1793404800000
  schemas:
    MinorAmount:
      type: integer
      description: The amount in the lowest currency unit. For NGN, this is kobo.
      examples: [150000]
    DebitAccountId:
      type: [string, 'null']
      format: uuid
      description: The account to debit. Defaults to your main account.
    IdentityType:
      type: string
      enum: [BVN, NIN]
    CreditBureau:
      type: string
      enum: [CRC, CRC_PREMIUM, CREDIT_REGISTRY, CREDIT_REGISTRY_PREMIUM, FIRST_CENTRAL, FIRST_CENTRAL_PREMIUM]
    Envelope:
      type: object
      required: [status, message, timestamp]
      properties:
        status: { type: string, enum: [success] }
        message: { type: string }
        timestamp: { type: integer, description: "Server time in Unix milliseconds." }
        data: {}
    Error:
      type: object
      required: [status, message]
      properties:
        status: { type: string, enum: [error] }
        message: { type: string }
        timestamp: { type: integer, description: "Server time in Unix milliseconds." }
    ValidationError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          required: [errors]
          properties:
            errors:
              type: object
              description: Field names mapped to their error messages.
              additionalProperties:
                type: array
                items: { type: string }
    Pagination:
      type: object
      properties:
        type: { type: string, enum: [offset, cursor] }
        meta:
          type: object
          description: Cursor pagination returns `next_cursor`, `prev_cursor`, `next_page_url`, and `prev_page_url`. Offset pagination returns the other fields.
          properties:
            next_cursor: { type: [string, 'null'] }
            prev_cursor: { type: [string, 'null'] }
            per_page: { type: integer }
            has_more_pages: { type: boolean }
            total: { type: integer }
            current_page: { type: integer }
            first_page: { type: [integer, 'null'] }
            last_page: { type: integer }
            next_page_url: { type: [string, 'null'] }
            prev_page_url: { type: [string, 'null'] }
            first_page_url: { type: [string, 'null'] }
            last_page_url: { type: [string, 'null'] }
    LaravelPage:
      type: object
      description: A page of results.
      properties:
        current_page: { type: integer }
        data:
          type: array
          items: { type: object, additionalProperties: true }
        first_page_url: { type: [string, 'null'] }
        from: { type: [integer, 'null'] }
        last_page: { type: integer }
        last_page_url: { type: [string, 'null'] }
        next_page_url: { type: [string, 'null'] }
        path: { type: string }
        per_page: { type: integer, examples: [30] }
        prev_page_url: { type: [string, 'null'] }
        to: { type: [integer, 'null'] }
        total: { type: integer }
    Account:
      type: object
      properties:
        id: { type: string, format: uuid }
        virtual_account_id: { type: [string, 'null'] }
        is_main: { type: boolean }
        isMain: { type: boolean, description: "Alias of `is_main`." }
        label: { type: [string, 'null'] }
        currency: { type: string, examples: [NGN] }
        available_balance: { type: integer, description: "Spendable balance in lowest units." }
        ledger_balance: { type: integer, description: "Ledger balance in lowest units." }
        service_usage_reserve: { type: integer, description: "Reserved balance in lowest units." }
        balance: { type: integer, description: "Balance in lowest units." }
        status: { type: string, enum: [active, restricted, suspended, closed, blacklisted, frozen, dormant] }
        virtual_account: { oneOf: [{ $ref: '#/components/schemas/VirtualAccount' }, { type: 'null' }] }
        virtual_accounts:
          type: array
          items: { $ref: '#/components/schemas/VirtualAccount' }
        static_virtual_accounts:
          type: array
          items: { $ref: '#/components/schemas/VirtualAccount' }
        created_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
        deleted_at: { type: [string, 'null'], format: date-time }
    Transaction:
      type: object
      properties:
        id: { type: string }
        parent_id: { type: [string, 'null'] }
        account_id: { type: [string, 'null'] }
        account_label: { type: [string, 'null'] }
        source_type: { type: [string, 'null'] }
        source_id: { type: [string, 'null'] }
        created_by_type: { type: [string, 'null'] }
        created_by_id: { type: [string, 'null'] }
        reference: { type: string }
        merchant_reference: { type: [string, 'null'] }
        session_id: { type: [string, 'null'] }
        type: { type: string, enum: [credit, debit] }
        currency: { type: string, examples: [NGN] }
        amount: { type: integer }
        fee: { type: integer }
        vat: { type: integer }
        total_fee: { type: integer }
        settled_amount: { type: integer }
        narration: { type: [string, 'null'] }
        category: { type: [string, 'null'] }
        isFee: { type: boolean }
        status: { type: string, enum: [pending, successful, failed, reversed] }
        settlement_status: { type: [string, 'null'] }
        provider_name: { type: [string, 'null'] }
        biller_logo_url: { type: [string, 'null'] }
        meter_number: { type: [string, 'null'] }
        meter_type: { type: [string, 'null'] }
        utility_token: { type: [string, 'null'] }
        utility_units: { type: [string, number, 'null'] }
        utility_units_display: { type: [string, 'null'] }
        customer_name: { type: [string, 'null'] }
        customer_address: { type: [string, 'null'] }
        receipt_details: { type: [object, 'null'], additionalProperties: true }
        settled_at: { type: [string, 'null'], format: date-time }
        created_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
        deleted_at: { type: [string, 'null'], format: date-time }
    TransactionStatus:
      type: object
      properties:
        id: { type: string }
        reference: { type: string, examples: [PIF01EXAMPLE] }
        merchant_reference: { type: [string, 'null'], examples: [MERCHANT-001] }
        type: { type: string, enum: [credit, debit] }
        status: { type: string, enum: [pending, successful, failed, reversed] }
        amount: { type: integer, description: "Gross amount in lowest units.", examples: [21075] }
        currency: { type: string, examples: [NGN] }
        fee: { type: integer, examples: [1000] }
        vat: { type: integer, examples: [75] }
        stamp_duty: { type: integer, examples: [0] }
        total_fee: { type: integer, examples: [1075] }
        settled_amount: { type: integer, description: "Net amount in lowest units.", examples: [20000] }
        account_id: { type: [string, 'null'] }
        base_amount: { type: integer, description: "Checkout transactions only. The original order amount." }
        customer_charge_amount: { type: integer, description: "Checkout transactions only. The extra amount added to the customer's total." }
        customer_fee_amount: { type: integer, description: "Checkout transactions only. Alias of `customer_charge_amount`." }
        charges_applied: { type: boolean, description: "Checkout transactions only." }
        created_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
        settled_at: { type: [string, 'null'], format: date-time }
    Customer:
      type: object
      properties:
        id: { type: string, format: uuid }
        business_id: { type: [string, 'null'] }
        type: { type: string, enum: [individual, business] }
        business_name: { type: [string, 'null'], description: "Business customers only." }
        business_registration_type: { type: [string, 'null'], description: "Business customers only." }
        business_registration_number: { type: [string, 'null'], description: "Business customers only." }
        business_registration_date: { type: [string, 'null'], description: "Business customers only." }
        business_tin: { type: [string, 'null'], description: "Business customers only." }
        first_name: { type: [string, 'null'] }
        middle_name: { type: [string, 'null'] }
        last_name: { type: [string, 'null'] }
        maiden_name: { type: [string, 'null'] }
        email: { type: [string, 'null'] }
        phone_number: { type: [string, 'null'], description: "E.164 format." }
        gender: { type: [string, 'null'] }
        date_of_birth: { type: [string, 'null'] }
        nationality: { type: [string, 'null'] }
        identification_type: { type: [string, 'null'] }
        identification_number: { type: [string, 'null'] }
        identification_expiry_date: { type: [string, 'null'] }
        nin: { type: [string, 'null'] }
        nin_verified: { type: boolean }
        nin_verified_at: { type: [string, 'null'] }
        bvn: { type: [string, 'null'] }
        bvn_verified: { type: boolean }
        bvn_verified_at: { type: [string, 'null'] }
        address_line1: { type: [string, 'null'] }
        address_line2: { type: [string, 'null'] }
        city: { type: [string, 'null'] }
        state: { type: [string, 'null'] }
        postal_code: { type: [string, 'null'] }
        country: { type: [string, 'null'] }
        address_verified: { type: boolean }
        address_verified_at: { type: [string, 'null'] }
        photo_url: { type: [string, 'null'] }
        status: { type: [string, 'null'] }
        kyc_tier: { type: [string, number, 'null'] }
        is_verified: { type: boolean }
        verified_at: { type: [string, 'null'] }
        metadata: { type: [object, 'null'], additionalProperties: true }
        created_at: { type: [string, 'null'] }
        updated_at: { type: [string, 'null'] }
        deleted_at: { type: [string, 'null'] }
    CustomerAddress:
      type: object
      required: [address_line1, city, state, country]
      properties:
        address_line1: { type: string, maxLength: 255 }
        address_line2: { type: [string, 'null'], maxLength: 255 }
        city: { type: string, maxLength: 100 }
        state: { type: string, maxLength: 50 }
        postal_code: { type: [string, 'null'], maxLength: 20 }
        country: { type: string, maxLength: 2, description: "ISO 3166-1 alpha-2 code." }
    CustomerMetadata:
      type: [object, 'null']
      description: Your own key-value data. Values are strings of up to 255 characters.
      additionalProperties: { type: string, maxLength: 255 }
    CreateIndividualCustomerRequest:
      type: object
      required: [first_name, last_name, gender, date_of_birth, email, phone_number, country_code, bvn, address]
      properties:
        first_name: { type: string, maxLength: 50 }
        middle_name: { type: [string, 'null'], maxLength: 50 }
        last_name: { type: string, maxLength: 50 }
        gender: { type: string, enum: [male, female] }
        date_of_birth: { type: string, format: date }
        email: { type: string, format: email, maxLength: 191 }
        phone_number: { type: string, maxLength: 20, description: "Must be valid for `country_code`. Stored in E.164 format." }
        country_code: { type: string, maxLength: 2, examples: [NG] }
        bvn: { type: string, maxLength: 20 }
        nin: { type: [string, 'null'], maxLength: 20 }
        address: { $ref: '#/components/schemas/CustomerAddress' }
        metadata: { $ref: '#/components/schemas/CustomerMetadata' }
    CreateBusinessCustomerRequest:
      allOf:
        - $ref: '#/components/schemas/CreateIndividualCustomerRequest'
        - type: object
          required: [business_name, business_registration_type, business_registration_number, business_registration_date]
          properties:
            business_name: { type: string, maxLength: 255 }
            business_registration_type: { type: string, enum: [limited, sole_proprietorship, partnership] }
            business_registration_number: { type: string, maxLength: 50 }
            business_registration_date: { type: string, format: date }
    UpdateCustomerRequest:
      type: object
      properties:
        business_name: { type: [string, 'null'], maxLength: 255 }
        business_registration_type: { type: [string, 'null'], enum: [limited, sole_proprietorship, partnership, null] }
        business_registration_number: { type: [string, 'null'], maxLength: 50 }
        business_registration_date: { type: [string, 'null'], format: date }
        first_name: { type: [string, 'null'], maxLength: 50 }
        middle_name: { type: [string, 'null'], maxLength: 50 }
        last_name: { type: [string, 'null'], maxLength: 50 }
        gender: { type: [string, 'null'], enum: [male, female, null] }
        date_of_birth: { type: [string, 'null'], format: date }
        email: { type: [string, 'null'], format: email, maxLength: 191 }
        phone_number: { type: [string, 'null'], maxLength: 20 }
        country_code: { type: [string, 'null'], maxLength: 2 }
        nin: { type: [string, 'null'], maxLength: 20 }
        bvn: { type: [string, 'null'], maxLength: 20 }
        metadata: { $ref: '#/components/schemas/CustomerMetadata' }
    IdentityVerification:
      type: object
      additionalProperties: true
      description: The fields returned depend on the identity type and the registry response.
      properties:
        verification_completed: { type: boolean }
        reference: { type: string }
        message: { type: string }
        type: { $ref: '#/components/schemas/IdentityType' }
        number: { type: [string, 'null'] }
        provider: { type: string, examples: [payinto] }
        verified_at: { type: [string, 'null'], format: date-time }
        first_name: { type: [string, 'null'] }
        middle_name: { type: [string, 'null'] }
        last_name: { type: [string, 'null'] }
        full_name: { type: [string, 'null'] }
        date_of_birth: { type: [string, 'null'] }
        gender: { type: [string, 'null'] }
        phone_number: { type: [string, 'null'] }
        email: { type: [string, 'null'] }
        photo: { type: [string, 'null'] }
        match_score_percent: { type: [number, 'null'] }
        match_summary: { type: [object, 'null'], additionalProperties: true }
    VirtualAccount:
      type: object
      properties:
        id: { type: string, format: uuid }
        settlement_account_id: { type: [string, 'null'] }
        type: { type: string, enum: [static, dynamic] }
        provider: { type: [string, 'null'] }
        provider_reference: { type: [string, 'null'] }
        merchant_reference: { type: [string, 'null'] }
        channel: { type: [string, 'null'] }
        is_primary: { type: boolean }
        inflow_role: { type: string, enum: [primary, alternative] }
        bank_name: { type: [string, 'null'] }
        bank_code: { type: [string, 'null'] }
        account_name: { type: [string, 'null'] }
        account_number: { type: [string, 'null'] }
        account_type: { type: [string, 'null'] }
        currency: { type: [string, 'null'] }
        country: { type: [string, 'null'] }
        status: { type: [string, 'null'], enum: [active, suspended, disabled, completed, expired, null] }
        amount: { type: [integer, 'null'], description: "Dynamic accounts only. The expected amount in lowest units." }
        amount_control: { type: [string, 'null'], description: "Dynamic accounts only." }
        valid_for: { type: [integer, 'null'], description: "Dynamic accounts only. The validity window in seconds." }
        expired_at: { type: [string, 'null'], format: date-time, description: "Dynamic accounts only." }
        requested_at: { type: [string, 'null'], format: date-time }
        issued_at: { type: [string, 'null'], format: date-time }
        created_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
        deleted_at: { type: [string, 'null'], format: date-time }
    CreateStaticVirtualAccountRequest:
      type: object
      required: [customer_id]
      description: Also send `merchant_reference`, or the older `reference` alias.
      properties:
        customer_id: { type: string, format: uuid }
        settlement_account_id: { type: [string, 'null'], format: uuid, description: "The account that receives funds. Defaults to your main account." }
        merchant_reference: { type: string, maxLength: 64, description: "Your unique reference. Required unless you send `reference`." }
        reference: { type: string, maxLength: 64, description: "Older alias of `merchant_reference`." }
        metadata: { $ref: '#/components/schemas/CustomerMetadata' }
    CreateDynamicVirtualAccountRequest:
      type: object
      required: [amount, amount_control]
      description: Also send `merchant_reference`, or the older `reference` alias.
      properties:
        amount: { $ref: '#/components/schemas/MinorAmount' }
        amount_control:
          type: string
          enum: [Fixed, UnderPayment, OverPayment]
          description: How Payinto treats a payment that differs from `amount`.
        valid_for: { type: [integer, 'null'], minimum: 900, maximum: 86400, description: "Validity window in seconds, from 15 minutes to 24 hours." }
        settlement_account_id: { type: [string, 'null'], format: uuid, description: "The account that receives funds. Defaults to your main account." }
        merchant_reference: { type: string, maxLength: 64, description: "Your unique reference. Required unless you send `reference`." }
        reference: { type: string, maxLength: 64, description: "Older alias of `merchant_reference`." }
        metadata: { $ref: '#/components/schemas/CustomerMetadata' }
    PaymentInflow:
      type: object
      properties:
        id: { type: string }
        business_id: { type: string }
        account_id: { type: string }
        reference: { type: string }
        merchant_reference: { type: [string, 'null'] }
        provider_reference: { type: [string, 'null'] }
        session_id: { type: [string, 'null'] }
        currency: { type: string, examples: [NGN] }
        amount: { type: integer }
        fee: { type: integer }
        vat: { type: integer }
        stamp_duty: { type: integer }
        settled_amount: { type: integer }
        narration: { type: [string, 'null'] }
        method: { type: [string, 'null'] }
        status: { type: string }
        virtual_account: { $ref: '#/components/schemas/VirtualAccount' }
        customer: { $ref: '#/components/schemas/Customer' }
        created_at: { type: [string, 'null'], format: date-time }
    Bank:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        code: { type: string, examples: ['058'] }
        country: { type: [string, 'null'], examples: [NG] }
        currency: { type: [string, 'null'], examples: [NGN] }
        logo_url: { type: [string, 'null'] }
        aliases:
          type: array
          items: { type: string }
    Beneficiary:
      type: object
      properties:
        reference: { type: string, description: "Send this as `resolved_account_reference`." }
        bank_code: { type: string }
        bank_name: { type: [string, 'null'] }
        account_name: { type: [string, 'null'] }
        alias: { type: [string, 'null'] }
        account_number: { type: string }
        is_saved: { type: boolean }
        updated_at: { type: [string, 'null'], format: date-time }
        bank_logo_url: { type: [string, 'null'] }
    TransferFee:
      type: object
      properties:
        amount: { type: integer, examples: [250000] }
        fee: { type: integer }
        vat: { type: integer }
        stamp_duty: { type: integer }
        amount_debited: { type: integer, description: "Amount plus fee, VAT, and stamp duty." }
        total: { type: integer, description: "Alias of `amount_debited`." }
        currency: { type: string, examples: [NGN] }
        free_transfer_benefit:
          type: [object, 'null']
          properties:
            eligible: { type: boolean }
            daily_limit: { type: integer }
            daily_used: { type: integer }
            daily_remaining: { type: integer }
    InitiateTransferRequest:
      type: object
      required: [resolved_account_reference, beneficiary_bank_code, beneficiary_account_number, amount]
      properties:
        debit_account_id: { $ref: '#/components/schemas/DebitAccountId' }
        resolved_account_reference: { type: string, description: "The `reference` from a resolve call." }
        beneficiary_bank_code: { type: string, examples: ['058'] }
        beneficiary_account_number: { type: string, pattern: '^[0-9]{10}$', examples: ['0123456789'] }
        amount: { $ref: '#/components/schemas/MinorAmount' }
        narration: { type: [string, 'null'] }
        merchant_reference: { type: [string, 'null'], maxLength: 64, description: "Your unique reference. Makes retries safe." }
        reference: { type: [string, 'null'], maxLength: 64, description: "Older alias of `merchant_reference`." }
        save_beneficiary: { type: [boolean, 'null'] }
    Transfer:
      type: object
      properties:
        id: { type: string }
        reference: { type: string }
        merchant_reference: { type: [string, 'null'] }
        provider_reference: { type: [string, 'null'] }
        session_id: { type: [string, 'null'] }
        resolved_account_reference: { type: [string, 'null'] }
        amount: { type: integer }
        fee: { type: integer }
        vat: { type: integer }
        stamp_duty: { type: integer }
        amount_debited: { type: integer }
        currency: { type: string, examples: [NGN] }
        bank_code: { type: string }
        account_number: { type: string }
        account_name: { type: [string, 'null'] }
        created_at: { type: [string, 'null'], format: date-time }
        receipt_details: { type: [object, 'null'], additionalProperties: true }
    CreateBulkTransferRequest:
      type: object
      required: [account_id]
      properties:
        account_id: { type: string, format: uuid, description: "The account to debit." }
        name: { type: [string, 'null'], maxLength: 255 }
        use_same_amount: { type: [boolean, 'null'], description: "Pay every recipient `same_amount`." }
        same_amount: { type: [integer, 'null'], minimum: 1, description: "Required when `use_same_amount` is true." }
        merchant_reference: { type: [string, 'null'], maxLength: 64, description: "Your unique reference. Makes retries safe." }
    UpdateBulkTransferRequest:
      type: object
      properties:
        account_id: { type: string, format: uuid }
        name: { type: [string, 'null'], maxLength: 255 }
        use_same_amount: { type: boolean }
        same_amount: { type: [integer, 'null'], minimum: 1 }
    CreateBulkTransferItemRequest:
      type: object
      required: [bank_code, bank_name, account_number, amount]
      properties:
        bank_code: { type: string, maxLength: 20 }
        bank_name: { type: string, maxLength: 255 }
        account_number: { type: string, pattern: '^[0-9]{10}$' }
        amount: { type: integer, minimum: 1, description: "Amount in lowest units." }
        narration: { type: [string, 'null'], maxLength: 255 }
    UpdateBulkTransferItemRequest:
      type: object
      properties:
        bank_code: { type: string, maxLength: 20 }
        bank_name: { type: string, maxLength: 255 }
        account_number: { type: string, pattern: '^[0-9]{10}$' }
        amount: { type: integer, minimum: 1 }
        narration: { type: [string, 'null'], maxLength: 255 }
    BulkTransferImportFile:
      type: object
      required: [file]
      properties:
        file: { type: string, format: binary, description: "A CSV or XLSX file of up to 5 MB with `account_number`, `bank_name` or `bank_code`, `amount`, and optional `narration` columns." }
    BulkTransferStatus:
      type: string
      enum: [draft, pending, validating, ready, awaiting_approval, queued, processing, completed, partially_successful, failed, cancelled]
    BulkTransfer:
      type: object
      properties:
        id: { type: string, format: uuid }
        reference: { type: string }
        merchant_reference: { type: [string, 'null'] }
        name: { type: [string, 'null'] }
        account_id: { type: string, format: uuid }
        scheduled_payment_id: { type: [string, 'null'], format: uuid, description: "The schedule that created this run." }
        currency: { type: string, examples: [NGN] }
        status: { $ref: '#/components/schemas/BulkTransferStatus' }
        use_same_amount: { type: boolean }
        same_amount: { type: [integer, 'null'] }
        total_transfers: { type: integer }
        requested_amount: { type: integer }
        expected_fee_amount: { type: integer }
        successful_amount: { type: integer }
        failed_amount: { type: integer }
        processing_amount: { type: integer }
        charged_fee_amount: { type: integer }
        successful_count: { type: integer }
        failed_count: { type: integer }
        processing_count: { type: integer }
        pending_count: { type: integer }
        items_count: { type: integer, description: "List responses only." }
        items:
          type: array
          description: "Not included in list responses."
          items: { $ref: '#/components/schemas/BulkTransferItem' }
        submitted_at: { type: [string, 'null'], format: date-time }
        approved_at: { type: [string, 'null'], format: date-time }
        processing_started_at: { type: [string, 'null'], format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        created_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
    BulkTransferItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        bulk_transfer_id: { type: string, format: uuid }
        reference: { type: string, description: "Use this with `GET /transactions/tsq/{reference}`." }
        bank_code: { type: string }
        bank_name: { type: string }
        account_number: { type: string }
        resolved_account_name: { type: [string, 'null'] }
        amount: { type: integer }
        effective_amount: { type: integer, description: "The amount that will be sent after `same_amount` is applied." }
        narration: { type: [string, 'null'] }
        resolution_status: { type: string, enum: [pending, resolved, failed] }
        status: { type: string, enum: [pending, queued, processing, successful, failed] }
        failure_code: { type: [string, 'null'], examples: [INSUFFICIENT_BALANCE] }
        failure_reason: { type: [string, 'null'] }
        transaction_id: { type: [string, 'null'] }
        attempt_count: { type: integer }
        attempts:
          type: array
          description: "Included when you get a single bulk transfer."
          items:
            type: object
            properties:
              attempt_number: { type: integer }
              amount: { type: integer }
              status: { type: string }
              failure_code: { type: [string, 'null'] }
              failure_reason: { type: [string, 'null'] }
              started_at: { type: [string, 'null'], format: date-time }
              completed_at: { type: [string, 'null'], format: date-time }
        resolved_at: { type: [string, 'null'], format: date-time }
        processed_at: { type: [string, 'null'], format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        created_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
    CreateScheduledBulkTransferRequest:
      type: object
      required: [source_account_id, bulk_transfer_id, schedule]
      properties:
        source_account_id: { type: string, format: uuid }
        bulk_transfer_id: { type: string, format: uuid, description: "A bulk transfer on the source account with resolved recipients." }
        merchant_reference: { type: [string, 'null'], maxLength: 64, description: "Your unique reference. Makes retries safe." }
        schedule:
          type: object
          required: [frequency, starts_at]
          properties:
            frequency: { type: string, enum: [once, daily, weekly, monthly, yearly, custom] }
            interval: { type: [integer, 'null'], minimum: 1, maximum: 365, default: 1 }
            starts_at: { type: string, format: date-time }
            timezone: { type: [string, 'null'], examples: [Africa/Lagos] }
            ends_type: { type: [string, 'null'], enum: [never, date, occurrences, null], default: never }
            ends_at: { type: [string, 'null'], format: date-time, description: "Required when `ends_type` is `date`." }
            max_occurrences: { type: [integer, 'null'], minimum: 1, maximum: 1000 }
            weekdays:
              type: [array, 'null']
              description: "For weekly schedules. Days of the week from 0, Sunday, to 6, Saturday."
              items: { type: integer, minimum: 0, maximum: 6 }
            month_day: { type: [integer, 'null'], minimum: 1, maximum: 31 }
            custom_unit: { type: [string, 'null'], enum: [days, weeks, months, years, null] }
    UpdateScheduledBulkTransferRequest:
      type: object
      properties:
        frequency: { type: string, enum: [once, daily, weekly, monthly, yearly, custom] }
        interval: { type: integer, minimum: 1, maximum: 365 }
        starts_at: { type: string, format: date-time }
        ends_type: { type: string, enum: [never, date, occurrences] }
        ends_at: { type: [string, 'null'], format: date-time }
        max_occurrences: { type: [integer, 'null'], minimum: 1, maximum: 1000 }
        recurrence_config: { type: object, additionalProperties: true }
    ScheduleStatus:
      type: string
      enum: [pending_approval, active, paused, cancelled, completed]
    ScheduledBulkTransfer:
      type: object
      properties:
        id: { type: string, format: uuid }
        merchant_reference: { type: [string, 'null'] }
        source_account_id: { type: string, format: uuid }
        bulk_transfer_id: { type: [string, 'null'], format: uuid }
        payment_type: { type: string, enum: [bulk_transfer] }
        status: { $ref: '#/components/schemas/ScheduleStatus' }
        name: { type: [string, 'null'] }
        amount: { type: [integer, 'null'], description: "The total paid on each run." }
        currency: { type: string, examples: [NGN] }
        frequency: { type: string }
        interval: { type: integer }
        custom_unit: { type: [string, 'null'] }
        weekdays:
          type: [array, 'null']
          items: { type: integer }
        month_day: { type: [integer, 'null'] }
        timezone: { type: string }
        starts_at: { type: string, format: date-time }
        next_execution_at: { type: [string, 'null'], format: date-time }
        last_execution_at: { type: [string, 'null'], format: date-time }
        ends_type: { type: string }
        ends_at: { type: [string, 'null'], format: date-time }
        max_occurrences: { type: [integer, 'null'] }
        occurrence_count: { type: integer }
        successful_execution_count: { type: integer }
        failed_execution_count: { type: integer }
        recipients:
          type: array
          items:
            type: object
            properties:
              bank_code: { type: string }
              bank_name: { type: string }
              account_number: { type: string }
              resolved_account_name: { type: [string, 'null'] }
              amount: { type: integer }
              narration: { type: [string, 'null'] }
        paused_at: { type: [string, 'null'], format: date-time }
        cancelled_at: { type: [string, 'null'], format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        created_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
    ScheduledBulkTransferExecution:
      type: object
      properties:
        id: { type: string, format: uuid }
        scheduled_payment_id: { type: string, format: uuid }
        bulk_transfer_id: { type: [string, 'null'], format: uuid, description: "The bulk transfer this run created." }
        status: { type: string, enum: [pending, processing, successful, failed, retrying, requires_user_action, skipped, cancelled] }
        amount: { type: [integer, 'null'] }
        currency: { type: [string, 'null'] }
        attempt_count: { type: integer }
        failure_code: { type: [string, 'null'] }
        failure_message: { type: [string, 'null'] }
        scheduled_for: { type: [string, 'null'], format: date-time }
        started_at: { type: [string, 'null'], format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
    BulkTransferFinishedEvent:
      type: object
      properties:
        id: { type: string, format: uuid }
        reference: { type: string }
        merchant_reference: { type: [string, 'null'] }
        name: { type: [string, 'null'] }
        account_id: { type: string, format: uuid }
        scheduled_payment_id: { type: [string, 'null'], format: uuid }
        status: { type: string, enum: [completed, partially_successful, failed] }
        currency: { type: string, examples: [NGN] }
        requested_amount: { type: integer }
        successful_amount: { type: integer }
        failed_amount: { type: integer }
        total_transfers: { type: integer }
        successful_count: { type: integer }
        failed_count: { type: integer }
        completed_at: { type: string, format: date-time }
    VasService:
      type: object
      properties:
        id: { type: string, format: uuid }
        code: { type: string, enum: [airtime, data, internet_service, cable_tv, electricity, betting, flight, gift_card] }
        name: { type: string }
        description: { type: [string, 'null'] }
        provider: { type: [string, 'null'] }
        transaction_limits:
          type: object
          description: Minimum and maximum amounts in lowest units, keyed by currency.
          additionalProperties: true
        fees:
          type: object
          description: Fees in lowest units, keyed by currency.
          additionalProperties: true
    VasBiller:
      type: object
      properties:
        id: { type: string, format: uuid }
        service_id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        description: { type: [string, 'null'] }
        country: { type: [string, 'null'] }
        country_name: { type: [string, 'null'] }
        currency: { type: [string, 'null'] }
        logo_url: { type: [string, 'null'] }
    VasProduct:
      type: object
      properties:
        id: { type: string, format: uuid }
        biller_id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        description: { type: [string, 'null'] }
        currency: { type: string }
        amount_type: { type: [string, 'null'] }
        amount: { type: integer }
        min_amount: { type: integer }
        max_amount: { type: integer }
    VasPaymentBase:
      type: object
      required: [biller_id, product_id, amount]
      properties:
        debit_account_id: { $ref: '#/components/schemas/DebitAccountId' }
        biller_id: { type: string, format: uuid }
        product_id: { type: string, format: uuid }
        amount: { $ref: '#/components/schemas/MinorAmount' }
        merchant_reference: { type: [string, 'null'], maxLength: 64, description: "Your unique reference. Makes retries safe." }
        reference: { type: [string, 'null'], maxLength: 64, description: "Older alias of `merchant_reference`." }
    VasPhoneRecipient:
      type: object
      required: [country_code, phone_number]
      properties:
        country_code: { type: string, examples: [NG] }
        phone_number: { type: string, examples: ['08012345678'] }
    VasPayment:
      type: object
      additionalProperties: true
      properties:
        id: { type: string }
        reference: { type: string }
        merchant_reference: { type: [string, 'null'] }
        provider_reference: { type: [string, 'null'] }
        recipient: { type: [string, 'null'] }
        biller_id: { type: [string, 'null'] }
        product_id: { type: [string, 'null'] }
        currency: { type: string, examples: [NGN] }
        amount: { type: integer }
        fee: { type: integer }
        created_at: { type: [string, 'null'], format: date-time }
    WebhookEnvelope:
      type: object
      required: [event, id, created_at, api_version, data]
      properties:
        event: { type: string }
        id: { type: string, description: "The webhook delivery ID." }
        created_at: { type: string, format: date-time }
        api_version: { type: string, examples: ['2026-05-27'] }
        data: {}
    CheckoutPaymentEvent:
      type: object
      properties:
        id: { type: string, description: "The payment inflow ID." }
        reference: { type: string }
        merchant_reference: { type: [string, 'null'] }
        payment_method: { type: string, enum: [bank_transfer, card] }
        currency: { type: string, examples: [NGN] }
        base_amount: { type: integer }
        customer_charge_amount: { type: integer }
        customer_fee_amount: { type: integer }
        amount: { type: integer }
        charges_applied: { type: boolean }
        fee: { type: integer }
        vat: { type: integer }
        stamp_duty: { type: integer }
        total_fee: { type: integer }
        settled_amount: { type: integer }
        payment_status: { type: string, enum: [successful] }
        paid_at: { type: [string, 'null'], format: date-time }
        customer: { type: [object, 'null'], additionalProperties: true }
        metadata: { type: [object, 'null'], additionalProperties: true }
