openapi: 3.1.0
info:
  title: MozoPay Public API
  version: "1.0.0"
  description: |
    Production public API for MozoPay business integrations. This API is authenticated using API keys and supports
    money movement, bill payments, virtual accounts, and operational transaction access.
servers:
  - url: https://api.mozopay.co/v1
    description: Production
  - url: http://localhost:4000/v1
    description: Local development
tags:
  - name: Authentication
    description: API key authentication, HMAC signatures, and required headers.
  - name: Transfers
    description: Intra‑MozoPay transfers, inter‑bank payouts, and email transfers.
  - name: Bills
    description: Airtime, data, TV, and electricity bill payments.
  - name: Virtual Accounts
    description: Provisioning, querying, and funding sync for virtual accounts.
  - name: Transactions
    description: Transaction listing, detail, statements, and reporting.
  - name: Checkout
    description: Checkout links for merchant payment collection.
security:
  - SecretKeyAuth: []
  - HmacAuth: []
paths:
  /transfers/banks:
    get:
      tags: [Transfers]
      operationId: listBanks
      summary: List banks for an account number
      description: |
        Returns supported banks for a 10‑digit NUBAN account number. Required before resolving an account name.
      parameters:
        - in: query
          name: accountNumber
          required: true
          schema: { type: string, pattern: "^[0-9]{10}$" }
      responses:
        "200":
          description: Bank list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Bank" }
              examples:
                success:
                  value:
                    data:
                      - code: "058"
                        name: "GTBank"
                        bankCode: "058"
                        logoUrl: "https://cdn.mozopay.co/banks/gtb.svg"
                        logoUrlPng: "https://cdn.mozopay.co/banks/gtb.png"
        "400":
          $ref: "#/components/responses/BadRequest"
  /transfers/banks/all:
    get:
      tags: [Transfers]
      operationId: listAllBanks
      summary: List all banks (no account number)
      description: |
        Returns the full bank directory without requiring an account number.
      responses:
        "200":
          description: Bank list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Bank" }
              examples:
                success:
                  value:
                    data:
                      - code: "058"
                        name: "GTBank"
                        bankCode: "058"
                        logoUrl: "https://cdn.mozopay.co/banks/gtb.svg"
                        logoUrlPng: "https://cdn.mozopay.co/banks/gtb.png"
  /transfers/resolve-bank-account:
    post:
      tags: [Transfers]
      operationId: resolveBankAccount
      summary: Resolve beneficiary account details
      description: |
        Resolves the beneficiary name for an inter‑bank transfer. Requires `transfers:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BankResolveRequest"
            examples:
              example:
                value:
                  accountNumber: "0123456789"
                  institutionCode: "058"
      responses:
        "200":
          description: Account resolution result
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BankResolveResponse" }
        "404":
          $ref: "#/components/responses/NotFound"
  /transfers/resolve-mozopay:
    post:
      tags: [Transfers]
      operationId: resolveMozoPayRecipient
      summary: Resolve MozoPay recipient
      description: |
        Resolve a MozoPay recipient by email, phone, or account number.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                identifier: { type: string, minLength: 3 }
              required: [identifier]
            examples:
              example:
                value: { identifier: "john@company.com" }
      responses:
        "200":
          description: Recipient resolution
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MozoPayRecipient" }
        "404":
          $ref: "#/components/responses/NotFound"
  /transfers/mozopay:
    post:
      tags: [Transfers]
      operationId: createMozoPayTransfer
      summary: Intra‑MozoPay transfer
      description: |
        Sends money to a MozoPay account using an identifier (email/phone/account).
        Requires `transfers:write` and `Idempotency-Key`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MozoPayTransferRequest"
      responses:
        "201":
          description: Transfer accepted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransferResponse" }
        "409":
          $ref: "#/components/responses/Conflict"
  /transfers/merchant-funded:
    post:
      tags: [Transfers]
      operationId: createInterbankTransfer
      summary: Inter‑bank transfer (merchant funded)
      description: |
        Sends money to a bank account. Requires `transfers:write` and `Idempotency-Key`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InterbankTransferRequest"
      responses:
        "202":
          description: Transfer queued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransferResponse" }
        "409":
          $ref: "#/components/responses/Conflict"
  /transfers/pay-via-email:
    post:
      tags: [Transfers]
      operationId: createPayViaEmail
      summary: Email transfers (single or batch)
      description: |
        Sends money to email recipients. Requires `transfers:write` and `Idempotency-Key`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PayViaEmailRequest"
      responses:
        "201":
          description: Email transfer accepted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransferResponse" }
  /transfers/pay-me-links:
    post:
      tags: [Transfers]
      operationId: createPayMeLink
      summary: Create pay‑me link
      description: Create a shareable link for inbound payments.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PayMeLinkRequest"
      responses:
        "201":
          description: Pay‑me link created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PayMeLinkResponse" }
  /bills/providers:
    get:
      tags: [Bills]
      operationId: listBillProviders
      summary: List bill providers
      parameters:
        - in: query
          name: category
          required: true
          schema: { type: string, enum: [TV, UTILITY] }
      responses:
        "200":
          description: Providers
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillProvidersResponse" }
  /bills/packages:
    get:
      tags: [Bills]
      operationId: listBillPackages
      summary: List bill packages
      parameters:
        - in: query
          name: category
          required: true
          schema: { type: string, enum: [DATA, TV] }
        - in: query
          name: customerAccount
          required: false
          schema: { type: string }
        - in: query
          name: billcode
          required: false
          schema: { type: string }
        - in: query
          name: providerKey
          required: false
          schema: { type: string }
      responses:
        "200":
          description: Packages
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillPackagesResponse" }
  /bills/lookup:
    post:
      tags: [Bills]
      operationId: lookupBillCustomer
      summary: Lookup TV / Electricity customer
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BillLookupRequest" }
      responses:
        "200":
          description: Lookup response
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillLookupResponse" }
  /bills/purchase:
    post:
      tags: [Bills]
      operationId: purchaseBill
      summary: Purchase a bill (airtime, data, TV, electricity)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BillPurchaseRequest" }
      responses:
        "202":
          description: Bill payment accepted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BillPurchaseResponse" }
  /virtual-accounts/me:
    get:
      tags: [Virtual Accounts]
      operationId: getVirtualAccount
      summary: Fetch virtual account
      parameters:
        - in: query
          name: ensure
          required: false
          schema: { type: boolean }
          description: Provision a virtual account if missing when set to true.
      responses:
        "200":
          description: Virtual account
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/VirtualAccount" }
  /virtual-accounts/provision:
    post:
      tags: [Virtual Accounts]
      operationId: provisionVirtualAccount
      summary: Provision virtual account
      responses:
        "201":
          description: Virtual account provisioned
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/VirtualAccount" }
  /virtual-accounts/transactions:
    get:
      tags: [Virtual Accounts]
      operationId: syncVirtualAccountTransactions
      summary: Sync and list virtual account funding
      parameters:
        - in: query
          name: offset
          schema: { type: integer, minimum: 0 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Virtual account transactions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/VirtualAccountTransaction" }
                  ingestedTransactionIds:
                    type: array
                    items: { type: integer }
  /checkout:
    get:
      tags: [Checkout]
      operationId: listCheckoutLinks
      summary: List checkout links
      responses:
        "200":
          description: Checkout links
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CheckoutLink" }
    post:
      tags: [Checkout]
      operationId: createCheckoutLink
      summary: Create checkout link
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CheckoutCreateRequest" }
      responses:
        "201":
          description: Checkout link created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CheckoutLink" }
  /checkout/{key}:
    get:
      tags: [Checkout]
      operationId: getCheckoutLink
      summary: Retrieve checkout link
      parameters:
        - in: path
          name: key
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Checkout link
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CheckoutLink" }
  /transactions:
    get:
      tags: [Transactions]
      operationId: listTransactions
      summary: List transactions
      parameters:
        - in: query
          name: reference
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: type
          schema: { type: string }
        - in: query
          name: walletKey
          schema: { type: string }
        - in: query
          name: startDate
          schema: { type: string, pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
        - in: query
          name: endDate
          schema: { type: string, pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 250 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Transactions list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/TransactionSummary" }
                  total:
                    type: integer
  /transactions/statement/email:
    post:
      tags: [Transactions]
      operationId: emailStatement
      summary: Email statement of account
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StatementEmailRequest" }
      responses:
        "202":
          description: Statement queued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatementEmailResponse" }
  /transactions/{reference}:
    get:
      tags: [Transactions]
      operationId: getTransaction
      summary: Fetch transaction detail
      parameters:
        - in: path
          name: reference
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Transaction detail
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionDetailResponse" }
  /transactions/{reference}/report:
    post:
      tags: [Transactions]
      operationId: reportTransaction
      summary: Report a transaction issue
      parameters:
        - in: path
          name: reference
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TransactionReportRequest" }
      responses:
        "201":
          description: Report submitted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionReportResponse" }
components:
  securitySchemes:
    SecretKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: MozoPaySecretKey
      description: |
        Use your secret key in the Authorization header.
        Example: `Authorization: Bearer sk_live_xxx`
    HmacAuth:
      type: apiKey
      in: header
      name: X-MOZO-KEY
      description: |
        Legacy HMAC authentication. Required headers:
        `X-MOZO-KEY`, `X-MOZO-NONCE`, `X-MOZO-TIMESTAMP`, `X-MOZO-SIGNATURE`.
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema: { type: string }
      description: Prevent duplicate requests for write operations.
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    NotFound:
      description: Not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Conflict:
      description: Conflict
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
  schemas:
    ErrorResponse:
      type: object
      properties:
        code: { type: string }
        message: { type: string }
        details: { type: object, nullable: true }
    Bank:
      type: object
      properties:
        code: { type: string }
        name: { type: string }
        bankCode: { type: string }
        logoUrl: { type: string, nullable: true }
        logoUrlPng: { type: string, nullable: true }
    BankResolveRequest:
      type: object
      properties:
        accountNumber: { type: string, pattern: "^[0-9]{10}$" }
        institutionCode: { type: string }
        bankName: { type: string, nullable: true }
        bankCode: { type: string, nullable: true }
      required: [accountNumber, institutionCode]
    BankResolveResponse:
      type: object
      properties:
        accountNumber: { type: string }
        institutionCode: { type: string }
        bankCode: { type: string }
        bankName: { type: string }
        accountName: { type: string }
    MozoPayRecipient:
      type: object
      properties:
        via: { type: string, enum: [EMAIL, PHONE, ACCOUNT_NUMBER] }
        userId: { type: integer }
        fullName: { type: string }
        email: { type: string }
        phone: { type: string, nullable: true }
        accountType: { type: string, enum: [PERSONAL, BUSINESS] }
        accountNumber: { type: string, nullable: true }
    MozoPayTransferRequest:
      type: object
      properties:
        amountKobo: { type: integer }
        narration: { type: string }
        recipientIdentifier: { type: string }
        recipientEmail: { type: string }
        recipientUserId: { type: integer }
      required: [amountKobo, narration]
    InterbankTransferRequest:
      type: object
      properties:
        amountKobo: { type: integer }
        accountNumber: { type: string, pattern: "^[0-9]{10}$" }
        institutionCode: { type: string }
        recipientName: { type: string }
        narration: { type: string }
      required: [amountKobo, accountNumber, institutionCode, narration]
    PayViaEmailRequest:
      type: object
      properties:
        narration: { type: string }
        amountKobo: { type: integer }
        recipientEmail: { type: string }
        recipients:
          type: array
          items:
            type: object
            properties:
              recipientEmail: { type: string }
              amountKobo: { type: integer }
            required: [recipientEmail, amountKobo]
      required: [narration]
    PayMeLinkRequest:
      type: object
      properties:
        amountKobo: { type: integer, nullable: true }
    PayMeLinkResponse:
      type: object
      properties:
        key: { type: string }
        url: { type: string }
        amountKobo: { type: integer, nullable: true }
        expiresAt: { type: string, nullable: true }
    TransferResponse:
      type: object
      properties:
        reference: { type: string }
        status: { type: string }
    BillProvidersResponse:
      type: object
      properties:
        category: { type: string }
        cashbackPercent: { type: number }
        providers:
          type: array
          items: { $ref: "#/components/schemas/BillProvider" }
    BillProvider:
      type: object
      properties:
        key: { type: string }
        label: { type: string }
        billcode: { type: string }
        category: { type: string }
        disco: { type: string, nullable: true }
        description: { type: string, nullable: true }
        minimumAmountKobo: { type: integer, nullable: true }
        maximumAmountKobo: { type: integer, nullable: true }
    BillPackagesResponse:
      type: object
      properties:
        category: { type: string }
        carrier: { type: string, nullable: true }
        provider: { type: string, nullable: true }
        providerKey: { type: string, nullable: true }
        billcode: { type: string, nullable: true }
        cashbackPercent: { type: number }
        groups:
          type: array
          items:
            type: object
            properties:
              key: { type: string }
              label: { type: string }
              items:
                type: array
                items:
                  $ref: "#/components/schemas/BillPackage"
    BillPackage:
      type: object
      properties:
        packageKey: { type: string }
        packageId: { type: string }
        name: { type: string }
        amountKobo: { type: integer }
        billcode: { type: string }
        providerKey: { type: string, nullable: true }
        providerLabel: { type: string, nullable: true }
    BillLookupRequest:
      type: object
      properties:
        category: { type: string, enum: [TV, UTILITY] }
        customerAccount: { type: string }
        billcode: { type: string }
        providerKey: { type: string, nullable: true }
      required: [category, customerAccount, billcode]
    BillLookupResponse:
      type: object
      properties:
        data: { type: object }
    BillPurchaseRequest:
      type: object
      properties:
        amountKobo: { type: integer }
        category: { type: string, enum: [AIRTIME, DATA, TV, UTILITY] }
        customerAccount: { type: string }
        billcode: { type: string, nullable: true }
        packageKey: { type: string, nullable: true }
        providerKey: { type: string, nullable: true }
        lookupUniqueId: { type: string, nullable: true }
        lookupAccessToken: { type: string, nullable: true }
        providerDisco: { type: string, nullable: true }
        customerPhoneNumber: { type: string, nullable: true }
        customerEmail: { type: string, nullable: true }
        useCashback: { type: boolean, nullable: true }
        promoCode: { type: string, nullable: true }
      required: [amountKobo, category, customerAccount]
    BillPurchaseResponse:
      type: object
      properties:
        reference: { type: string }
        status: { type: string }
        message: { type: string, nullable: true }
    VirtualAccount:
      type: object
      properties:
        id: { type: integer }
        accountName: { type: string }
        accountNumber: { type: string }
        bankName: { type: string }
        status: { type: string }
        createdAt: { type: string, nullable: true }
        updatedAt: { type: string, nullable: true }
    VirtualAccountTransaction:
      type: object
      properties:
        id: { type: integer }
        reference: { type: string }
        amountKobo: { type: integer }
        direction: { type: string, enum: [CREDIT, DEBIT] }
        status: { type: string }
        bankName: { type: string, nullable: true }
        accountNumber: { type: string, nullable: true }
        createdAt: { type: string, nullable: true }
    CheckoutLink:
      type: object
      properties:
        key: { type: string }
        amountKobo: { type: integer }
        expiresAt: { type: string }
        title: { type: string, nullable: true }
        description: { type: string, nullable: true }
        metadata: { type: object, nullable: true }
    CheckoutCreateRequest:
      type: object
      properties:
        amountKobo: { type: integer }
        expiresAt: { type: string, format: date-time }
        title: { type: string, nullable: true }
        description: { type: string, nullable: true }
        metadata: { type: object, nullable: true }
      required: [amountKobo, expiresAt]
    TransactionSummary:
      type: object
      properties:
        id: { type: integer }
        reference: { type: string }
        type: { type: string }
        status: { type: string }
        amountKobo: { type: integer }
        currency: { type: string }
        createdAt: { type: string, nullable: true }
        metadata: { type: object, nullable: true }
    TransactionDetailResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            reference: { type: string }
            type: { type: string }
            status: { type: string }
            amountKobo: { type: integer }
            currency: { type: string }
            providerDetails: { type: object, nullable: true }
    StatementEmailRequest:
      type: object
      properties:
        startDate: { type: string, pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
        endDate: { type: string, pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
        type: { type: string, nullable: true }
        fileType: { type: string, enum: [PDF, EXCEL, CSV] }
        recipients:
          type: array
          items: { type: string, format: email }
      required: [startDate, endDate]
    StatementEmailResponse:
      type: object
      properties:
        success: { type: boolean }
        message: { type: string }
        transactionCount: { type: integer }
        period: { type: string }
        fileType: { type: string }
        recipientCount: { type: integer }
    TransactionReportRequest:
      type: object
      properties:
        issueType: { type: string }
        description: { type: string }
      required: [issueType, description]
    TransactionReportResponse:
      type: object
      properties:
        success: { type: boolean }
        reportId: { type: integer }
        message: { type: string }
