> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sakawave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate a customer

> Asks the biller who this customer is and what they owe. An enquiry is required before a payment and is valid for **ten minutes**.

## Why the enquiry exists

A bill is paid to a person, not just to a biller. The enquiry confirms the meter or account exists, returns the name and address the biller holds for it, and prices the payment — charge, commission, and `totalDebit`. Show that name to the payer before taking money: it is the only check that the reference was typed correctly.

## Naming the product

Sakawave identifiers only. A slug will not resolve here, and neither will an upstream provider’s id — the route to a provider, and whatever that provider needs in its own payload, is resolved on our side. The same body therefore works whichever provider serves the biller, and keeps working when we change one.

| Send | Rule |
| - | - |
| `billerId` | Required. The biller’s id from the catalog. |
| `packageId` | Required when the biller has packages, rejected when it has none. |
| `amount` | Required when no package fixes the price; must match a fixed package’s price if sent. |

Any other field — `groupId`, `groupSlug`, `billerSlug`, `packageSlug`, `productName` — is rejected.

## Amounts are decimals here

Bill payments use **decimal strings** — `"5000.00"` — not the minor units the rest of the API uses. Everything in this service, request and response alike, is in naira and kobo written out.

<Warning>An enquiry expires after ten minutes, and paying with an expired one fails with `409 bill_enquiry_expired`. Enquire immediately before paying, not when the user opens the form.</Warning>

## Error codes

| Status | `code` | Cause |
| - | - | - |
| `400` | `validation_failed` | A field failed validation, or an unrecognised field was sent. |
| `400` | `customer_lookup_failed` | The biller could not validate this customer reference. Its own message is passed through. |
| `400` | `bill_package_required` | The biller sells packages and none was named. |
| `400` | `bill_package_not_supported` | The biller is paid by amount and has no packages. |
| `400` | `bill_amount_required` | This product needs an amount. |
| `400` | `bill_amount_mismatch` | The amount does not match the fixed price of the package. |
| `404` | `resource_not_found` | No such biller, or the package does not belong to it. |
| `409` | `bill_product_unavailable` | No active provider currently serves this biller or package. |
| `401` | `INVALID_API_KEY` | The key is missing, invalid, inactive, expired, from the other environment, or blocked by its IP allowlist. |
| `429` | `api_key_rate_limit_exceeded` | The per-minute limit for this key was exceeded. |


## OpenAPI

````yaml /openapi.json post /v1/bill-payments/enquiries
openapi: 3.1.0
info:
  title: Sakawave Business API
  version: 1.0.0-draft
  summary: >-
    Accounts, transfers, FX quotes, and transaction reconciliation for Sakawave
    businesses.
  description: >-
    Generated from the public NestJS controllers and their Zod request schemas.
    Public URLs and policy statements remain draft until the Phase 1 contract
    checklist is approved.
  contact:
    name: Sakawave API Support
    email: api-support@sakawave.example
servers:
  - url: https://sandbox-api.sakawave.example
    description: Sandbox (default)
  - url: https://api.sakawave.example
    description: Live
security:
  - bearerAuth: []
tags:
  - name: Bill payments
    description: Browse the biller catalog, validate a customer, and pay a bill.
  - name: Cards
    description: Issue, fund, and control USD virtual and disposable cards.
  - name: eSIM
    description: >-
      Sell mobile data abroad: browse the catalog, buy an eSIM, and manage it
      once installed.
  - name: Currencies and countries
    description: >-
      Reference data: what may be held, what may be sent where, and which
      jurisdictions are served.
  - name: Customers
    description: Create and manage customers of your business.
  - name: Accounts
    description: >-
      Open business and customer accounts, and issue the destinations that fund
      them.
  - name: Transfer destinations
    description: Resolve temporary destinations and saved recipients.
  - name: FX quotes
    description: Create and retrieve time-limited cross-currency quotes.
  - name: Transfers
    description: Preview, create, and monitor transfers.
  - name: Transactions
    description: Read ledger entries for reconciliation.
paths:
  /v1/bill-payments/enquiries:
    post:
      tags:
        - Bill payments
      summary: Validate a customer
      description: >-
        Asks the biller who this customer is and what they owe. An enquiry is
        required before a payment and is valid for **ten minutes**.
      operationId: createBillEnquiry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBillEnquiryRequest'
            example:
              customerId: '04117997788'
              billerId: 01KZXPYGX5VZXDH9VHJ4YTPCB9
              packageId: 01KZXPYGX5VZXDH9VHJ4YTPCC1
              amount: '5000.00'
      responses:
        '201':
          description: Successful response.
          headers:
            X-Request-ID:
              description: >-
                The request identifier assigned by Sakawave. Include it in
                support requests.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillEnquiryResponse'
              example:
                id: 01K2Z3AB4W5X6Y7Z8ABCDEFGHJ
                billerId: 01KZXPYGX5VZXDH9VHJ4YTPCB9
                billerSlug: example-disco
                packageId: 01KZXPYGX5VZXDH9VHJ4YTPCC1
                packageSlug: example-disco-prepaid
                customer:
                  customerName: ADA OKAFOR
                  address: 14 Marina Road
                  district: Lagos Island
                  canVend: true
                  minimumVend: '500.00'
                totalDue: null
                minimumAmount: '500.00'
                charge: '100.00'
                commission: '0.00'
                totalDebit: '5100.00'
                expiresAt: '2026-08-14T09:20:00.000Z'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/AuthenticationError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '409':
          $ref: '#/components/responses/ConflictError'
        '429':
          $ref: '#/components/responses/RateLimitError'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - bearerAuth: []
components:
  schemas:
    CreateBillEnquiryRequest:
      type: object
      additionalProperties: false
      properties:
        customerId:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            What the biller identifies the customer by: a meter number, an
            account number, or a phone number for airtime and data.
        billerId:
          $ref: '#/components/schemas/Ulid'
          description: The biller’s Sakawave id, from the catalog.
        packageId:
          $ref: '#/components/schemas/Ulid'
          description: >-
            The package’s Sakawave id. Required when the biller has packages,
            rejected when it has none.
        amount:
          $ref: '#/components/schemas/DecimalAmount'
      required:
        - customerId
        - billerId
      description: >-
        Sakawave identifiers only. Slugs and provider references are rejected,
        so the same body works whichever provider serves the biller.
    BillEnquiryResponse:
      type: object
      additionalProperties: false
      properties:
        id:
          $ref: '#/components/schemas/Ulid'
          description: Send this as `enquiryId` when creating the payment.
        billerId:
          $ref: '#/components/schemas/Ulid'
        billerSlug:
          type: string
        packageId:
          anyOf:
            - $ref: '#/components/schemas/Ulid'
              description: Null when the biller sells no packages.
            - type: 'null'
        packageSlug:
          anyOf:
            - type: string
            - type: 'null'
        customer:
          type: object
          additionalProperties: true
          description: >-
            What the biller returned about the customer, filtered to a safe
            subset: names, customer type, address, district, whether they can be
            vended, minimum vend, total due, due date, and available products.
            Shape varies by biller.
        totalDue:
          anyOf:
            - $ref: '#/components/schemas/DecimalAmount'
            - type: 'null'
        minimumAmount:
          anyOf:
            - $ref: '#/components/schemas/DecimalAmount'
            - type: 'null'
        charge:
          anyOf:
            - $ref: '#/components/schemas/DecimalAmount'
            - type: 'null'
        commission:
          anyOf:
            - $ref: '#/components/schemas/DecimalAmount'
            - type: 'null'
        totalDebit:
          anyOf:
            - $ref: '#/components/schemas/DecimalAmount'
            - type: 'null'
        expiresAt:
          $ref: '#/components/schemas/IsoTimestamp'
      required:
        - id
        - billerId
        - billerSlug
        - packageId
        - packageSlug
        - customer
        - totalDue
        - minimumAmount
        - charge
        - commission
        - totalDebit
        - expiresAt
    Ulid:
      type: string
      minLength: 26
      maxLength: 26
      pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
      example: 01K2Z3AB4C5D6E7F8G9HJKMNPQ
      description: A 26-character ULID.
    DecimalAmount:
      type: string
      pattern: ^-?\d+(?:\.\d+)?$
      example: '25000.00'
      description: >-
        A decimal display amount returned by transfer APIs. Preserve it as a
        string.
    IsoTimestamp:
      type: string
      format: date-time
      example: '2026-08-14T09:00:00.000Z'
    RequestError:
      anyOf:
        - $ref: '#/components/schemas/ValidationError'
        - $ref: '#/components/schemas/Error'
      description: >-
        Either a field-level validation failure carrying an errors array, or a
        rule the request broke, identified by code.
    Error:
      type: object
      additionalProperties: true
      properties:
        statusCode:
          type: integer
        message:
          type: string
          description: >-
            A sentence suitable for a log or a support conversation, not for a
            user interface.
        code:
          type: string
          description: >-
            The stable machine-readable reason. Branch on this, never on the
            message.
      required:
        - message
        - code
      description: >-
        An error body. Individual errors may add context fields alongside these
        three.
    RateLimitError:
      type: object
      additionalProperties: false
      properties:
        statusCode:
          const: 429
        message:
          type: string
        code:
          const: api_key_rate_limit_exceeded
        retryAfterSeconds:
          type: integer
          minimum: 1
      required:
        - statusCode
        - message
        - code
        - retryAfterSeconds
    ValidationError:
      type: object
      additionalProperties: false
      properties:
        statusCode:
          const: 400
        message:
          type: string
          description: The single failure, or a count when more than one field failed.
        code:
          const: validation_failed
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationFailure'
      required:
        - statusCode
        - message
        - code
        - errors
    ValidationFailure:
      type: object
      additionalProperties: false
      properties:
        message:
          type: string
        field:
          type: string
        code:
          type: string
        path:
          type: array
          items:
            anyOf:
              - type: string
              - type: integer
      required:
        - message
        - field
        - code
        - path
  responses:
    ValidationError:
      description: >-
        The request was rejected before it was acted on. Field failures list
        every failing field.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RequestError'
          example:
            statusCode: 400
            message: Must be a valid identifier.
            code: validation_failed
            errors:
              - message: Must be a valid identifier.
                field: accountId
                code: custom
                path:
                  - accountId
    AuthenticationError:
      description: >-
        The API key is missing, invalid, inactive, expired, for another
        environment, or blocked by its IP allowlist.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 401
            code: INVALID_API_KEY
            message: Invalid or inactive API credentials.
    NotFoundError:
      description: The requested resource was not found for this business.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 404
            code: resource_not_found
            message: Resource not found.
    ConflictError:
      description: The request conflicts with the current resource state.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 409
            code: resource_conflict
            message: The request conflicts with the current resource state.
    RateLimitError:
      description: The API key rate limit was exceeded.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
          example:
            statusCode: 429
            message: Too many API requests. Try again later.
            code: api_key_rate_limit_exceeded
            retryAfterSeconds: 17
    ServerError:
      description: An unexpected server error occurred.
      headers:
        X-Request-ID:
          description: >-
            The request identifier assigned by Sakawave. Include it in support
            requests.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            statusCode: 500
            code: internal_server_error
            message: An unexpected error occurred.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sakawave API key
      description: >-
        A secret API key, sent as `Authorization: Bearer sk_test_…` in sandbox
        or `Bearer sk_live_…` in live.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.