> ## 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.

# Airtime, data, and utilities

> How the product families differ in what they need, what they return, and how they fail.

This is the bills-specific integration guide. The three steps are the same for every bill; what changes is what the enquiry needs, what it gives back, and what "delivered" means.

## The families

| Family | Customer id | Package | Amount | Returns |
| - | - | - | - | - |
| **Airtime** | Phone number | Optional | You choose | Nothing to store |
| **Data** | Phone number | **Required** | Fixed by the package | Nothing to store |
| **Utilities** (prepaid) | Meter number | Required | You choose, above a minimum | **A token** |
| **Utilities** (postpaid) | Account number | Required | Often `totalDue` | Nothing to store |
| **Cable TV** | Smartcard number | Required | Fixed by the package | Nothing to store |

The two columns that drive your code are *package* and *amount*. Get those wrong and the enquiry refuses before you reach a payment.

## Airtime: the exception

Airtime billers sell no packages at all — the customer names an amount.

```jsonc theme={null}
{ "customerId": "+2348012345678", "billerId": "01KZXPYGX5VZXDH9VHJ4YTPCC1", "amount": "1000.00" }
```

Send the airtime biller and an amount. Naming a package is `bill_package_not_supported`, except for the legacy airtime package id, which is still accepted and ignored. You can tell the two shapes apart from the catalog: an airtime biller returns no packages.

The phone number is normalised for you, so `0801 234 5678`, `+2348012345678`, and `2348012345678` are the same customer.

<Note>
  For airtime and data, the recipient is the phone number from the **enquiry**. The `phoneNumber` in the payment body is contact information, not the destination. Getting the number right matters at enquiry time, not at payment time.
</Note>

## Data: fixed prices

A data bundle is priced by its package, and the price is in the catalog:

```json theme={null}
{
  "id": "01KZXPYGX5VZXDH9VHJ4YTPCE3",
  "billerId": "01KZXPYGX5VZXDH9VHJ4YTPCD2",
  "name": "5GB Monthly",
  "slug": "example-telco-5gb",
  "amount": "3000.00",
  "currency": "NGN",
  "isFixedAmount": true
}
```

`isFixedAmount: true` means the price is authoritative: send that exact amount, or omit it. Sending a different one is `bill_amount_mismatch`. Where `isFixedAmount` is `false`, `amount` is a suggestion you may override, and where it is also `null` you must supply one.

## Utilities: minimums and tokens

Prepaid electricity is the richest case, and the one worth building against first.

**The enquiry gives you a minimum.**

```json theme={null}
{
  "customer": { "customerName": "ADA OKAFOR", "address": "14 Marina Road", "district": "Lagos Island", "canVend": true, "minimumVend": "500.00" },
  "minimumAmount": "500.00",
  "charge": "100.00",
  "totalDebit": "5100.00"
}
```

Below `minimumAmount`, the payment is refused with `amount_below_minimum`. Validate against the value the enquiry returned rather than a constant of your own — it varies by biller and can change.

**Postpaid gives you a balance instead.** `totalDue` is what the biller says is outstanding. Offering it as the default amount is usually what a customer wants.

**Prepaid gives you a token.**

```json theme={null}
{
  "status": "confirmed",
  "vendStatus": "SUCCESSFUL",
  "tokenData": { "token": "1234-5678-9012-3456-7890", "units": "31.4" }
}
```

<Warning>
  The token is the value. It is issued once, is not re-issued, and a customer who loses it has lost what they paid for. Store it when it arrives and deliver it through a channel the customer keeps — not only an ephemeral screen.
</Warning>

`tokenData` is whatever the biller returned; its keys vary. Store the whole object rather than plucking a field you have seen once.

## `canVend`, and what it does not promise

Several billers return `canVend` on the customer. It means the biller will accept a payment for this customer *right now*. It does **not** mean the vend will succeed — a payment can still come back `failed` after a confirmed enquiry.

Treat it as a pre-flight check that saves a wasted debit, not as a guarantee.

## Reading `vendStatus`

`vendStatus` is the biller's own word for delivery, upper-cased and passed through. It is not a Sakawave enum, and its vocabulary differs by biller.

* **Branch on `status`** — `submitted`, `pending`, `confirmed`, `failed`. That is Sakawave's, and it is stable.
* **Display `vendStatus`** in support tooling, where the biller's own wording helps someone reading a ticket.
* **Never parse it.** A new biller can introduce a word you have never seen.

The same applies to `providerStatusCode`.

## Failure, and who tells the customer

Every family fails the same way: the payment goes `failed`, and Sakawave reverses the debit automatically.

What differs is what the customer expected. Someone who bought electricity and received a silent refund has no electricity and no explanation. Wire `bills_payment.failed` to a customer-facing message, with `failureReason` where it is readable.

```ts theme={null}
onEvent('bills_payment.failed', async (data) => {
  const bill = data.relatedOperation;
  await notify(customerFor(bill), {
    what: `${bill.billerName} — ${bill.packageName}`,
    outcome: 'not delivered; your account has been refunded',
    reason: bill.failureReason,
  });
});
```

## Choosing amounts per family

```ts theme={null}
function amountFor(pkg, enquiry, requested) {
  if (pkg.amount) return pkg.amount;                    // data, cable — fixed
  if (enquiry.totalDue) return enquiry.totalDue;        // postpaid — the balance
  if (enquiry.minimumAmount && lessThan(requested, enquiry.minimumAmount)) {
    throw new BelowMinimum(enquiry.minimumAmount);      // prepaid — a floor
  }
  return requested;                                     // airtime, prepaid top-up
}
```

Four rules, in that order, cover every family in the catalog.

## A checklist per family

* [ ] Airtime: airtime biller's `billerId`, no package, recipient from the enquiry
* [ ] Data: package required, amount from the package, never chosen
* [ ] Prepaid utility: validate against `minimumAmount`, store `tokenData`
* [ ] Postpaid utility: default the amount to `totalDue`
* [ ] All: show `customer.customerName` before paying, and `totalDebit` as the price
* [ ] All: branch on `status`, display `vendStatus`, parse neither the biller's codes nor its words


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