Pay Bills and Top Up
End-to-end service purchase : catalogue, validate, quote, order, fulfilment webhook.
The full flow for selling airtime, data, electricity, TV, and other bill services. Every service uses the same endpoints: only the input steps differ, and the API tells you which steps apply.
1. Resolve the catalogue and definition
Load the catalogue for the service country. For a home purchase that is the customer's home country; for cross-border, it is the country the customer picked inside the service flow (see Countries).
curl "https://api.ouipay.africa/v1/services?country=NG" \
-H "Authorization: Bearer sk_test_..."Render items where access.executable is true. Then load the definition for
the chosen service: its steps array is the form schema, requires_validation
says whether to verify the recipient, and country_context says whether a
country picker applies.
2. Collect the inputs the steps ask for
Resolve each select-* step against its catalogue endpoint:
| Step | Endpoint | Collected field |
|---|---|---|
select-operator | GET /services/{code}/operators?country= | operator_code |
select-variant | GET /services/{code}/variants?country= | meter_type |
select-plan | GET /services/{code}/plans?country=&operator= | product_code |
Text steps (phone-input, account-input, email-input, amount-input) are
customer input: honour min_length, keyboard, quick_amounts, and
labels_by_variant from the step definition.
3. Validate the recipient
When requires_validation is true (metered bills, TV, education, internet),
verify the account before quoting:
curl https://api.ouipay.africa/v1/services/electricity/validate \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"country_code": "NG",
"account_ref": "04591234567",
"operator_code": "ikeja-electric",
"meter_type": "prepaid"
}'Require valid and serviceability.serviceable, show customer_name for
confirmation, and keep the whole response: it goes back on the order as
validation_snapshot. See
Validate a recipient.
4. Quote the price
curl https://api.ouipay.africa/v1/services/quote \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"provider_cost_minor": 500000,
"currency": "NGN",
"country_code": "NG",
"service_code": "electricity",
"payment_method": "wallet"
}'Show customer_amount_minor, fee_minor, and expires_at on the review
screen. For cross-currency purchases the quote locks the FX rate: quote_id
is then mandatory on the order. See
Create a quote.
5. Create the order
curl https://api.ouipay.africa/v1/services/orders \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: svc-order-8842" \
-H "Content-Type: application/json" \
-d '{
"country_code": "NG",
"service_code": "electricity",
"provider_cost_minor": 500000,
"currency": "NGN",
"payment_method": "wallet",
"operator_code": "ikeja-electric",
"meter_type": "prepaid",
"account_ref": "04591234567",
"quote_id": "01M2S17..."
}'The 201 response carries order, payment, and the collection next action
(checkout_url or instructions). Complete the payment the way payment
directs. See Create a service order.
6. Deliver on the webhook
Fulfilment starts when the payment settles. On service_order.completed, pull
the order and read the delivered artifact from fulfillment_snapshot (meter
token, PIN serial, voucher). On service_order.failed, the customer's funds
are released or refunded: surface the failure, do not re-purchase.
{
"event_type": "service_order.completed",
"event_key": "service_order.completed:01M2S18...",
"data": {
"transaction_id": "01M2S18...",
"service_order_id": "01M2S18...",
"service_code": "electricity",
"amount_minor": 510000,
"currency": "NGN"
}
}unknown status is a reconciliation state, not a failure: wait for the
terminal event. See Webhook Events and
order status.
Cross-border purchases
When country_context.selection.can_select_other_country is true for a
service, let the customer choose from available_countries and pass that code
as country / country_code on every subsequent call: catalogue, operators,
plans, validate, quote, order. Ineligible choices fail fast with
SERVICE_COUNTRY_NOT_ELIGIBLE (403), so the picker must stay inside the
returned list.
Failure modes to expect
error.code / failure.code | Cause | Your move |
|---|---|---|
SERVICE_COUNTRY_NOT_ELIGIBLE | Country not eligible for this account | Restrict the picker to available_countries |
SERVICE_UNAVAILABLE | Service not offered in the country | Hide the tile for that country |
SERVICE_NOT_EXECUTABLE | Coming soon / maintenance | Render the tile as unavailable |
QUOTE_MISMATCH / QUOTE_EXPIRED | Order does not match the locked quote | Re-quote, then re-submit |
INSUFFICIENT_FUNDS | Wallet cannot cover amount + fee | Prompt for funding or another method |
PIN_REQUIRED | Step-up needed | Collect PIN, re-send with X-Transaction-Authorization |
PROVIDER_FAILURE | Fulfilment rail error | Retry-safe via the same idempotency key |