openapi: 3.0.3
info:
  title: MarzPay Demo Merchant API
  description: |
    Official OpenAPI specification for the MarzPay Demo merchant API.

    **Live markets:** Uganda (`UG` / UGX — MTN, Airtel) and Kenya (`KE` / KES — M-Pesa).
    Always send `country` on money-movement requests. Check `GET /collect-money/services` for your enabled countries.

    **Authentication:** HTTP Basic Auth — `base64(api_key:api_secret)`

  version: 1.0.0
  contact:
    name: MarzPay Demo Support
    url: https://demo-wallet.wearemarz.com/documentation

servers:
  - url: https://demo-wallet.wearemarz.com/api/v1
    description: MarzPay API v1

security:
  - BasicAuth: []

tags:
  - name: Balance
    description: Wallet balance (requires IP whitelist)
  - name: Collections
    description: Collect money via mobile money or card
  - name: Send Money
    description: Mobile money disbursements (requires IP whitelist + disbursement permission)
  - name: Bank Transfer
    description: Push funds to bank accounts (requires IP whitelist + disbursement permission)
  - name: Bill Payments
    description: LIGHT, NWSC, DSTV, GOTV utility payments
  - name: Airtime & Data
    description: MTN, Airtel, Lyca airtime and data bundles
  - name: Phone Verification
    description: Uganda mobile number KYC lookup
  - name: Transactions
    description: Transaction history and status
  - name: Services
    description: Subscribed and available services
  - name: Account
    description: Business account (requires IP whitelist)
  - name: Webhooks
    description: Manage dashboard webhooks

paths:
  /balance:
    get:
      tags: [Balance]
      summary: Get account balance
      description: Returns main wallet, card wallet, and balance summaries. Requires IP whitelist.
      responses:
        '200':
          description: Balance retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /balance/history:
    get:
      tags: [Balance]
      summary: Get balance history
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: History retrieved
        '401':
          $ref: '#/components/responses/Unauthorized'

  /collect-money:
    post:
      tags: [Collections]
      summary: Initiate collection
      description: |
        Mobile money (default): customer receives a payment prompt (Uganda MTN/Airtel or Kenya M-Pesa).
        Card: set `method` to `card` (omit `phone_number`); redirect customer to `redirect_url` in response.
        Optional `metadata` is echoed on create (`data.collection.metadata`) and webhooks (top-level `metadata`).
        Do not confuse that with `data.metadata` on some API responses (response diagnostics only).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CollectMoneyRequest'
            examples:
              uganda_mobile_money:
                summary: Uganda mobile money (MTN/Airtel)
                value:
                  amount: 5000
                  phone_number: '+256712345678'
                  reference: c97fae8b-9b7f-4192-9f72-6f0859d33e67
                  country: UG
                  description: Order payment
                  callback_url: https://your-app.com/webhooks/marzpay
                  metadata:
                    - orderId: ORD-123456789
                    - customerId: customer@email.com
                      isPII: true
              kenya_mpesa:
                summary: Kenya M-Pesa collection
                value:
                  amount: 100
                  phone_number: '+254710000000'
                  reference: 123e4567-e89b-12d3-a456-426614174000
                  country: KE
                  description: Payment for services
                  callback_url: https://your-app.com/webhooks/marzpay
                  metadata:
                    - orderId: ORD-123456789
                    - customerId: customer@email.com
                      isPII: true
              card:
                summary: Card payment
                value:
                  amount: 5000
                  method: card
                  reference: 123e4567-e89b-12d3-a456-426614174000
                  country: UG
                  callback_url: https://your-app.com/thank-you
      responses:
        '201':
          description: Collection initiated
        '422':
          $ref: '#/components/responses/ValidationError'

  /collect-money/services:
    get:
      tags: [Collections]
      summary: List collection services
      responses:
        '200':
          description: Services list

  /collect-money/{uuid}:
    get:
      tags: [Collections]
      summary: Get collection by UUID
      parameters:
        - $ref: '#/components/parameters/TransactionUuid'
      responses:
        '200':
          description: Collection details
        '404':
          $ref: '#/components/responses/NotFound'

  /send-money:
    post:
      tags: [Send Money]
      summary: Send money (disbursement)
      description: |
        Requires IP whitelist, API disbursement permission, and sufficient balance.
        Uganda: MTN/Airtel. Kenya: M-Pesa (`country: KE`, `+254` phones, KES amounts).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMoneyRequest'
            examples:
              uganda:
                summary: Uganda send money
                value:
                  amount: 10000
                  phone_number: '+256712345678'
                  reference: payout-2026-07-31-001
                  country: UG
                  description: Salary payment
                  callback_url: https://your-app.com/webhooks/marzpay
                  metadata:
                    - orderId: ORD-123456789
                    - customerId: customer@email.com
                      isPII: true
              kenya_mpesa:
                summary: Kenya M-Pesa send money
                value:
                  amount: 100
                  phone_number: '+254710000000'
                  reference: 123e4567-e89b-12d3-a456-426614174001
                  country: KE
                  description: Payout to customer
                  callback_url: https://your-app.com/webhooks/marzpay
                  metadata:
                    - orderId: ORD-123456789
                    - customerId: customer@email.com
                      isPII: true
      responses:
        '201':
          description: Disbursement initiated
        '422':
          $ref: '#/components/responses/ValidationError'

  /send-money/services:
    get:
      tags: [Send Money]
      summary: List disbursement services and limits
      responses:
        '200':
          description: Services list

  /send-money/{uuid}:
    get:
      tags: [Send Money]
      summary: Get disbursement by UUID
      parameters:
        - $ref: '#/components/parameters/TransactionUuid'
      responses:
        '200':
          description: Disbursement details
        '404':
          $ref: '#/components/responses/NotFound'

  /bank-transfer:
    post:
      tags: [Bank Transfer]
      summary: Create bank transfer
      description: Debits wallet (amount + tiered charge). Validate account first with POST /bank-transfer/validate.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BankTransferRequest'
      responses:
        '201':
          description: Transfer created
        '422':
          $ref: '#/components/responses/ValidationError'

  /bank-transfer/validate:
    post:
      tags: [Bank Transfer]
      summary: Validate bank account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bank_name, account_number]
              properties:
                bank_name:
                  type: string
                  example: Equity Bank
                account_number:
                  type: string
                  example: '60001256421'
      responses:
        '200':
          description: Account validated

  /bank-transfer/banks:
    get:
      tags: [Bank Transfer]
      summary: List supported banks
      responses:
        '200':
          description: Banks list

  /bank-transfer/services:
    get:
      tags: [Bank Transfer]
      summary: Transfer limits and pricing info
      responses:
        '200':
          description: Service settings

  /bank-transfer/{reference}:
    get:
      tags: [Bank Transfer]
      summary: Get transfer by reference
      parameters:
        - $ref: '#/components/parameters/Reference'
      responses:
        '200':
          description: Transfer details
        '404':
          $ref: '#/components/responses/NotFound'

  /bill-payment/verify:
    post:
      tags: [Bill Payments]
      summary: Verify meter or account number
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BillPaymentVerifyRequest'
      responses:
        '200':
          description: Verification successful

  /bill-payment:
    get:
      tags: [Bill Payments]
      summary: List bill payment transactions
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Transaction list
    post:
      tags: [Bill Payments]
      summary: Pay a utility bill
      description: Requires IP whitelist. Wallet debited for amount + service charge.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BillPaymentRequest'
      responses:
        '201':
          description: Payment initiated
        '422':
          $ref: '#/components/responses/ValidationError'

  /bill-payment/services:
    get:
      tags: [Bill Payments]
      summary: Utility service requirements
      responses:
        '200':
          description: Services info

  /bill-payment/nwsc/areas:
    get:
      tags: [Bill Payments]
      summary: List NWSC areas
      responses:
        '200':
          description: Areas list

  /bill-payment/dstv/bouquet-codes:
    get:
      tags: [Bill Payments]
      summary: List DSTV bouquet codes and prices
      responses:
        '200':
          description: Bouquet codes

  /bill-payment/gotv/bouquet-codes:
    get:
      tags: [Bill Payments]
      summary: List GOTV bouquet codes and prices
      responses:
        '200':
          description: Bouquet codes

  /bill-payment/{reference}:
    get:
      tags: [Bill Payments]
      summary: Get bill payment by reference
      parameters:
        - $ref: '#/components/parameters/Reference'
      responses:
        '200':
          description: Payment details

  /airtime-data/catalog:
    get:
      tags: [Airtime & Data]
      summary: List airtime and bundle catalog
      responses:
        '200':
          description: Catalog

  /airtime-data/detect-network:
    get:
      tags: [Airtime & Data]
      summary: Detect network from MSISDN
      parameters:
        - name: msisdn
          in: query
          required: true
          schema:
            type: string
            example: '256771234567'
      responses:
        '200':
          description: Network detected

  /airtime-data/provider-balances:
    get:
      tags: [Airtime & Data]
      summary: Provider float balances
      responses:
        '200':
          description: Provider balances

  /airtime-data:
    get:
      tags: [Airtime & Data]
      summary: List airtime/data purchases
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Purchase list
    post:
      tags: [Airtime & Data]
      summary: Purchase airtime or data bundle
      description: |
        No merchant webhooks — poll GET /airtime-data/{reference} for pending Airtel data bundles.
        Requires IP whitelist.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AirtimeDataRequest'
            examples:
              airtime:
                value:
                  reference: 550e8400-e29b-41d4-a716-446655440000
                  purchase_type: airtime
                  msisdn: '256771234567'
                  amount: 5000
              bundle:
                value:
                  reference: 660e8400-e29b-41d4-a716-446655440001
                  purchase_type: bundle
                  msisdn: '256771234567'
                  bundle_id: RACT_UG_Data_201
      responses:
        '200':
          description: Completed synchronously
        '202':
          description: Pending (Airtel data) — poll status
        '422':
          $ref: '#/components/responses/ValidationError'

  /airtime-data/{reference}:
    get:
      tags: [Airtime & Data]
      summary: Get purchase by reference
      parameters:
        - $ref: '#/components/parameters/Reference'
      responses:
        '200':
          description: Purchase details

  /phone-verification/verify:
    post:
      tags: [Phone Verification]
      summary: Verify phone number
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number]
              properties:
                phone_number:
                  type: string
                  example: '256712345678'
      responses:
        '200':
          description: Verification result

  /phone-verification/service-info:
    get:
      tags: [Phone Verification]
      summary: Phone verification service info
      responses:
        '200':
          description: Service info

  /phone-verification/subscription-status:
    get:
      tags: [Phone Verification]
      summary: Subscription status
      responses:
        '200':
          description: Subscription status

  /transactions:
    get:
      tags: [Transactions]
      summary: List transactions
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: type
          in: query
          schema:
            type: string
            enum: [collection, withdrawal, charge, refund]
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, processing, completed, failed, cancelled]
        - name: provider
          in: query
          schema:
            type: string
            enum: [mtn, airtel]
        - name: start_date
          in: query
          schema:
            type: string
            format: date
        - name: end_date
          in: query
          schema:
            type: string
            format: date
        - name: reference
          in: query
          schema:
            type: string
        - name: country_code
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Paginated transactions

  /transactions/{uuid}:
    get:
      tags: [Transactions]
      summary: Get transaction by UUID
      parameters:
        - $ref: '#/components/parameters/TransactionUuid'
      responses:
        '200':
          description: Transaction details
        '404':
          $ref: '#/components/responses/NotFound'

  /services:
    get:
      tags: [Services]
      summary: List services
      responses:
        '200':
          description: Services list

  /services/{uuid}:
    get:
      tags: [Services]
      summary: Get service by UUID
      parameters:
        - $ref: '#/components/parameters/TransactionUuid'
      responses:
        '200':
          description: Service details

  /account:
    get:
      tags: [Account]
      summary: Get account details
      responses:
        '200':
          description: Account details
    put:
      tags: [Account]
      summary: Update account
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Account updated

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhooks
      responses:
        '200':
          description: Webhooks list
    post:
      tags: [Webhooks]
      summary: Create webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreateRequest'
      responses:
        '201':
          description: Webhook created

  /webhooks/{uuid}:
    get:
      tags: [Webhooks]
      summary: Get webhook
      parameters:
        - $ref: '#/components/parameters/TransactionUuid'
      responses:
        '200':
          description: Webhook details
    put:
      tags: [Webhooks]
      summary: Update webhook
      parameters:
        - $ref: '#/components/parameters/TransactionUuid'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreateRequest'
      responses:
        '200':
          description: Webhook updated
    delete:
      tags: [Webhooks]
      summary: Delete webhook
      parameters:
        - $ref: '#/components/parameters/TransactionUuid'
      responses:
        '200':
          description: Webhook deleted

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
      description: base64(api_key:api_secret)

  parameters:
    TransactionUuid:
      name: uuid
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Reference:
      name: reference
      in: path
      required: true
      schema:
        type: string
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25

  schemas:
    SuccessEnvelope:
      type: object
      properties:
        status:
          type: string
          example: success
        data:
          type: object

    ErrorEnvelope:
      type: object
      properties:
        status:
          type: string
          example: error
        message:
          type: string
        error_code:
          type: string
          example: VALIDATION_ERROR
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string

    CollectMoneyRequest:
      type: object
      required: [amount, reference, country]
      properties:
        amount:
          type: number
          minimum: 500
          maximum: 10000000
          example: 5000
        phone_number:
          type: string
          pattern: '^\+[0-9]{10,15}$'
          example: '+256712345678'
          description: Required for mobile money; omit for card
        reference:
          type: string
          format: uuid
          example: c97fae8b-9b7f-4192-9f72-6f0859d33e67
        country:
          type: string
          enum: [UG, KE]
          description: UG (UGX, MTN/Airtel) or KE (KES, M-Pesa)
        method:
          type: string
          enum: [mobile_money, card]
          default: mobile_money
        description:
          type: string
          maxLength: 255
        callback_url:
          type: string
          format: uri
        metadata:
          type: array
          maxItems: 10
          description: |
            Optional. Array of objects echoed on create (`data.collection.metadata`) and webhooks (top-level `metadata`).
            Each item has exactly one field name + string/number value, plus optional `isPII: true`.
          items:
            type: object
            additionalProperties: true
            example:
              orderId: ORD-123456789
          example:
            - orderId: ORD-123456789
            - customerId: customer@email.com
              isPII: true

    SendMoneyRequest:
      type: object
      required: [amount, phone_number, reference, country]
      properties:
        amount:
          type: number
        phone_number:
          type: string
          pattern: '^\+[0-9]{10,15}$'
          example: '+256712345678'
          description: E.164 — Uganda +256… or Kenya +254…
        reference:
          type: string
          maxLength: 50
        country:
          type: string
          enum: [UG, KE]
          description: UG (UGX, MTN/Airtel) or KE (KES, M-Pesa)
        description:
          type: string
        callback_url:
          type: string
          format: uri
        metadata:
          type: array
          maxItems: 10
          description: |
            Optional. Array of objects echoed on create (`data.withdrawal.metadata`) and webhooks (top-level `metadata`).
            Each item has exactly one field name + string/number value, plus optional `isPII: true`.
          items:
            type: object
            additionalProperties: true
          example:
            - orderId: ORD-123456789
            - customerId: customer@email.com
              isPII: true

    BankTransferRequest:
      type: object
      required: [amount, description, bank_name, bank_account_number]
      properties:
        amount:
          type: number
          minimum: 2500
        description:
          type: string
          maxLength: 500
        bank_name:
          type: string
          example: Equity Bank
        bank_account_number:
          type: string
        bank_account_name:
          type: string
        bank_branch:
          type: string
        wallet_source:
          type: string
          enum: [main, card]
          default: main

    BillPaymentVerifyRequest:
      type: object
      required: [utility_code, meter_number]
      properties:
        utility_code:
          type: string
          enum: [LIGHT, UMEME, NWSC, DSTV, GOTV]
        meter_number:
          type: string
        area:
          type: string
          description: Required for NWSC

    BillPaymentRequest:
      type: object
      required: [utility_code, meter_number, phone_number, amount, reference]
      properties:
        utility_code:
          type: string
          enum: [LIGHT, UMEME, NWSC, DSTV, GOTV]
        meter_number:
          type: string
        area:
          type: string
          description: Required for NWSC
        bouquet_code:
          type: string
          description: Required for DSTV/GOTV; amount must match bouquet price
        phone_number:
          type: string
          pattern: '^\+?256[0-9]{9}$'
        amount:
          type: number
          minimum: 1000
        reference:
          type: string
        customer_name:
          type: string
        email:
          type: string
          format: email
        callback_url:
          type: string
          format: uri

    AirtimeDataRequest:
      type: object
      required: [reference, purchase_type, msisdn]
      properties:
        reference:
          type: string
          format: uuid
        purchase_type:
          type: string
          enum: [airtime, bundle]
        msisdn:
          type: string
          example: '256771234567'
        amount:
          type: integer
          minimum: 500
          description: Required for airtime; prohibited for bundle
        bundle_id:
          type: string
          description: Required for bundle; use product_id from catalog

    WebhookCreateRequest:
      type: object
      required: [name, url, event_type, environment]
      properties:
        name:
          type: string
        url:
          type: string
          format: uri
        event_type:
          type: string
          enum: [success, failure, collection.completed, collection.failed, collection.cancelled]
        environment:
          type: string
          enum: [test, production]
        is_active:
          type: boolean
          default: true

  responses:
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: Invalid API credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: Permission denied or account frozen
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
