openapi: 3.1.0

info:
  title: Pdirect Payment Gateway API
  version: 1.0.0
  summary: Accept and disburse payments across cards, mobile money, e-wallets, and bank transfers.
  description: |
    The Pdirect Payment Gateway is the unified API behind every Pdirect
    payment integration — direct HTTP, the Flutter SDK (`pdirect_pay`),
    and the Angular SDK (`@mms/pdirect-pay`).

    ### How it fits together

    1. Your **merchant backend** holds a long-lived `merchant_secret` and
       calls `POST /internal/sessions/create` once per checkout to mint a
       short-lived `session_token`.
    2. Your **mobile or web SDK** (or your own client) presents that
       session token as `Authorization: Bearer <session_token>` on every
       payment call. The session is bound to a specific amount, currency,
       and customer reference — drift returns `400`.
    3. After `POST /payments/submit` succeeds, the session is consumed.
       The next checkout needs a new session.

    > **Auth-v3 across both SDKs.** The Flutter SDK (v3.0.0+) and the
    > Angular SDK (v1.3.0+) both speak Auth-v3 — session tokens minted
    > server-to-server and presented as `Authorization: Bearer
    > <session_token>`. `amount`, `currency`, and `customer_reference`
    > are bound to the session at mint time; new integrations should
    > omit them from `/payments/collect` bodies. The fields remain
    > accepted for backwards compatibility (gateway soft-checks against
    > the session binding; drift returns `400`).

    ### Conventions

    - **Amount encoding.** Sent and stored as a positive decimal string
      (`"12.50"`). The session stores the value verbatim — `"1.00"` and
      `"1"` are not equivalent on rebind.
    - **Currency.** Lowercase ISO 4217 (`usd`, `cdf`, `xof`).
    - **Idempotency.** Send `Idempotency-Key: <uuid>` on `POST` to make
      retries safe. Same key + same body within 24 h replays the cached
      response; same key + different body returns `409`.
    - **Errors** all share the [`PaymentGatewayError`](#tag/Errors)
      envelope with stable `error_code` values (1001–1599).
    - **Rate limits.** `30/min` per session on most endpoints,
      `5 / 5min` on OTP verify/resend.
  contact:
    name: Pdirect Developer Support
    url: https://docs.pdirect.com/support
  license:
    name: Proprietary
    url: https://pdirect.com/terms

servers:
  - url: https://app.api.gtwy.pdirect.com/api/v1
    description: Production
  - url: https://staging.app.api.gtwy.pdirect.com/api/v1
    description: Staging
  - url: https://app.api.gtwy.pdirect.com/api/v1
    description: Sandbox (free test credentials)

tags:
  - name: Sessions
    description: |
      Server-to-server endpoints your **merchant backend** calls with a
      `merchant_secret`. The mobile / web SDK never touches these.
  - name: Payments
    description: |
      The core checkout flow. Your SDK or client presents a
      `session_token` and walks the customer from collect → submit
      → (optional OTP) → completion.
  - name: Payouts
    description: |
      Disburse funds to one or many beneficiaries via mobile money or
      e-wallet.
  - name: Cards
    description: |
      Card payments via Mastercard Hosted Session and 3-D Secure. Use
      these alongside `/payments/submit` when `payment_method: card`.
  - name: OTP
    description: |
      One-time-password verification and resend. Used after
      `/payments/submit` for e-wallet, mobile money, and any flow that
      requires a customer challenge.
  - name: Status
    description: |
      Read-only status queries. Safe to poll, but prefer webhooks for
      authoritative state.
  - name: Public
    description: |
      Unauthenticated utility endpoints — health, supported countries,
      bank list. Suitable for client-side use without a session.

externalDocs:
  description: Full developer documentation
  url: https://docs.pdirect.com

security: []

paths:

  /internal/sessions/create:
    post:
      tags: [Sessions]
      operationId: createSession
      summary: Mint a session token for an SDK checkout
      description: |
        Called once per checkout by your merchant backend. Returns a
        single-use `session_token` your SDK presents on subsequent
        payment calls.

        The cleartext token is shown **exactly once** — store the
        `session_id` if you need to revoke it later, but the token
        itself cannot be re-fetched.

        > **Path naming.** This endpoint lives under `/internal/` for
        > legacy reasons but is intentionally exposed to merchant
        > backends. A v2 release will rename it to
        > `/api/v1/merchant/sessions/create`; the existing path will
        > stay alive for a deprecation window.
      security:
        - merchantSecret: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionCreateRequest'
            examples:
              wallet_checkout:
                summary: Wallet checkout, default 30-min TTL
                value:
                  amount: "12.50"
                  currency: usd
                  customer_reference: cust_abc123
              with_fingerprint:
                summary: Pre-declared device fingerprint, 1-h TTL
                value:
                  amount: "100.00"
                  currency: cdf
                  customer_reference: order_2026_05_05_8842
                  ttl_seconds: 3600
                  device_fingerprint: 9f8e7d6c5b4a3928176054321aabbccdd11ee
      responses:
        '201':
          description: Session minted. The `session_token` is shown once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionCreateResponse'
              example:
                session_id: "607f1f77bcf86cd799439011"
                session_token: "sess_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
                issued_at: "2026-05-05T10:15:00Z"
                expires_at: "2026-05-05T10:45:00Z"
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/collect:
    post:
      tags: [Payments]
      operationId: collectPayment
      summary: Start a customer-to-business payment
      description: |
        Begin a checkout. The body's `amount`, `currency`, and
        `customer_reference` must exactly match the values your
        merchant backend declared when minting the session.

        For `payment_method: card` with Mastercard Hosted Session, the
        SDK then walks the customer through the hosted page; for
        wallet, mobile money, and flash, follow up with
        [`POST /payments/submit`](#tag/Payments/operation/submitPayment).
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/DeviceFingerprint'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentCollectionRequest'
            examples:
              wallet:
                summary: Wallet collection (full user info)
                description: |
                  `amount`, `currency`, and `customer_reference` are
                  bound to the `session_token` and omitted from the
                  body. The gateway will read them off the session.
                value:
                  payment_method: wallet
                  user_info: full
                  payment_method_info: full
                  fee_covered_by: buyer
                  delivery_behaviour: direct_delivery
                  notify_url: https://merchant.example.com/pdirect/webhook
                  first_name: John
                  last_name: Doe
                  email: john.doe@example.com
                  phone_number: "+243998857000"
                  wallet_number: "3005710720108954"
                  wallet_pin: "048698"
              card_hosted:
                summary: Card via Mastercard Hosted Session
                value:
                  payment_method: card
                  user_info: full
                  payment_method_info: none
                  fee_covered_by: buyer
                  delivery_behaviour: direct_delivery
                  notify_url: https://merchant.example.com/pdirect/webhook
                  success_redirect_url: https://merchant.example.com/checkout/success
                  fail_redirect_url: https://merchant.example.com/checkout/fail
                  first_name: John
                  last_name: Doe
                  email: john.doe@example.com
                  phone_number: "+243998857000"
                  mastercard_payment_method: HOSTED_SESSION
              legacy_with_bound_fields:
                summary: Legacy — echo the bound fields (deprecated)
                description: |
                  Pre-Auth-v3 callers that still send
                  `(amount, currency, customer_reference)` in the body.
                  Accepted with a soft-check against the session; any
                  drift returns `400`. New integrations should omit.
                value:
                  customer_reference: cust_abc123
                  amount: "12.50"
                  currency: usd
                  payment_method: wallet
                  user_info: full
                  payment_method_info: full
                  fee_covered_by: buyer
                  delivery_behaviour: direct_delivery
                  notify_url: https://merchant.example.com/pdirect/webhook
                  first_name: John
                  last_name: Doe
                  email: john.doe@example.com
                  phone_number: "+243998857000"
                  wallet_number: "3005710720108954"
                  wallet_pin: "048698"
      responses:
        '200':
          description: Payment created. Status reflects the next step.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentCollectionResponse'
              example:
                success: true
                transaction_id: txn_8f3a4c2e9b1d7a6f5c0e8d
                status: pending
                message: Payment request received
                amount: "12.50"
                currency: usd
                fee_amount: "0.50"
                total_amount: "13.00"
                created_at: "2026-05-05T10:15:00Z"
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
      x-sdk-methods:
        flutter:
          package: pdirect_pay
          symbol: PdirectPaymentService.createPayment
          since: 3.0.0
        angular:
          package: "@mms/pdirect-pay"
          symbol: PdirectPaymentService.createPayment
          since: 1.0.0
          notes: |
            Auth-v3 since 1.3.0 — sends Authorization: Bearer
            <session_token>; omits amount / currency / customer_reference
            from the body when unset.

  /payments/collect-redirect:
    post:
      tags: [Payments]
      operationId: collectPaymentWithRedirect
      summary: Start a payment and return a redirect URL
      description: |
        Variant of [`/payments/collect`](#tag/Payments/operation/collectPayment)
        that returns a `redirect_url` instead of inline state. Useful
        for hosted-page flows where you want to hand the customer's
        browser straight to the gateway-managed landing page.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/DeviceFingerprint'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentCollectionRequest'
      responses:
        '200':
          description: Redirect URL returned.
          content:
            application/json:
              schema:
                type: object
                required: [transaction_id, redirect_url]
                properties:
                  transaction_id:
                    type: string
                    example: txn_8f3a4c2e9b1d7a6f5c0e8d
                  redirect_url:
                    type: string
                    format: uri
                    example: https://app.api.gtwy.pdirect.com/process?paymentID=txn_8f3a4c2e9b1d7a6f5c0e8d
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/send:
    post:
      tags: [Payouts]
      operationId: sendPayout
      summary: Disburse funds to one or many beneficiaries
      description: |
        Send a single immediate payout, schedule a future one, or set
        up a recurring series. Up to 100 beneficiaries per request.

        > `/payments/b2c` is a deprecated alias of this endpoint —
        > new integrations should use `/payments/send`.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/B2CPaymentRequest'
            examples:
              immediate_mobile_money:
                summary: Single immediate mobile-money payout
                value:
                  customer_reference: payout_2026_05_05_001
                  currency: usd
                  receipt_mode: mobile_money
                  notify_url: https://merchant.example.com/pdirect/webhook
                  sending_type: immediate
                  beneficiaries:
                    - phone_number: "+243998857000"
                      amount: "10.00"
                  note: "Payout for May 5"
              recurring_payroll:
                summary: Monthly recurring payroll, 12 occurrences
                value:
                  customer_reference: payroll_2026_05
                  currency: usd
                  receipt_mode: wallet
                  notify_url: https://merchant.example.com/pdirect/webhook
                  sending_type: recurring
                  recurring_frequency: monthly
                  recurring_start_date: "2026-05-15"
                  recurring_occurrences: 12
                  beneficiaries:
                    - wallet_number: "3005710720108954"
                      amount: "1500.00"
                    - wallet_number: "3005710720108955"
                      amount: "1200.00"
                  note: "Salary payment"
      responses:
        '200':
          description: Batch accepted. Per-beneficiary status in the response body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/B2CPaymentResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/b2c:
    post:
      tags: [Payouts]
      operationId: sendPayoutB2cAlias
      summary: (Deprecated) Disburse funds — use /payments/send
      deprecated: true
      description: |
        **Deprecated alias** of [`/payments/send`](#tag/Payouts/operation/sendPayout).
        Behaviour is identical; the request and response shapes are
        the same. New integrations must use `/payments/send`.

        Responses on this path will include a `Deprecation` header
        starting in API v1.x; the route will be removed in v2.0.
      security:
        - sessionToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/B2CPaymentRequest'
      responses:
        '200':
          description: Batch accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/B2CPaymentResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/submit:
    post:
      tags: [Payments]
      operationId: submitPayment
      summary: Submit payment-method details to complete a checkout
      description: |
        Provide the customer's payment-method specifics (wallet number
        and PIN, mobile-money MSISDN, card details for non-hosted
        flows, etc.) to advance the transaction. The exact fields
        required depend on the `payment_method` declared at
        [`/payments/collect`](#tag/Payments/operation/collectPayment).

        ### Side effects

        - On a successful response, the underlying session is marked
          **CONSUMED**. Subsequent calls with the same session token
          return `401` — mint a new session for the next checkout.
        - For e-wallet and mobile-money flows, the response may
          indicate that an OTP step is needed; follow up with
          [`/payments/verify-otp`](#tag/OTP/operation/verifyOtp).
        - Card data fields (PAN, CVV, expiry, holder name) and PINs
          are **never logged**, in body or in error responses.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - $ref: '#/components/parameters/DeviceFingerprint'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentSubmitRequest'
            examples:
              wallet_with_pin:
                summary: E-wallet submission
                value:
                  payment_id: pay_encrypted_abc123def456
                  payment_method_id: pm_wallet_001
                  ewallet_number: "3005710720108954"
                  ewallet_pin: "048698"
              mobile_money:
                summary: Mobile money submission
                value:
                  payment_id: pay_encrypted_abc123def456
                  payment_method_id: pm_mobile_money_001
                  mobile_money_phone_number: "+243998857000"
              card_hosted_session:
                summary: Card via Mastercard hosted session
                value:
                  payment_id: pay_encrypted_abc123def456
                  payment_method_id: pm_card_mc_001
                  mastercard_payment_method: HOSTED_SESSION
                  session_id: SESSION0002826410421L29410421
      responses:
        '200':
          description: Submitted; status indicates next step.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentSubmitResponse'
              example:
                success: true
                payment_id: pay_encrypted_abc123def456
                status: processing
                message: Payment submitted; awaiting OTP
                processing_fee: 0.50
                total_amount: 13.00
                submitted_at: "2026-05-05T10:16:00Z"
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
      x-sdk-methods:
        flutter:
          package: pdirect_pay
          symbol: PdirectPaymentService.submitPaymentDetails
          since: 3.0.0
        angular:
          package: "@mms/pdirect-pay"
          symbol: PdirectPaymentService.submitPaymentDetails
          since: 1.0.0

  /payments/verify-otp:
    post:
      tags: [OTP]
      operationId: verifyOtp
      summary: Verify a one-time password
      description: |
        Confirm a six-digit OTP for transactions that require a
        customer challenge (typically e-wallet and mobile money).

        **Tight rate limit:** five attempts per five minutes per
        session. Hitting the limit returns `429` with
        `error_code: "1301"` — back off rather than retry immediately.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentOTPVerifyRequest'
            example:
              payment_id: pay_encrypted_abc123def456
              otp_code: "482915"
      responses:
        '200':
          description: OTP verified; payment may now be completed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentOTPVerifyResponse'
              example:
                success: true
                payment_id: pay_encrypted_abc123def456
                status: approved
                message: OTP verified successfully
                debit_amount: 12.50
                wallet_balance: 87.50
                verified_at: "2026-05-05T10:17:30Z"
                completed_at: "2026-05-05T10:17:35Z"
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
      x-sdk-methods:
        flutter:
          package: pdirect_pay
          symbol: PdirectPaymentService.verifyOtp
          since: 3.0.0
        angular:
          package: "@mms/pdirect-pay"
          symbol: PdirectPaymentService.verifyOtp
          since: 1.0.0

  /payments/resend-otp:
    post:
      tags: [OTP]
      operationId: resendOtp
      summary: Request a fresh OTP for a pending payment
      description: |
        Ask the gateway to dispatch a new OTP to the customer.
        Subject to the same `5 / 5min` rate limit as
        [`/payments/verify-otp`](#tag/OTP/operation/verifyOtp), and an
        independent `max_resends` ceiling per payment (default 3).
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentResendOTPRequest'
            example:
              payment_id: pay_encrypted_abc123def456
      responses:
        '200':
          description: OTP resent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentResendOTPResponse'
              example:
                success: true
                payment_id: pay_encrypted_abc123def456
                status: pending_otp_verification
                message: OTP resent successfully
                otp_sent_to: "j***@example.com"
                resend_count: 1
                max_resends: 3
                resent_at: "2026-05-05T10:18:00Z"
                expires_at: "2026-05-05T10:28:00Z"
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
      x-sdk-methods:
        flutter:
          package: pdirect_pay
          symbol: PdirectPaymentService.resendOtp
          since: 3.0.0
        angular:
          package: "@mms/pdirect-pay"
          symbol: PdirectPaymentService.resendOtp
          since: 1.0.0

  /payments/status/{transaction_id}:
    get:
      tags: [Status]
      operationId: getPaymentStatus
      summary: Read the current status of a transaction
      description: |
        Generic status query keyed by `transaction_id`. Safe to poll
        but prefer your `notify_url` webhook for authoritative state
        transitions.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: transaction_id
          in: path
          required: true
          description: The transaction identifier returned from `/payments/collect`.
          schema:
            type: string
            example: txn_8f3a4c2e9b1d7a6f5c0e8d
      responses:
        '200':
          description: Status retrieved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentStatusResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/collect-status/{transaction_id}:
    get:
      tags: [Status]
      operationId: getCollectStatus
      summary: Status of a `/payments/collect` transaction
      description: |
        Variant of [`/payments/status`](#tag/Status/operation/getPaymentStatus)
        for collect-flow payments. Identical shape with an added
        `customer_reference` field.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: transaction_id
          in: path
          required: true
          schema:
            type: string
            example: txn_8f3a4c2e9b1d7a6f5c0e8d
      responses:
        '200':
          description: Status retrieved.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaymentStatusResponse'
                  - type: object
                    required: [customer_reference]
                    properties:
                      customer_reference:
                        type: string
                        example: cust_abc123
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/check-status/{order_id}:
    get:
      tags: [Status, Cards]
      operationId: checkOrderStatus
      summary: Refresh the status of a Mastercard order
      description: |
        Calls the upstream Mastercard API to refresh the order's
        status, then mirrors the result into the local transaction
        record. Use this for hosted-session card flows; for everything
        else prefer
        [`/payments/status`](#tag/Status/operation/getPaymentStatus).
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: order_id
          in: path
          required: true
          description: The Mastercard order identifier (`successIndicator` from `/payments/collect`).
          schema:
            type: string
            example: order_2026_05_05_8842
      responses:
        '200':
          description: Status refreshed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MastercardOrderStatusResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/transaction/{encrypted_payment_id}:
    get:
      tags: [Status]
      operationId: getTransactionDetails
      summary: Full transaction record by encrypted payment ID
      description: |
        Returns the complete transaction view, including the resolved
        `payment_methods` list when the original collect was created
        with `payment_method: none`.

        > `encrypted_payment_id` is opaque to clients — pass it
        > through verbatim from earlier responses.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: encrypted_payment_id
          in: path
          required: true
          description: Opaque encrypted payment identifier.
          schema:
            type: string
            example: pay_encrypted_abc123def456
      responses:
        '200':
          description: Transaction details retrieved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionDetailsEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalError' }
      x-sdk-methods:
        flutter:
          package: pdirect_pay
          symbol: PdirectPaymentService.getTransactionDetails
          since: 3.0.0
        angular:
          package: "@mms/pdirect-pay"
          symbol: PdirectPaymentService.getTransactionDetails
          since: 1.0.0

  /payments/mastercard/status/{transaction_id}:
    get:
      tags: [Status, Cards]
      operationId: getMastercardStatus
      summary: Refresh a Mastercard transaction's status
      description: |
        Calls Mastercard's API to refresh the card transaction's
        status, then mirrors the result locally. Used by the Flutter
        SDK's hosted-checkout WebView to drive the post-3DS spinner.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: transaction_id
          in: path
          required: true
          schema:
            type: string
            example: pay_encrypted_abc123def456
      responses:
        '200':
          description: Status refreshed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MastercardOrderStatusResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/mobile-money/status/{payment_id}:
    get:
      tags: [Status]
      operationId: getMobileMoneyStatus
      summary: Poll a mobile-money payment for live status
      description: |
        Polls the upstream provider (Onafriq) for the latest
        mobile-money state. Returns immediately if the local record is
        already terminal (`approved`, `failed`, `cancelled`).

        Both SDKs use this internally; client polling defaults to a
        five-second interval with a 60-attempt cap.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: payment_id
          in: path
          required: true
          schema:
            type: string
            minLength: 10
            example: pay_encrypted_abc123def456
      responses:
        '200':
          description: Status retrieved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MobileMoneyStatusEnvelope'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/card/submit:
    post:
      tags: [Cards, Payments]
      operationId: submitCardPayment
      summary: Submit a card payment (hosted session or 3DS challenge)
      description: |
        Card-specific submission entry point. Returns `200` on
        immediate success, `202` while the customer is in a 3DS
        challenge, or `400` on a hard decline. The body shape is the
        same `PaymentSubmitRequest` used by
        [`/payments/submit`](#tag/Payments/operation/submitPayment) —
        populate the card-related fields.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentSubmitRequest'
      responses:
        '200':
          description: Card captured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardSubmitResponse'
        '202':
          description: Card payment is in flight; customer is in a 3DS challenge.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardSubmitResponse'
              example:
                success: true
                status_code: 202
                payment_id: pay_encrypted_abc123def456
                status: processing
                message: 3DS challenge issued
                redirect_url: https://3ds.example.com/auth?token=...
                submitted_at: "2026-05-05T10:16:00Z"
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/card/3ds-callback:
    post:
      tags: [Cards]
      operationId: handle3dsCallback
      summary: Mastercard 3DS authentication callback
      description: |
        Hit by Mastercard out-of-band after the customer completes the
        3DS challenge. Accepts either `application/json` or
        `application/x-www-form-urlencoded`.

        When `MASTERCARD_WEBHOOK_SECRET` is configured on the gateway,
        the request must include an `x-signature` header containing
        `hex(HMAC-SHA256(secret, raw_body))`. Mismatched signatures
        return `401` with `error_code: "1304"`.
      security:
        - sessionToken: []
      parameters:
        - name: x-signature
          in: header
          required: false
          description: Hex-encoded HMAC-SHA256 of the raw body (when configured).
          schema:
            type: string
            example: 9f8e7d6c5b4a3928176054321aabbccdd11ee
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreeDSCallback'
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                order_id:
                  type: string
                auth_status:
                  type: string
      responses:
        '200':
          description: Callback processed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreeDSCallbackResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/process-payment-after-card-update:
    post:
      tags: [Cards]
      operationId: processPaymentAfterCardUpdate
      summary: Capture a card payment after the SDK has updated the Mastercard session
      description: |
        Called by the SDK after pushing real card data into the
        Mastercard hosted session. The gateway validates Mastercard's
        response and either marks the local transaction approved
        (`result == SUCCESS && order.status == CAPTURED`) or returns
        a structured failure envelope.

        Inputs are accepted as either query parameters or
        `application/x-www-form-urlencoded` body.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: session_id
          in: query
          required: true
          schema: { type: string, example: "SESSION0002826410421L29410421" }
        - name: order_id
          in: query
          required: true
          schema: { type: string, example: "order_2026_05_05_8842" }
        - name: transaction_id
          in: query
          required: true
          schema: { type: string, example: "txn_8f3a4c2e9b1d7a6f5c0e8d" }
        - name: amount
          in: query
          required: true
          schema: { type: number, format: float, example: 12.50 }
        - name: currency
          in: query
          required: false
          schema: { type: string, default: "USD", example: "usd" }
      responses:
        '200':
          description: Result envelope (check `success`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardCaptureResult'
        '400':
          description: Hard failure on the upstream call.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardCaptureResult'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/pay:
    post:
      tags: [Cards]
      operationId: payWithSession
      summary: Pay using a Mastercard session (low-level)
      description: |
        Lower-level companion to
        [`/payments/process-payment-after-card-update`](#tag/Cards/operation/processPaymentAfterCardUpdate).
        Triggers the Mastercard `pay` operation directly. Most clients
        should prefer the higher-level endpoints.
      security:
        - sessionToken: []
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
        - name: order_id
          in: query
          required: true
          schema: { type: string, example: "order_2026_05_05_8842" }
        - name: txn_id
          in: query
          required: true
          schema: { type: string, example: "txn_8f3a4c2e9b1d7a6f5c0e8d" }
        - name: session_id
          in: query
          required: true
          schema: { type: string, example: "SESSION0002826410421L29410421" }
        - name: amount
          in: query
          required: true
          schema: { type: number, format: float, example: 12.50 }
        - name: currency
          in: query
          required: false
          schema: { type: string, default: "USD", example: "usd" }
      responses:
        '200':
          description: Mastercard `pay` response, mapped through the gateway.
          content:
            application/json:
              schema:
                type: object
                properties:
                  order_id:
                    type: string
                    example: order_2026_05_05_8842
                  transaction_id:
                    type: string
                    example: txn_8f3a4c2e9b1d7a6f5c0e8d
                  status:
                    type: string
                    example: captured
                  message:
                    type: string
                    example: Payment captured
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/get-config-bank:
    get:
      tags: [Public]
      operationId: getBankConfig
      summary: List banks supported for bank-transfer collections
      description: |
        Unauthenticated. Returns the bank list the SDK uses to render
        bank-transfer pickers, plus locale-aware metadata.
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      responses:
        '200':
          description: Bank configuration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BankConfigResponse'
        '500': { $ref: '#/components/responses/InternalError' }
      x-sdk-methods:
        flutter:
          package: pdirect_pay
          symbol: PdirectCommonService.fetchAvailableBanks
          since: 3.0.0

  /payments/countries/fetch/countries-available:
    get:
      tags: [Public]
      operationId: getCountriesAvailable
      summary: List countries and their available payment methods
      description: |
        Unauthenticated. Use this to populate country pickers and to
        decide which payment methods to surface in your UI.
      parameters:
        - $ref: '#/components/parameters/AcceptLanguage'
      responses:
        '200':
          description: Country list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CountriesResponse'
        '500': { $ref: '#/components/responses/InternalError' }

  /payments/health:
    get:
      tags: [Public]
      operationId: paymentsHealth
      summary: Payments subsystem health
      description: Lightweight payment-subsystem liveness probe. Always returns `200`.
      responses:
        '200':
          description: Healthy.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentsHealthResponse'

  /health:
    get:
      tags: [Public]
      operationId: gatewayHealth
      summary: Gateway liveness probe
      description: Top-level liveness probe. Always returns `200` when the gateway is up.
      responses:
        '200':
          description: Healthy.
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    type: string
                    enum: [healthy]
                    example: healthy

components:

  securitySchemes:
    merchantSecret:
      type: http
      scheme: bearer
      description: |
        Server-to-server authentication for your **merchant backend**.
        The `Authorization` header value is `<key_id>:<cleartext>`,
        joined by a single colon — for example:

        ```
        Authorization: Bearer mch_3f9a2e1b:sk_live_abcdef0123456789...
        ```

        The colon lets the gateway look up the key row by its public
        identifier before running the (intentionally slow) Argon2id
        verification on the cleartext.

        Apply this scheme **only** to `/internal/sessions/create`. Never
        send a `merchant_secret` from a mobile or browser client.
    sessionToken:
      type: http
      scheme: bearer
      description: |
        Per-checkout token minted via
        [`/internal/sessions/create`](#tag/Sessions/operation/createSession).
        Format: `sess_<base64url>`. Single-use — once
        [`/payments/submit`](#tag/Payments/operation/submitPayment)
        succeeds, the underlying session is consumed and the token
        cannot be reused.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        UUID v4 identifying this attempt. Same key + same body within
        24 h replays the cached response; same key + different body
        returns `409`. Recommended on every `POST` to make retries
        safe.
      schema:
        type: string
        format: uuid
        example: 7c2e9a1d-4b6f-4c8e-9a5f-1d3e2c7b9a4d
    AcceptLanguage:
      name: Accept-Language
      in: header
      required: false
      description: ISO 639-1 language code. Drives translated error messages.
      schema:
        type: string
        default: fr
        example: en
    DeviceFingerprint:
      name: X-Device-Fingerprint
      in: header
      required: false
      description: |
        SHA-256 of stable device attributes. The Pdirect SDKs attach
        this automatically; raw HTTP clients can omit it unless the
        merchant pre-declared a fingerprint at session-mint time.
      schema:
        type: string
        minLength: 8
        maxLength: 512
        example: 9f8e7d6c5b4a3928176054321aabbccdd11ee

  responses:
    BadRequest:
      description: Validation or business-logic failure.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PaymentGatewayError'
          example:
            error: true
            error_code: "1101"
            message: Invalid payment amount
            timestamp: "2026-05-05T10:15:00Z"
            request_id: req_8f3a4c2e9b1d7a6f
            retryable: false
    Unauthorized:
      description: Missing, invalid, or revoked credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PaymentGatewayError'
          example:
            error: true
            error_code: "1001"
            message: Invalid or expired session token
            timestamp: "2026-05-05T10:15:00Z"
            request_id: req_8f3a4c2e9b1d7a6f
            retryable: false
    NotFound:
      description: The requested transaction does not exist or is not visible to this caller.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PaymentGatewayError'
          example:
            error: true
            error_code: "1204"
            message: Transaction not found
            timestamp: "2026-05-05T10:15:00Z"
            request_id: req_8f3a4c2e9b1d7a6f
            retryable: false
    ValidationError:
      description: Pydantic-level validation error (malformed JSON, type mismatch).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PaymentGatewayError'
          example:
            error: true
            error_code: "1110"
            message: Invalid field format
            details:
              - field: amount
                message: ensure this value is greater than 0
                code: AMOUNT_TOO_SMALL
            timestamp: "2026-05-05T10:15:00Z"
            request_id: req_8f3a4c2e9b1d7a6f
            retryable: false
    RateLimited:
      description: Too many requests for this session.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 60
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PaymentGatewayError'
          example:
            error: true
            error_code: "1301"
            message: Rate limit exceeded
            timestamp: "2026-05-05T10:15:00Z"
            request_id: req_8f3a4c2e9b1d7a6f
            retryable: true
            retry_after: 60
    InternalError:
      description: Unexpected server-side failure.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PaymentGatewayError'
          example:
            error: true
            error_code: "1401"
            message: Internal server error
            timestamp: "2026-05-05T10:15:00Z"
            request_id: req_8f3a4c2e9b1d7a6f
            support_reference: sup_987654321
            retryable: true

  schemas:

    Currency:
      type: string
      enum: [usd, cdf, xof]
      description: Lowercase ISO 4217 currency code.
      example: usd

    PaymentMethod:
      type: string
      enum: [card, mobile_money, wallet, flash, bank_transfer, google_pay, apple_pay, none]
      description: |
        - `none` — defer choice; the SDK fetches available methods via
          [`/payments/transaction/{encrypted_payment_id}`](#tag/Status/operation/getTransactionDetails).
        - `card` — Mastercard hosted session (or, in future, direct capture).
        - `mobile_money` — Onafriq / partner mobile money rails.
        - `wallet` — Pdirect e-wallet.
        - `flash` — Flash voucher.
        - `bank_transfer` — bank account collection.
        - `google_pay` / `apple_pay` — native pay tokens.
      example: wallet

    PaymentStatus:
      type: string
      description: Canonical payment lifecycle states.
      enum:
        - pending
        - processing
        - approved
        - declined
        - failed
        - cancelled
        - expired
        - refunded
        - pending_otp_verification
        - pending_mobile_money_verification
        - pending_email_verification
        - pending_bank_validation
        - pending_bank_proof_upload
        - bank_payment_validated
        - pending_bank_submission
        - session_created
      example: pending

    ErrorCode:
      type: string
      description: |
        Stable error code. The leading digit groups the category:
        `10xx` auth, `11xx` validation, `12xx` business logic,
        `13xx` security, `14xx` system, `15xx` payment processor.
      enum:
        - "1001"
        - "1002"
        - "1003"
        - "1004"
        - "1005"
        - "1101"
        - "1102"
        - "1103"
        - "1104"
        - "1105"
        - "1106"
        - "1107"
        - "1108"
        - "1109"
        - "1110"
        - "1201"
        - "1202"
        - "1203"
        - "1204"
        - "1205"
        - "1206"
        - "1207"
        - "1301"
        - "1302"
        - "1303"
        - "1304"
        - "1305"
        - "1401"
        - "1402"
        - "1403"
        - "1404"
        - "1405"
        - "1501"
        - "1502"
        - "1503"
        - "1504"
        - "1505"
      example: "1101"

    ErrorDetail:
      type: object
      required: [message]
      properties:
        field:
          type:
            - string
            - "null"
          description: The request field this detail relates to, when applicable.
          example: amount
        message:
          type: string
          description: Human-readable description of the issue.
          example: Amount must be greater than 0
        code:
          type:
            - string
            - "null"
          description: Internal sub-code for the validation failure.
          example: AMOUNT_TOO_SMALL

    PaymentGatewayError:
      type: object
      description: |
        Standard error envelope. Returned for every `4xx` and `5xx`
        response. Use the `error_code` (not the HTTP status) for
        programmatic handling.
      required: [error, error_code, message, timestamp]
      properties:
        error:
          type: boolean
          description: Always `true` for error envelopes.
          example: true
        error_code:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
          example: Invalid payment amount
        details:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/ErrorDetail'
        timestamp:
          type: string
          format: date-time
          example: "2026-05-05T10:15:00Z"
        request_id:
          type:
            - string
            - "null"
          description: Unique request identifier for tracking and support.
          example: req_8f3a4c2e9b1d7a6f
        support_reference:
          type:
            - string
            - "null"
          example: sup_987654321
        transaction_id:
          type:
            - string
            - "null"
          example: txn_8f3a4c2e9b1d7a6f5c0e8d
        gateway_app_id:
          type:
            - string
            - "null"
          example: app_123
        retryable:
          type: boolean
          default: false
          example: false
        retry_after:
          type:
            - integer
            - "null"
          description: Seconds to wait before retrying.
          example: 60
        documentation_url:
          type:
            - string
            - "null"
          format: uri
          example: https://docs.pdirect.com/errors/1101

    MerchantInfo:
      type: object
      description: Merchant branding shown in the SDK checkout chrome.
      properties:
        name:
          type: string
          example: My Store
        url:
          type:
            - string
            - "null"
          format: uri
          example: https://mystore.example.com
        logo:
          type:
            - string
            - "null"
          format: uri
          example: https://mystore.example.com/logo.png

    SessionCreateRequest:
      type: object
      required: [amount, currency, customer_reference]
      properties:
        amount:
          type: string
          minLength: 1
          maxLength: 32
          description: |
            Amount the SDK will charge, encoded as a positive decimal
            string. Stored verbatim and string-compared on rebind, so
            `"1.00"` and `"1"` are not equivalent.
          example: "12.50"
        currency:
          $ref: '#/components/schemas/Currency'
        customer_reference:
          type: string
          minLength: 1
          maxLength: 128
          description: Merchant-assigned customer or order reference. Bound to this session.
          example: cust_abc123
        ttl_seconds:
          type:
            - integer
            - "null"
          minimum: 60
          maximum: 3600
          default: 1800
          description: Session lifetime in seconds. Default 1800 (30 min). Maximum 3600 (1 h).
          example: 1800
        device_fingerprint:
          type:
            - string
            - "null"
          minLength: 8
          maxLength: 512
          description: |
            Optional pre-declared fingerprint. When set, the gateway
            enforces a strict match on every SDK request bound to this
            session.
          example: 9f8e7d6c5b4a3928176054321aabbccdd11ee

    SessionCreateResponse:
      type: object
      required: [session_id, session_token, issued_at, expires_at]
      properties:
        session_id:
          type: string
          description: Stable identifier for the session row. Use this to revoke.
          example: "607f1f77bcf86cd799439011"
        session_token:
          type: string
          description: Cleartext bearer the SDK will present. **Returned exactly once.**
          example: "sess_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"
        issued_at:
          type: string
          format: date-time
          example: "2026-05-05T10:15:00Z"
        expires_at:
          type: string
          format: date-time
          example: "2026-05-05T10:45:00Z"

    PaymentCollectionRequest:
      type: object
      required:
        - payment_method
        - user_info
        - payment_method_info
        - fee_covered_by
        - delivery_behaviour
        - notify_url
      description: |
        Body of [`/payments/collect`](#tag/Payments/operation/collectPayment).

        `amount`, `currency`, and `customer_reference` are **bound to
        the `session_token`** at mint time
        ([`POST /internal/sessions/create`](#tag/Sessions/operation/createSession))
        — the gateway reads them straight off the session row and does
        not require them in the body.

        These three fields remain accepted for backwards compatibility.
        When present, the gateway runs a soft check against the session
        binding: any drift is a `400` (`error_code` `1101` for amount,
        `1102` for currency, `1103` for customer_reference). New
        integrations should omit them.
      properties:
        customer_reference:
          type: string
          description: |
            Optional. Bound to the session at mint time; only include
            if echoing the value the session was minted with.
          example: cust_abc123
        amount:
          type: string
          description: |
            Optional. Bound to the session at mint time. When present,
            must string-match the session's `bound_amount` exactly.
          example: "12.50"
        currency:
          allOf:
            - $ref: '#/components/schemas/Currency'
          description: |
            Optional. Bound to the session at mint time. When present,
            must match the session's `bound_currency`.
        payment_method:
          $ref: '#/components/schemas/PaymentMethod'
        user_info:
          type: string
          enum: [full, none]
          description: Whether the merchant has already collected user info.
          example: full
        payment_method_info:
          type: string
          enum: [full, none]
          description: Whether the merchant has already collected payment-method details.
          example: full
        fee_covered_by:
          type: string
          enum: [seller, buyer]
          example: buyer
        delivery_behaviour:
          type: string
          enum: [direct_delivery, hold_delivery, reversal_delivery]
          example: direct_delivery
        notify_url:
          type: string
          format: uri
          description: Webhook URL that must be on the merchant allow-list. `https://` required in production.
          example: https://merchant.example.com/pdirect/webhook
        success_redirect_url:
          type:
            - string
            - "null"
          format: uri
          example: https://merchant.example.com/checkout/success
        fail_redirect_url:
          type:
            - string
            - "null"
          format: uri
          example: https://merchant.example.com/checkout/fail
        merchant:
          oneOf:
            - $ref: '#/components/schemas/MerchantInfo'
            - type: "null"
        cfg_system_country_id:
          type:
            - string
            - "null"
          example: sys_country_cd
        first_name:
          type:
            - string
            - "null"
          example: John
        last_name:
          type:
            - string
            - "null"
          example: Doe
        email:
          type:
            - string
            - "null"
          format: email
          example: john.doe@example.com
        phone_number:
          type:
            - string
            - "null"
          description: E.164 (`+CCNNNNNNNN`).
          example: "+243998857000"
        wallet_number:
          type:
            - string
            - "null"
        wallet_pin:
          type:
            - string
            - "null"
          format: password
        flash_number:
          type:
            - string
            - "null"
        flash_pin:
          type:
            - string
            - "null"
          format: password
        mobile_money_phone_number:
          type:
            - string
            - "null"
          example: "+243998857000"
        card_number:
          type:
            - string
            - "null"
          format: password
        card_expiry_date:
          type:
            - string
            - "null"
          description: "`MM/YYYY`."
          example: "12/2027"
        card_cvv:
          type:
            - string
            - "null"
          format: password
        card_holder_name:
          type:
            - string
            - "null"
        transactional_currency:
          type:
            - string
            - "null"
          description: Override the currency the gateway debits the customer in.
          example: cdf
        mastercard_payment_method:
          type:
            - string
            - "null"
          enum: [HOSTED_SESSION, DIRECT_CAPTURE, null]
          description: |
            `HOSTED_SESSION` is the production-ready path.
            `DIRECT_CAPTURE` is reserved for future use.
        native_pay_token:
          type:
            - string
            - "null"
          description: Google Pay / Apple Pay token payload.
        native_pay_token_type:
          type:
            - string
            - "null"

    PaymentCollectionResponse:
      type: object
      required: [success, transaction_id, status, message, created_at]
      properties:
        success:
          type: boolean
          example: true
        transaction_id:
          type: string
          example: txn_8f3a4c2e9b1d7a6f5c0e8d
        status:
          $ref: '#/components/schemas/PaymentStatus'
        payment_url:
          type:
            - string
            - "null"
          format: uri
        message:
          type: string
          example: Payment request received
        amount:
          type:
            - string
            - "null"
          example: "12.50"
        currency:
          type:
            - string
            - "null"
          example: usd
        fee_amount:
          type:
            - string
            - "null"
          example: "0.50"
        total_amount:
          type:
            - string
            - "null"
          example: "13.00"
        created_at:
          type: string
          format: date-time
          example: "2026-05-05T10:15:00Z"
        expires_at:
          type:
            - string
            - "null"
          format: date-time
        additional_data:
          type:
            - object
            - "null"
          additionalProperties: true
        system_ref:
          type:
            - string
            - "null"

    B2CBeneficiary:
      type: object
      required: [amount]
      properties:
        phone_number:
          type:
            - string
            - "null"
          description: Required when `receipt_mode == mobile_money`.
          example: "+243998857000"
        amount:
          type: string
          description: Positive decimal string.
          example: "10.00"
        wallet_number:
          type:
            - string
            - "null"
          description: Required when `receipt_mode == wallet`.
          example: "3005710720108954"

    B2CBeneficiaryResult:
      type: object
      required: [phone_number, amount, currency, status, message]
      properties:
        phone_number:
          type:
            - string
            - "null"
          example: "+243998857000"
        amount:
          type: string
          example: "10.00"
        currency:
          type: string
          example: usd
        status:
          type: string
          enum: [pending, processing, completed, failed]
          example: completed
        transaction_id:
          type:
            - string
            - "null"
          example: txn_b2c_8f3a4c
        message:
          type: string
          example: Payout sent
        fee_amount:
          type:
            - string
            - "null"
          example: "0.10"

    B2CPaymentRequest:
      type: object
      required: [customer_reference, currency, receipt_mode, notify_url, beneficiaries]
      properties:
        customer_reference:
          type: string
          example: payout_2026_05_05_001
        currency:
          $ref: '#/components/schemas/Currency'
        receipt_mode:
          type: string
          enum: [mobile_money, wallet]
          example: mobile_money
        notify_url:
          type: string
          format: uri
          example: https://merchant.example.com/pdirect/webhook
        beneficiaries:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/B2CBeneficiary'
        sending_type:
          type: string
          enum: [immediate, scheduled, recurring, none]
          default: immediate
          example: immediate
        scheduled_dates:
          type:
            - array
            - "null"
          description: Required when `sending_type == scheduled`. ISO `YYYY-MM-DD`.
          items:
            type: string
            format: date
          example: ["2026-05-15", "2026-06-15"]
        recurring_frequency:
          type:
            - string
            - "null"
          enum: [daily, weekly, biweekly, monthly, none, null]
          example: monthly
        recurring_start_date:
          type:
            - string
            - "null"
          format: date
          example: "2026-05-15"
        recurring_occurrences:
          type:
            - integer
            - "null"
          minimum: 1
          example: 12
        note:
          type:
            - string
            - "null"
          example: Salary payment

    B2CPaymentResponse:
      type: object
      required:
        - batch_id
        - customer_reference
        - total_amount
        - total_fee
        - successful_count
        - failed_count
        - pending_count
        - status
        - beneficiaries
        - created_at
      properties:
        batch_id:
          type: string
          example: batch_xyz789
        customer_reference:
          type: string
          example: payout_2026_05_05_001
        total_amount:
          type: string
          example: "100.00"
        total_fee:
          type: string
          example: "2.50"
        successful_count:
          type: integer
          example: 8
        failed_count:
          type: integer
          example: 0
        pending_count:
          type: integer
          example: 2
        status:
          type: string
          enum: [pending, processing, completed, partial_success, failed]
          example: partial_success
        beneficiaries:
          type: array
          items:
            $ref: '#/components/schemas/B2CBeneficiaryResult'
        created_at:
          type: string
          format: date-time
          example: "2026-05-05T10:15:00Z"
        completed_at:
          type:
            - string
            - "null"
          format: date-time
          example: "2026-05-05T10:20:00Z"
        additional_data:
          type:
            - object
            - "null"
          additionalProperties: true

    PaymentSubmitRequest:
      type: object
      required: [payment_id, payment_method_id]
      description: |
        Body of [`/payments/submit`](#tag/Payments/operation/submitPayment)
        and [`/payments/card/submit`](#tag/Cards/operation/submitCardPayment).
        Populate the fields appropriate to the channel selected at
        `/payments/collect`.
      properties:
        payment_id:
          type: string
          description: Encrypted payment identifier from `/payments/collect`.
          example: pay_encrypted_abc123def456
        payment_method_id:
          type: string
          example: pm_card_001
        first_name:
          type:
            - string
            - "null"
          example: John
        last_name:
          type:
            - string
            - "null"
          example: Doe
        email:
          type:
            - string
            - "null"
          format: email
          example: john.doe@example.com
        phone_number:
          type:
            - string
            - "null"
          example: "+243998857000"
        ewallet_number:
          type:
            - string
            - "null"
          example: "3005710720108954"
        ewallet_pin:
          type:
            - string
            - "null"
          format: password
        mobile_money_phone_number:
          type:
            - string
            - "null"
          example: "+243998857000"
        flash_number:
          type:
            - string
            - "null"
        flash_pin:
          type:
            - string
            - "null"
          format: password
        card_number:
          type:
            - string
            - "null"
          format: password
        card_expiry_date:
          type:
            - string
            - "null"
          description: "`MM/YY`."
          example: "12/27"
        card_cvv:
          type:
            - string
            - "null"
          format: password
        card_holder_name:
          type:
            - string
            - "null"
          example: John Doe
        bank_account_id:
          type:
            - string
            - "null"
        bank_account_number:
          type:
            - string
            - "null"
        bank_account_label:
          type:
            - string
            - "null"
        bank_id:
          type:
            - string
            - "null"
        bank_entity_id:
          type:
            - string
            - "null"
        mastercard_payment_method:
          type:
            - string
            - "null"
          enum: [HOSTED_SESSION, DIRECT_CAPTURE, null]
        session_id:
          type:
            - string
            - "null"
          description: Mastercard hosted-session ID.
          example: SESSION0002826410421L29410421
        card_data:
          type:
            - object
            - "null"
          additionalProperties: true
          description: Encrypted card data envelope from a hosted-session capture.
        save_payment_method:
          type:
            - boolean
            - "null"
          default: false

    PaymentSubmitResponse:
      type: object
      required: [success, payment_id, status, message, submitted_at]
      properties:
        success:
          type: boolean
          example: true
        payment_id:
          type: string
          example: pay_encrypted_abc123def456
        status:
          type: string
          example: processing
        message:
          type: string
          example: Payment submitted successfully
        transaction_reference:
          type:
            - string
            - "null"
          example: ref_12345
        processing_fee:
          type:
            - number
            - "null"
          format: float
          example: 0.50
        total_amount:
          type:
            - number
            - "null"
          format: float
          example: 13.00
        redirect_url:
          type:
            - string
            - "null"
          format: uri
          description: Present when the customer must be redirected (e.g. 3DS).
        callback_url:
          type:
            - string
            - "null"
          format: uri
        submitted_at:
          type: string
          format: date-time
          example: "2026-05-05T10:16:00Z"
        estimated_completion:
          type:
            - string
            - "null"
          format: date-time

    CardSubmitResponse:
      type: object
      required: [success, payment_id, status, message, submitted_at]
      properties:
        success:
          type: boolean
        status_code:
          type: integer
          enum: [200, 202, 400]
        payment_id:
          type: string
          example: pay_encrypted_abc123def456
        status:
          type: string
          example: completed
        message:
          type: string
        transaction_reference:
          type:
            - string
            - "null"
        processing_fee:
          type:
            - number
            - "null"
          format: float
          example: 0.50
        total_amount:
          type:
            - number
            - "null"
          format: float
          example: 100.00
        submitted_at:
          type: string
          format: date-time
        estimated_completion:
          type:
            - string
            - "null"
          format: date-time
        redirect_url:
          type:
            - string
            - "null"
          format: uri
        callback_url:
          type:
            - string
            - "null"
          format: uri

    PaymentOTPVerifyRequest:
      type: object
      required: [payment_id, otp_code]
      properties:
        payment_id:
          type: string
          example: pay_encrypted_abc123def456
        otp_code:
          type: string
          minLength: 6
          maxLength: 6
          description: Six-digit one-time password.
          example: "482915"

    PaymentOTPVerifyResponse:
      type: object
      required: [success, payment_id, status, message, verified_at]
      properties:
        success:
          type: boolean
        payment_id:
          type: string
        status:
          type: string
          example: approved
        message:
          type: string
        transaction_reference:
          type:
            - string
            - "null"
        debit_amount:
          type:
            - number
            - "null"
          format: float
          example: 12.50
        wallet_balance:
          type:
            - number
            - "null"
          format: float
          example: 87.50
        emails_sent:
          type:
            - array
            - "null"
          items:
            type: string
            format: email
        verified_at:
          type: string
          format: date-time
        completed_at:
          type:
            - string
            - "null"
          format: date-time

    PaymentResendOTPRequest:
      type: object
      required: [payment_id]
      properties:
        payment_id:
          type: string
          example: pay_encrypted_abc123def456

    RiskRule:
      type: object
      properties:
        name:
          type:
            - string
            - "null"
        type:
          type:
            - string
            - "null"
        recommendation:
          type:
            - string
            - "null"
        data:
          type:
            - string
            - "null"

    PaymentResendOTPResponse:
      type: object
      required: [success, payment_id, status, message, resent_at]
      properties:
        success:
          type: boolean
        payment_id:
          type: string
        status:
          type: string
          example: pending_otp_verification
        message:
          type: string
        otp_sent_to:
          type:
            - string
            - "null"
          description: Masked destination (e.g. `j***@example.com`, `+24399****000`).
        resend_count:
          type:
            - integer
            - "null"
        max_resends:
          type:
            - integer
            - "null"
          example: 3
        resent_at:
          type: string
          format: date-time
        expires_at:
          type:
            - string
            - "null"
          format: date-time
        gateway_entry_point:
          type:
            - string
            - "null"
        gateway_code:
          type:
            - string
            - "null"
        gateway_message:
          type:
            - string
            - "null"
        risk_gateway_code:
          type:
            - string
            - "null"
        risk_decision:
          type:
            - string
            - "null"
        risk_rules:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/RiskRule'
        raw_gateway:
          type:
            - object
            - "null"
          additionalProperties: true

    PaymentStatusResponse:
      type: object
      required: [transaction_id, status, amount, currency, created_at]
      properties:
        transaction_id:
          type: string
          example: txn_8f3a4c2e9b1d7a6f5c0e8d
        status:
          $ref: '#/components/schemas/PaymentStatus'
        amount:
          type: string
          example: "12.50"
        currency:
          type: string
          example: usd
        created_at:
          type: string
          format: date-time
          example: "2026-05-05T10:15:00Z"
        completed_at:
          type:
            - string
            - "null"
          format: date-time
        failure_reason:
          type:
            - string
            - "null"
          example: Insufficient funds
        additional_data:
          type:
            - object
            - "null"
          additionalProperties: true

    PaymentMethodOption:
      type: object
      required: [method, display_name, available]
      properties:
        method:
          $ref: '#/components/schemas/PaymentMethod'
        display_name:
          type: string
          example: Credit Card
        available:
          type: boolean
          example: true

    TransactionDetailsEnvelope:
      type: object
      description: Envelope wrapper around the inner transaction record.
      required: [status_code, message]
      properties:
        status_code:
          type: integer
          example: 200
        message:
          type: string
          example: Transaction details retrieved successfully
        payment_id:
          type: string
          example: pay_encrypted_abc123def456
        transactional_amount:
          type: string
          example: "12.50"
        transactional_currency:
          type: string
          example: usd
        description:
          type:
            - string
            - "null"
          example: Payment for Order #123
        channel:
          type: string
          enum: [none, card, eflash, ewallet, mobile_money, cash, bank_transfer, google_pay, apple_pay]
          example: none
        merchant_name:
          type:
            - string
            - "null"
        merchant_url:
          type:
            - string
            - "null"
          format: uri
        merchant_logo:
          type:
            - string
            - "null"
          format: uri
        filling_info:
          type: string
          enum: [full, none]
          example: none
        channel_info:
          type: string
          enum: [full, none]
          example: none
        payment_methods:
          type:
            - array
            - "null"
          items:
            $ref: '#/components/schemas/PaymentMethodOption'
        status:
          $ref: '#/components/schemas/PaymentStatus'
        created_at:
          type: string
          format: date-time

    MastercardOrderStatusResponse:
      type: object
      description: Pass-through summary of the upstream Mastercard order status.
      properties:
        transaction_id:
          type: string
          example: txn_8f3a4c2e9b1d7a6f5c0e8d
        order_id:
          type: string
          example: order_2026_05_05_8842
        mastercard_order_id:
          type:
            - string
            - "null"
        status:
          type: string
          example: CAPTURED
        result:
          type: string
          example: SUCCESS
        message:
          type:
            - string
            - "null"
          example: Card payment captured successfully
        updated_at:
          type:
            - string
            - "null"
          format: date-time
      additionalProperties: true

    MobileMoneyStatusEnvelope:
      type: object
      required: [status_code, message, data]
      properties:
        status_code:
          type: integer
          example: 200
        message:
          type: string
          example: Status retrieved successfully
        data:
          type: object
          required: [status, progress, message]
          properties:
            status:
              type: string
              enum: [pending, processing, completed, failed, cancelled]
              example: processing
            progress:
              type: integer
              minimum: 0
              maximum: 100
              example: 50
            message:
              type: string
              example: Payment is being processed
            payment_info:
              type:
                - object
                - "null"
              additionalProperties: true
            provider_reference:
              type:
                - string
                - "null"
              example: onafriq_ref_abc123
            estimated_completion:
              type:
                - string
                - "null"
              format: date-time

    ThreeDSCallback:
      type: object
      description: |
        3DS callback shape from Mastercard. Either the structured form
        below (preferred) or a flat `order_id` + `auth_status` form
        body is accepted.
      properties:
        order:
          type: object
          additionalProperties: true
          properties:
            id:
              type: string
              example: order_2026_05_05_8842
        "3DSecure":
          type: object
          additionalProperties: true
          properties:
            summaryStatus:
              type: string
              enum: [Y, N, U, A, C, R]
              example: Y

    ThreeDSCallbackResponse:
      type: object
      required: [success, status, message, order_id, timestamp]
      properties:
        success:
          type: boolean
        status:
          type: string
          enum: [approved, declined]
          example: approved
        message:
          type: string
          example: 3D Secure authentication processed
        order_id:
          type: string
          example: order_2026_05_05_8842
        redirect_url:
          type:
            - string
            - "null"
          format: uri
        timestamp:
          type: string
          format: date-time

    CardCaptureResult:
      type: object
      required: [success, status, message, order_id, transaction_id, session_id]
      properties:
        success:
          type: boolean
        status:
          type: string
          enum: [approved, declined, failed]
          example: approved
        message:
          type: string
          example: Payment approved successfully
        order_id:
          type: string
          example: order_2026_05_05_8842
        transaction_id:
          type: string
          example: txn_8f3a4c2e9b1d7a6f5c0e8d
        session_id:
          type: string
          example: SESSION0002826410421L29410421
        gateway_response:
          type:
            - object
            - "null"
          additionalProperties: true

    Bank:
      type: object
      properties:
        id:
          type: string
          example: bank_brc_001
        name:
          type: string
          example: Banque Centrale du Congo
        code:
          type:
            - string
            - "null"
          example: BCC
        logo:
          type:
            - string
            - "null"
          format: uri
        currency:
          type:
            - string
            - "null"
          example: cdf
      additionalProperties: true

    BankConfigResponse:
      type: object
      properties:
        banks:
          type: array
          items:
            $ref: '#/components/schemas/Bank'
        configurations:
          type:
            - object
            - "null"
          additionalProperties: true
      additionalProperties: true

    Country:
      type: object
      properties:
        id:
          type: string
          example: sys_country_cd
        code:
          type: string
          example: CD
        name:
          type: string
          example: Democratic Republic of Congo
        currency:
          type: string
          example: cdf
        available_payment_methods:
          type: array
          items:
            $ref: '#/components/schemas/PaymentMethod'
      additionalProperties: true

    CountriesResponse:
      type: object
      properties:
        countries:
          type: array
          items:
            $ref: '#/components/schemas/Country'
      additionalProperties: true

    PaymentsHealthResponse:
      type: object
      required: [status, service]
      properties:
        status:
          type: string
          enum: [healthy]
          example: healthy
        service:
          type: string
          example: pdirect-payment-gateway
        timestamp:
          type:
            - string
            - "null"
          format: date-time
        version:
          type:
            - string
            - "null"
          example: "1.0.0"
