MojoPay Omni Docs

OpenAPI 3.0 specification for the MojoPay Omni API. Import the raw YAML file into Postman, Insomnia, or your codegen tool.

openapi: 3.0.3
info:
  title: MojoPay Omni API
  version: "1.0"
  description: |
    HTTPS JSON API for collections, hosted checkout, payouts, bank and mobile verification, mandates, reference metadata, and wallet balance.

    Errors return an `ApiError` object: `error.code`, `error.message`, `error.request_id`, and optional `error.doc_url` for more detail.
    `request_id` values look like `REQ_` + 12 uppercase alphanumeric characters.
    On POST requests that create or move money (or create checkout sessions or mandates), send an `Idempotency-Key` header. Reuse the same key only when safely retrying the **same** JSON body.

    **Integration quick start:** See `/doc` for credentials setup, hosted checkout curl examples, webhook signature verification (PHP/Node), and an integration checklist. Sample JSON bodies: `/doc/samples.json`. Portal Mojo Ai can help debug Omni API payloads against the same guide.

    **Testing mode:** New merchants start in Testing mode on live rails. Money-in (`POST /v1/collections` and fixed-amount `POST /v1/checkout/sessions`, plus amounts entered on `/pay/{id}`) is capped to a per-country max until Go Live. `amount_mode: open` is not allowed while Testing. Details: `/doc#testing-mode`.

    **Required headers on authenticated requests:** `Authorization: Bearer {token}`, `Content-Type: application/json`, `Accept: application/json`.
    **Sub-accounts (when enabled for your merchant):** also send `X-Mojo-Account-Ref` with the target account ref on every request except `GET /v1/metadata/*` and `GET /v1/balance`. Use `GET /v1/account` to verify the resolved account.
    **Money-moving POSTs also require:** `Idempotency-Key` (UUID recommended; reuse only when retrying the same body).

    **Webhooks** (configured per API client in the merchant portal): at-least-once delivery with `MojoPay-Signature` to the client that created the resource. Event types (v1.0): `collection.processing|succeeded|failed`, `checkout_session.processing|succeeded|failed`, `payout.processing|succeeded|failed`, `mandate.processing|succeeded|cancelled`, `mandate_debit.processing|succeeded|failed`, and subscription lifecycle events (`subscription.created`, `subscription.active`, `subscription.cancelled`, `subscription.failed`, `subscription.charge_failed`) when linked to plans. Event `id` values look like `EVT_` + 24 uppercase alphanumeric characters. Verify HMAC on `{timestamp}.{raw_body}`; reject timestamps outside ±300 seconds. Portal-created pay links do not emit outbound webhooks.

    **Security:** Bearer tokens are merchant-scoped; resource lookups never expose another tenant’s data (404). Optional API IP allowlist is configured in the portal. Portal **Team member** users can view activity but cannot change API clients, webhooks, or financial settings.

    **Wallet balance (`GET /v1/balance`):** `collection_wallet` is the **available** (T+1-released) collection balance; `collection_wallet_actual` is total succeeded credits; `collection_wallet_held` is still within the T+1 hold. Payouts draw from `payout_wallet`. Merchants move funds via the portal settlement page or approved auto-settlement.

    Rate limits apply to keep the API reliable. When exceeded, responses use HTTP 429 and error code `rate_limited`. OAuth token requests are limited per client; authenticated calls are limited per merchant account.

servers:
  - url: https://omni.mojo-pay.com
    description: Current application (APP_URL)
  - url: https://api.mojopay.com
    description: Production

tags:
  - name: OAuth
  - name: Collections
  - name: Checkout
  - name: Transactions
  - name: Payouts
  - name: Verifications
  - name: Mandates
  - name: Subscriptions
  - name: Metadata
  - name: Balance
  - name: Accounts

paths:
  /v1/oauth/token:
    post:
      tags: [OAuth]
      summary: Issue access token (client_credentials)
      operationId: oauthToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OAuthTokenRequest"
      responses:
        "200":
          description: Bearer token issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthTokenResponse"
        "400":
          description: invalid_parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "401":
          description: invalid_client
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "403":
          description: Forbidden (e.g. invalid credentials or product not enabled for your account)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "429":
          description: rate_limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/collections:
    post:
      tags: [Collections]
      summary: Create collection (wallet collection)
      operationId: createCollection
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCollectionRequest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Collection"
        "400":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "401":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "403":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "409":
          description: idempotency_key_mismatch
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "429":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
    get:
      tags: [Collections]
      summary: Get collection by merchant_reference
      operationId: getCollectionByMerchantReference
      security: [{ bearerAuth: [] }]
      parameters:
        - name: merchant_reference
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Collection"
        "400":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "401":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "404":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/collections/{id}:
    get:
      tags: [Collections]
      summary: Get collection by id
      operationId: getCollectionById
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Collection"
        "401":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "404":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/checkout/sessions:
    post:
      tags: [Checkout]
      summary: Create hosted checkout session
      operationId: createCheckoutSession
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCheckoutSessionRequest"
      responses:
        "201":
          description: Created (slim create payload)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutSessionCreate"
        "400":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "409":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/checkout/sessions/{id}:
    get:
      tags: [Checkout]
      summary: Get checkout session
      operationId: getCheckoutSession
      security: [{ bearerAuth: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutSession"
        "404":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/transactions/verify/{merchant_reference}:
    get:
      tags: [Transactions]
      summary: Unified verify by merchant_reference
      operationId: verifyTransaction
      security: [{ bearerAuth: [] }]
      parameters:
        - name: merchant_reference
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Collection, payout, or checkout snapshot
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "404":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/payouts:
    post:
      tags: [Payouts]
      summary: Create payout
      operationId: createPayout
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePayoutRequest"
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payout"
        "400":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "409":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/payouts/{merchant_reference}:
    get:
      tags: [Payouts]
      summary: Get payout by merchant_reference
      operationId: getPayout
      security: [{ bearerAuth: [] }]
      parameters:
        - name: merchant_reference
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payout"
        "404":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/verifications/msisdn:
    post:
      tags: [Verifications]
      summary: MSISDN / mobile name check
      operationId: verifyMsisdn
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mobile]
              properties:
                mobile:
                  type: string
                  pattern: "^\\d{10,15}$"
                provider:
                  type: string
                  maxLength: 64
      responses:
        "200":
          description: Verification outcome
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerificationResult"
        "502":
          description: gateway_error (upstream KYC provider failure)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/verifications/bank_accounts:
    post:
      tags: [Verifications]
      summary: Bank account name check
      operationId: verifyBankAccount
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bank_code, account_number]
              properties:
                bank_code:
                  type: string
                  maxLength: 32
                  description: Bank code from metadata banks.
                account_number:
                  type: string
                  maxLength: 64
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerificationResult"
        "502":
          description: gateway_error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/mandates:
    post:
      tags: [Mandates]
      summary: Create mandate
      operationId: createMandate
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMandateRequest"
      responses:
        "201":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
    get:
      tags: [Mandates]
      summary: List / get mandate by merchant_reference
      operationId: listMandates
      security: [{ bearerAuth: [] }]
      parameters:
        - name: merchant_reference
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/mandates/{mandate_id}/cancel:
    post:
      tags: [Mandates]
      summary: Cancel mandate
      operationId: cancelMandate
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: mandate_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mobile, reason]
              properties:
                mobile:
                  type: string
                reason:
                  type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/mandates/{mandate_id}/debits:
    post:
      tags: [Mandates]
      summary: Create mandate debit
      operationId: createMandateDebit
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: mandate_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency, merchant_reference, provider]
              properties:
                amount:
                  type: string
                currency:
                  type: string
                  minLength: 3
                  maxLength: 3
                merchant_reference:
                  type: string
                  maxLength: 255
                provider:
                  type: string
                  maxLength: 64
      responses:
        "201":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/mandate_debits/status:
    get:
      tags: [Mandates]
      summary: Mandate debit status
      operationId: mandateDebitStatus
      security: [{ bearerAuth: [] }]
      parameters:
        - name: mandate_id
          in: query
          required: true
          schema:
            type: string
        - name: merchant_reference
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/subscription_plans:
    get:
      tags: [Subscriptions]
      summary: List active subscription plans
      description: Requires the subscriptions product to be enabled for your merchant.
      operationId: listSubscriptionPlans
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/MerchantAccountRef"
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                required: [object, data]
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SubscriptionPlan"
        "403":
          description: forbidden (subscriptions not enabled)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/subscriptions:
    post:
      tags: [Subscriptions]
      summary: Enroll a subscription
      description: Creates a subscriber and starts mandate enrollment for a subscription plan.
      operationId: createSubscription
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/MerchantAccountRef"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSubscriptionRequest"
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
        "403":
          description: forbidden (subscriptions not enabled)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "422":
          description: validation_error (e.g. invalid or inactive plan_id)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
    get:
      tags: [Subscriptions]
      summary: Get subscription by merchant_reference
      operationId: getSubscription
      security: [{ bearerAuth: [] }]
      parameters:
        - name: merchant_reference
          in: query
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/MerchantAccountRef"
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
        "404":
          description: not_found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/metadata/banks:
    get:
      tags: [Metadata]
      summary: Bank reference list
      operationId: metadataBanks
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/metadata/mobile_providers:
    get:
      tags: [Metadata]
      summary: Mobile money providers
      operationId: metadataMobileProviders
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/balance:
    get:
      tags: [Balance]
      summary: Wallet balance
      operationId: getBalance
      security: [{ bearerAuth: [] }]
      parameters:
        - name: currency
          in: query
          required: false
          schema:
            type: string
            minLength: 3
            maxLength: 3
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BalanceSnapshot"

  /v1/account:
    get:
      tags: [Accounts]
      summary: Resolved merchant account context
      description: Returns the authenticated merchant and the sub-account resolved from `X-Mojo-Account-Ref` when sub-accounts are enabled.
      operationId: getAccountContext
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/MerchantAccountRef"
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountContext"
        "400":
          description: invalid_parameter (missing account header when required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "404":
          description: not_found (unknown account ref)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: Short-lived access token from POST /v1/oauth/token

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        maxLength: 255
        example: 7c9e6679-7425-40de-944b-e07fc1f90ae7
      description: Required for money-moving and session/mandate creates; replays must use the same body.

    MerchantAccountRef:
      name: X-Mojo-Account-Ref
      in: header
      required: false
      schema:
        type: string
        pattern: '^[a-z0-9][a-z0-9_-]{2,63}$'
        example: accra-branch
      description: Required when sub-accounts are enabled for the merchant (except metadata and balance routes). Targets the sub-account for this request.

  schemas:
    AccountContext:
      type: object
      required: [object, merchant]
      properties:
        object:
          type: string
          example: account_context
        merchant:
          type: object
          properties:
            id:
              type: integer
            name:
              type: string
            sub_accounts_enabled:
              type: boolean
        account:
          type: object
          nullable: true
          properties:
            id:
              type: string
              example: MAC_ABCDEF123456
            object:
              type: string
              example: merchant_account
            account_ref:
              type: string
              example: accra-branch
            name:
              type: string
            status:
              type: string
              example: active

    ApiError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
            message:
              type: string
            doc_url:
              type: string
              format: uri
              description: Optional URL with more detail for this error.
            request_id:
              type: string
              description: Correlation id, e.g. REQ_8F3A2C1B9D0E
              example: REQ_8F3A2C1B9D0E

    OAuthTokenRequest:
      type: object
      required: [client_id, client_secret]
      properties:
        grant_type:
          type: string
          enum: [client_credentials]
        client_id:
          type: string
        client_secret:
          type: string

    OAuthTokenResponse:
      type: object
      required: [access_token, token_type, expires_in]
      properties:
        access_token:
          type: string
          description: Opaque 64-character bearer token (not a JWT).
          example: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 1200
      example:
        access_token: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
        token_type: Bearer
        expires_in: 1200

    CreateCollectionRequest:
      type: object
      required: [amount, currency, merchant_reference, mobile, provider]
      properties:
        amount:
          type: string
          description: >
            Merchant principal to receive. While the merchant is in Testing mode, must be at or below the
            country testing max amount (see `/doc#testing-mode`).
        currency:
          type: string
          minLength: 3
          maxLength: 3
        merchant_reference:
          type: string
        mobile:
          type: string
        provider:
          type: string
        description:
          type: string

    Collection:
      type: object
      description: >
        Includes id, status, amounts, and references; fields may vary by payment rail and lifecycle.
        `amount` is credited to the merchant on success. When fees apply, `fee_amount` is charged to the payer on top,
        and `amount_payable` is the gross collected (`amount` + `fee_amount`).
      additionalProperties: true
      properties:
        fee_amount:
          type: string
          description: Platform fee added on top of `amount` (same currency).
        amount_payable:
          type: string
          description: Gross amount the payer pays (`amount` + `fee_amount`).

    CreateCheckoutSessionRequest:
      type: object
      required: [currency, merchant_reference, success_url, cancel_url]
      properties:
        amount_mode:
          type: string
          enum: [fixed, open]
          default: fixed
          description: >
            `fixed` (default) requires `amount` at create time. `open` lets the customer enter the amount on the hosted pay page.
            `open` is not available while the merchant is in Testing mode (use `fixed` at or below the country testing max, or Go Live).
            See `/doc#testing-mode`.
        amount:
          type: string
          description: >
            Required when `amount_mode` is `fixed` (or omitted). Omit for `open`.
            In Testing mode, fixed amounts must be at or below the country testing max.
        currency:
          type: string
        merchant_reference:
          type: string
        payment_rail:
          type: string
          nullable: true
          enum: [mobile_money, card]
          description: >
            `mobile_money` locks the session to mobile money; `mobile` is then required at create unless pre-filled later on the hosted page.
            When the key is omitted, the session stores `null` and the customer chooses mobile money on `/pay/{id}` (portal pay links send explicit JSON `null`).
            Card is not accepted on hosted checkout in v1.0.
        mobile:
          type: string
          description: >
            Required when `payment_rail` is `mobile_money` at create time.
            Omit when the customer will enter wallet details on the hosted page.
        provider:
          type: string
          description: Mobile money provider code when required for the chosen rail (metadata mobile_providers).
        success_url:
          type: string
          format: uri
        cancel_url:
          type: string
          format: uri
        description:
          type: string

    CheckoutSessionCreate:
      type: object
      required: [id, object, status, url, expires_at]
      description: >
        Fixed-amount creates include `currency`, `amount_mode`, `amount`, `fee_amount`, and `amount_payable` when fees are computed at create time.
        Open-amount creates omit amount fields until the customer submits an amount on the hosted pay page.
        `expires_at` is typically one hour after create.
      properties:
        id:
          type: string
          example: CS_4NQP81ABCDEF
        object:
          type: string
          enum: [checkout_session]
        status:
          type: string
          enum: [processing]
        currency:
          type: string
          example: GHS
        amount_mode:
          type: string
          enum: [fixed, open]
        amount:
          type: string
          description: Merchant principal; present for fixed amount at create time.
        fee_amount:
          type: string
          description: Platform fee added on top of `amount` when fees apply.
        amount_payable:
          type: string
          description: Gross amount the payer pays (`amount` + `fee_amount`).
        url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
      example:
        id: CS_4NQP81ABCDEF
        object: checkout_session
        status: processing
        currency: GHS
        amount_mode: fixed
        amount: "1.00"
        fee_amount: "0.02"
        amount_payable: "1.02"
        url: https://checkout.example.com/pay/CS_4NQP81ABCDEF
        expires_at: "2026-04-15T11:00:00Z"

    CheckoutSession:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
        object:
          type: string
          enum: [checkout_session]
        status:
          type: string
        currency:
          type: string
        amount_mode:
          type: string
          enum: [fixed, open]
        amount:
          type: string
          nullable: true
        fee_amount:
          type: string
        amount_payable:
          type: string
          nullable: true
        merchant_reference:
          type: string
        mobile:
          type: string
          nullable: true
        payment_rail:
          type: string
          nullable: true
        provider:
          type: string
          nullable: true
        card_scheme:
          type: string
          nullable: true

    VerificationResult:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [verified, invalid]
        profile:
          type: object
          nullable: true
          properties:
            first_name:
              type: string
            last_name:
              type: string

    BalanceSnapshot:
      type: object
      required: [currency, available, collection_wallet, collection_wallet_actual, collection_wallet_held, payout_wallet, pending]
      properties:
        currency:
          type: string
        available:
          type: string
          description: Total across available collection_wallet and payout_wallet.
        collection_wallet:
          type: string
          description: Available collection balance (released after T+1; movable without instant settlement fee).
        collection_wallet_actual:
          type: string
          description: Total succeeded collection credits on record (actual collection wallet).
        collection_wallet_held:
          type: string
          description: Collection funds still held until the next T+1 release window.
        payout_wallet:
          type: string
        pending:
          type: string
        provider_float:
          type: string
          description: Present when upstream float query succeeds.

    CreatePayoutRequest:
      type: object
      required: [type, currency, amount, merchant_reference]
      properties:
        type:
          type: string
          enum: [mobile_money, bank]
        currency:
          type: string
        amount:
          type: string
        merchant_reference:
          type: string
        mobile:
          type: string
        provider:
          type: string
        account_number:
          type: string
        bank_code:
          type: string
        narration:
          type: string
        transaction_type:
          type: string
        transaction_date:
          type: string
        expires_at:
          type: string

    Payout:
      type: object
      additionalProperties: true

    CreateMandateRequest:
      type: object
      required: [mobile, provider, currency, amount, frequency, starts_at, ends_at, merchant_reference]
      properties:
        mobile:
          type: string
        provider:
          type: string
        currency:
          type: string
        amount:
          type: string
        frequency:
          type: string
          enum: [DAILY, WEEKLY, MONTHLY]
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        merchant_reference:
          type: string
        description:
          type: string
        plan_id:
          type: string
          description: When set, links the mandate to a subscription plan and creates a subscriber.
        customer_name:
          type: string
          description: Required when plan_id is set.
        customer_email:
          type: string
          format: email
          description: Required when plan_id is set.
        notes:
          type: string

    SubscriptionPlan:
      type: object
      required: [object, plan_id, name, currency, amount, frequency, status]
      properties:
        object:
          type: string
          example: subscription_plan
        plan_id:
          type: string
          example: PLN_ABCDEF123456
        name:
          type: string
        description:
          type: string
        currency:
          type: string
          minLength: 3
          maxLength: 3
        amount:
          type: string
        frequency:
          type: string
          enum: [DAILY, WEEKLY, MONTHLY]
        duration_type:
          type: string
        duration_months:
          type: integer
          nullable: true
        status:
          type: string
          example: active

    CreateSubscriptionRequest:
      type: object
      required: [plan_id, merchant_reference, customer_name, customer_email, mobile, provider]
      properties:
        plan_id:
          type: string
          example: PLN_ABCDEF123456
        merchant_reference:
          type: string
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        mobile:
          type: string
          pattern: "^\\d{10,15}$"
        provider:
          type: string
        notes:
          type: string

    Subscription:
      type: object
      additionalProperties: true
      properties:
        object:
          type: string
          example: subscription
        subscription_id:
          type: string
          example: SUB_ABCDEF123456
        status:
          type: string
        merchant_reference:
          type: string
        plan_id:
          type: string
        mandate_id:
          type: string