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

# Preview an eligible Numero service fee

> Requires wallet:read and a valid request signature. Returns the business's applicable fee and savings. Statutory charges remain payable. This preview does not reserve allowance, confirm service availability, or quote the complete transaction debit. Use the transaction's normal quote and confirmation flow before purchasing.



## OpenAPI

````yaml /openapi/numero-public-api.yaml post /business/partner-benefits/fee-preview
openapi: 3.1.0
info:
  title: Numero Public API
  version: 1.0.0
  summary: >-
    Server-to-server API for transfers, virtual accounts, bill payments (VAS)
    and identity verification.
  description: >
    The Numero Public API is the API-key-authenticated, server-to-server surface
    that powers

    payouts, collections (virtual accounts), bill payments and identity
    verification for your

    business. It is the `/business/*` surface — distinct from the dashboard
    (JWT) endpoints your

    own staff use in the Numero merchant dashboard.


    ## Quickstart


    1. **Get your API keys** — issued on business approval; reveal/rotate them
    in
       **Dashboard → Settings → Developer → API keys** (step-up-OTP gated). You get two secret keys, and
       **the key decides the mode** — there is no separate sandbox host:
       `test_key_...` = Test (simulated providers + an isolated test wallet, live balances untouched),
       `live_key_...` = Live (real money). Build with your test key first.
    2. **First call** — read your wallet balance:
       ```
       curl https://api.usenumero.com/numeroaccount/api/v1/business/balance \
         -H "X-Numero-Api-Key: test_key_xxxxxxxxxxxxxxxxxxxxxxxx"
       ```
    3. **Send money** — `POST /business/validate` (name enquiry) → `POST
    /business/single`
       with a unique `Idempotency-Key` header and the HMAC `X-Numero-Signature`.
    4. **Collect money** — create a virtual account, then handle the
    `FUNDING_NOTIFICATION` webhook
       when a customer funds it.
    5. **Go live** — swap your `test_key_` for your `live_key_`. Same base URL,
    same endpoints,
       identical behaviour — nothing else changes.

    ## Money & amounts


    **All amounts are in major currency units — naira, NOT kobo** — as JSON
    numbers with up to 2

    decimal places. `1000` and `1000.00` both mean ₦1,000.00 (never ₦10.00).
    This applies to every

    `amount`, `fee`, `stampDuty`, and to balances (`available`, `pending`,
    `total`). Default currency

    is **NGN**; a `currency` / `feeCurrency` field names the ISO code where
    relevant.


    ## Internal transfers (Numero-to-Numero)


    To pay another Numero merchant **instantly** with no provider or settlement
    delay, send a normal

    `POST /business/transfer/single` but set **`destinationBankCode: "NUMERO"`**
    and

    `destinationAccountNumber` to the recipient's Numero account number. Run
    name enquiry the same way

    (`POST /business/transfer/validate` with `bankCode: "NUMERO"`) — the
    recipient resolves from

    Numero's own records, so `destinationAccountName` is optional. The transfer
    posts as an instant

    book entry; the **Numero fee is waived** (stamp duty still applies on
    amounts ≥ ₦10,000).


    ## Authentication


    Every request must carry your **API key** in the `X-Numero-Api-Key` header.
    Your keys are

    issued on business approval and are managed in **Dashboard → Settings →
    Developer → API keys**

    (reveal/rotate are step-up-OTP gated).


    ```

    X-Numero-Api-Key: live_key_xxxxxxxxxxxxxxxxxxxxxxxx

    ```


    ## Request signing (write endpoints)


    Money-movement and other state-changing endpoints additionally require an
    **HMAC-SHA256

    signature** so a leaked-but-read-only key can't move funds. Signed endpoints
    are marked with

    the 🔒 **(signed)** note and require the `NumeroSignature` scheme below.


    Compute the signature as:


    1. Take the **exact raw request body** you are about to send (for `POST`),
    or the
       **canonicalized query string** (for signed `GET`s — keys sorted, `&`-joined `name=value`).
    2. `HMAC_SHA256(publicApiKey, input)` → take the 32-byte digest →
    **Base64-encode** it.
       The HMAC secret is your **Public Key**, used as its **raw UTF-8 bytes** — do not Base64-decode
       it first. Despite the name it is a SECRET: it is the signing key and must never be published.
       It is a different value from the `X-Numero-Api-Key` you send as the authentication header.
    3. Send it as `X-Numero-Signature`, and set `X-Numero-Signature-Version:
    v2`.


    ```

    X-Numero-Api-Key: live_key_...

    X-Numero-Signature: 9b2c...base64...==

    X-Numero-Signature-Version: v2

    ```


    > Helper: `POST /business/generatesignature` returns the signature for a
    given body during

    > integration/testing. Don't rely on it in production — sign locally with
    your own secret.


    ## Response envelope


    Every response is wrapped in a canonical envelope. **Success** carries
    `data` (and `error: null`):


    ```json

    {
      "data": { },
      "error": null,
      "meta": { "request_id": "N20260603120000123", "pagination": null }
    }

    ```


    **Failure** carries `error` (and `data: null`) with a **real HTTP status
    code** (4xx/5xx) — not a

    `200` with a flag:


    ```json

    {
      "data": null,
      "error": { "code": "invalid_signature", "message": "Invalid signature.", "param": null },
      "meta": { "request_id": "N20260603120000123", "pagination": null }
    }

    ```


    `meta.request_id` is also returned as the `X-Request-Id` response header —
    quote it in support

    requests. `error.code` is a stable snake_case machine code (branch on it,
    not on `message`).


    ## Pagination


    List endpoints return the rows as a plain array in `data`, and the page info
    in `meta.pagination`:


    ```json

    { "data": [ ... ], "error": null,
      "meta": { "request_id": "...", "pagination": { "page": 1, "page_size": 50, "total": 240, "has_more": true } } }
    ```


    ## Error codes


    `error.code` is one of a stable set including: `missing_field`,
    `validation_failed`,

    `invalid_request`, `invalid_api_key`, `invalid_signature`, `unauthorized`,
    `forbidden`,

    `insufficient_balance`, `invalid_amount`, `limit_exceeded`, `rate_limited`,
    `duplicate_request`,

    `idempotency_conflict`, `business_not_found`, `account_not_found`,
    `transaction_not_found`,

    `customer_not_found`, `virtual_account_not_found`, `invalid_otp`,
    `otp_required`, `invalid_pin`,

    `kyc_required`, `approval_required`, `activation_required`, `invalid_bank`,

    `invalid_account_number`, `invalid_bvn`, `provider_error`,
    `provider_unavailable`,

    `provider_timeout`, `not_found`, `method_not_allowed`, `payload_too_large`,
    `internal_error`,

    `server_error`, `request_failed`.


    ## Fees


    Responses for billable events surface the fee **you were charged** (`fee` /
    `feeCurrency`).


    ## Idempotency


    Money-movement endpoints (single & bulk transfer, FX conversion, VAS
    purchases) **require** a

    Stripe-style **`Idempotency-Key`** request header — a unique string you
    generate per logical

    operation (a UUID is ideal). Retrying with the **same key and the same
    body** returns the

    original response instead of acting twice; reusing a key with a **different
    body** is rejected

    (`idempotency_conflict`). Omitting the header on a required endpoint returns
    `400`. Keys are

    scoped to your business and retained for replay — keep them unique per
    transaction.


    ```

    Idempotency-Key: 5f1c0b6e-9a2d-4c3a-8b7e-1f2a3b4c5d6e

    ```


    ## Service pauses


    Numero can temporarily pause a service (or all services) for maintenance.
    While paused, the

    affected endpoints return **HTTP `503`** with `error.code`
    **`service_paused`** and a

    user-facing `error.message`. Treat it as transient — surface the message and
    retry later.


    ## Webhooks


    Subscribe a URL + signing secret in **Dashboard → Settings → Developer →
    Webhooks**. Every event

    is delivered as a `POST` with the envelope:


    ```json

    { "event": "FUNDING_NOTIFICATION", "data": { } }

    ```


    **Verify the signature before trusting any payload.** Each delivery carries:


    ```

    X-Numero-Signature: t=1717412400123,v1=<base64 HMAC-SHA256>

    ```


    The signed string is **`"{t}.{rawRequestBody}"`**, HMAC-SHA256-keyed by your
    subscription's

    signing secret, then Base64-encoded. Recompute it from the `t` value + the
    exact raw body,

    compare in constant time, and reject deliveries whose `t` is older than your
    tolerance (replay

    guard).


    > A legacy `X-Webhook-Signature` (bare-body HMAC, Base64) is sent in
    parallel until **2026-06-26**

    > for back-compat — migrate to `X-Numero-Signature`.


    Events: `FUNDING_NOTIFICATION` (inbound credit landed),
    `TRANSFER_NOTIFICATION` (outbound transfer

    status change), `BILLS_PURCHASE_NOTIFICATION` (VAS success/failure),
    `VERIFICATION_NOTIFICATION`

    (verification completed/failed), `VIRTUAL_ACCOUNT_CREATED`. For money
    events, `data` is the

    transaction object (reference, amount, status, fee, and the account/customer
    involved). Respond

    `2xx` to acknowledge — failed deliveries are persisted and **retried
    automatically** (and can be

    re-sent from the dashboard), so dedupe on the envelope `id`.


    **Read `data.status`** (`Successful` / `Unsuccessful` / `Pending`), not the
    event name, to decide

    the outcome. The async money movements — `TRANSFER_NOTIFICATION`,
    `PAYOUT_NOTIFICATION` — fire on

    **both** success and failure (a failed one carries `status: "Unsuccessful"`,
    `requestState:

    "Failed"`; the debit is returned via the reversal workflow), as do
    `BILLS_PURCHASE_NOTIFICATION`

    and `VERIFICATION_NOTIFICATION`. FX conversion and virtual-account creation
    are **synchronous** —

    failures come back in the call's own response, not a webhook. See
    `docs/webhooks.md` for the full

    contract.


    ## Bill payments (VAS)


    Each VAS vertical follows **discover → (validate) → purchase**:


    - **Discover** the billers/networks at runtime (don't hardcode them):
    airtime
      `GET /business/vas/airtime/fetch-providers`; data `…/internet/data/fetch-providers` (+ `…/lookup`
      for plans); electricity `…/electricity/billers`; cable `…/cable-tv/fetch-providers` and
      `…/cable-tv/fetch-billers`; betting `…/betting/billers`.
    - **Validate** the customer where applicable (electricity meter, cable
    smartcard, betting id)
      before charging.
    - **Purchase**, then reconcile via the `BILLS_PURCHASE_NOTIFICATION` webhook
    or by polling
      `GET /business/vas/status?reference=…`.

    Common codes — **airtime networks**: `MTN`, `AIRTEL`, `GLO`, `9MOBILE` (data
    products use the

    `MTNDATA` / `GLODATA` / `AIRTELDATA` / `9MOBILEDATA` provider codes returned
    by lookup). **Cable**:

    `DSTV`, `GOTV`, `STARTIMES`. **Electricity discos**: AEDC, BEDC, EEDC,
    EKEDC, IBEDC, IKEDC, JEDC,

    KAEDCO, KEDCO, PHEDC, YEDC. Always prefer the discovery endpoints — the
    catalog can change.


    ## Not documented here (in flight)


    The cross-border surface — `/business/fx/*`, `/business/payout/*`,
    `/business/usd/*` — is being

    rebuilt and is intentionally omitted from this reference until it
    stabilises.
  contact:
    name: Numero Developer Support
    email: developers@usenumero.com
servers:
  - url: https://api.usenumero.com/numeroaccount/api/v1
    description: >-
      The only base URL. Mode is decided by your key, not the host: send a
      live_key_ for Live (real money) or a test_key_ for Test (simulated
      providers + an isolated test wallet).
security:
  - NumeroApiKey: []
tags:
  - name: Balance
    description: Wallet balance.
  - name: Transfers
    description: NGN bank transfers (payouts) — name enquiry, fee preview, single & bulk.
  - name: Virtual Accounts
    description: Issue and look up virtual accounts (collections).
  - name: Airtime
  - name: Data
  - name: Electricity
  - name: Cable TV
  - name: Betting
  - name: Verification
    description: Identity & business verification (KYC/KYB).
  - name: Cards
    description: >-
      Issue and manage virtual USD cards. Two ways to use it: issue directly
      (the card endpoints below auto-create a cardholder from the details you
      pass), or run the customer-first model — create a Cardholder, then issue
      cards to them (see Cardholders). The card issuer is abstracted; you
      integrate one Numero card API. Requires an approved card-product
      entitlement + a funded USD card wallet (fund-first). Permanent
      termination/cancellation is not currently exposed through the public API
      in test or live mode. Freeze is reversible and does not return card funds.
  - name: Cardholders
    description: >-
      Your customers (cardholders) as a first-class resource — create a
      cardholder once (you attest their KYC), then issue them cards, list their
      cards, and pull their statement.
  - name: Webhooks
    description: >-
      Events Numero delivers to your endpoint. Subscriptions (URL + event types
      + signing secret) are managed in Dashboard → Settings → Developer →
      Webhooks — that surface is JWT/dashboard-only, not callable with an API
      key. Verify the X-Numero-Signature on every delivery before trusting it.
  - name: Customers
    description: >-
      End-customer records — attach virtual accounts, transactions and KYC to a
      customer you create once.
  - name: Customer Wallets
    description: >-
      Wallet-as-a-Service. Give each customer one or more currency wallets,
      attach a virtual account so a wallet can receive external money, move
      value between wallets, pay out to a bank, and back a card. Wallet balances
      are a strictly-maintained sub-ledger of your pooled balance: a wallet can
      never spend another customer's money.
  - name: Invoices
    description: >-
      Invoices, each with a dedicated one-time virtual account for
      pay-by-transfer.
  - name: Credit
    description: >-
      Credit-bureau reports and scores, keyed by BVN. credit-summary aggregates
      all four bureaus in one call; the individual bureau endpoints are there
      when you need a specific source.
  - name: Sandbox
    description: >-
      Test-mode helpers, callable only with a `test_key_`: fund or reset your
      isolated test wallet and fire sample webhook deliveries. They can never
      touch live balances.
  - name: Utilities
paths:
  /business/partner-benefits/fee-preview:
    post:
      tags:
        - Partner Benefits
      summary: Preview an eligible Numero service fee
      description: >-
        Requires wallet:read and a valid request signature. Returns the
        business's applicable fee and savings. Statutory charges remain payable.
        This preview does not reserve allowance, confirm service availability,
        or quote the complete transaction debit. Use the transaction's normal
        quote and confirmation flow before purchasing.
      operationId: previewPartnerFee
      parameters:
        - name: X-Numero-Signature
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - serviceCode
                - currency
                - amount
              properties:
                serviceCode:
                  type: string
                  enum:
                    - FTXFR
                    - INTXFR
                    - AIRTIME
                    - DATA
                    - CABLETV
                    - ELECTRICITY
                    - BETTING
                    - GLOBALAIRTIME
                    - GLOBALDATA
                    - GIFTCARD
                    - UTILITY
                currency:
                  type: string
                  enum:
                    - NGN
                    - GBP
                amount:
                  type: number
                  exclusiveMinimum: 0
      responses:
        '200':
          description: Fee-only preview in major currency units.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: boolean
                  data:
                    type: object
                    properties:
                      serviceCode:
                        type: string
                      currency:
                        type: string
                      standardFee:
                        type: number
                      fee:
                        type: number
                      savings:
                        type: number
                      note:
                        type: string
        '400':
          $ref: '#/components/responses/Error'
        '403':
          description: Required scope is missing.
      security:
        - NumeroApiKey: []
          NumeroSignature: []
components:
  responses:
    Error:
      description: >-
        Auth, signature, or validation failure (real 4xx/5xx status; envelope
        below)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiResponse'
          examples:
            missingApiKey:
              summary: No API key (HTTP 400)
              value:
                data: null
                error:
                  code: missing_field
                  message: Header key is missing.
                  param: null
                meta:
                  request_id: N20260603120000123
                  pagination: null
            invalidSignature:
              summary: Bad signature (HTTP 400)
              value:
                data: null
                error:
                  code: invalid_signature
                  message: Invalid signature.
                  param: null
                meta:
                  request_id: N20260603120000123
                  pagination: null
  schemas:
    ApiResponse:
      type: object
      description: >-
        Canonical response envelope for the public surface. `data` on success,
        `error` on failure.
      required:
        - data
        - error
        - meta
      properties:
        data:
          description: Endpoint payload on success (object or array); null on failure.
        error:
          oneOf:
            - $ref: '#/components/schemas/ApiError'
            - type: 'null'
        meta:
          $ref: '#/components/schemas/Meta'
    ApiError:
      type: object
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
          description: Human-readable reason (don't branch on this — use code).
        param:
          type:
            - string
            - 'null'
          description: The offending field, when applicable.
    Meta:
      type: object
      properties:
        request_id:
          type: string
          description: Also returned as the X-Request-Id header.
        pagination:
          oneOf:
            - $ref: '#/components/schemas/Pagination'
            - type: 'null'
    ErrorCode:
      type: string
      description: Stable machine-readable error code.
      enum:
        - missing_field
        - validation_failed
        - invalid_request
        - invalid_api_key
        - invalid_signature
        - unauthorized
        - forbidden
        - insufficient_balance
        - invalid_amount
        - limit_exceeded
        - rate_limited
        - duplicate_request
        - idempotency_conflict
        - already_exists
        - business_not_found
        - business_inactive
        - account_not_found
        - transaction_not_found
        - customer_not_found
        - virtual_account_not_found
        - recipient_not_found
        - invalid_otp
        - otp_expired
        - otp_required
        - invalid_pin
        - pin_not_set
        - kyc_required
        - approval_required
        - activation_required
        - invalid_bank
        - invalid_account_number
        - invalid_bvn
        - provider_error
        - provider_unavailable
        - provider_timeout
        - not_found
        - not_implemented
        - internal_error
        - request_failed
    Pagination:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        total:
          type: integer
        has_more:
          type: boolean
  securitySchemes:
    NumeroApiKey:
      type: apiKey
      in: header
      name: X-Numero-Api-Key
      description: >-
        Your business API key (`live_key_...`). Issued on approval; managed in
        Dashboard → Settings → Developer.
    NumeroSignature:
      type: apiKey
      in: header
      name: X-Numero-Signature
      description: >
        Base64(HMAC-SHA256(publicApiKey, input)) over the raw request body
        (POST) or canonicalized

        query string (signed GET). Also send `X-Numero-Signature-Version: v2`.
        Required on

        money-movement / state-changing endpoints (marked 🔒).


        The HMAC key is your **Public Key** as raw UTF-8 bytes — a SECRET
        despite the name, and a

        different value from the `X-Numero-Api-Key` authentication header.
        Signing with the API key

        will never produce a valid signature.

````

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