OuiPay
Guides

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.

Rendering diagram...

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:

StepEndpointCollected field
select-operatorGET /services/{code}/operators?country=operator_code
select-variantGET /services/{code}/variants?country=meter_type
select-planGET /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.codeCauseYour move
SERVICE_COUNTRY_NOT_ELIGIBLECountry not eligible for this accountRestrict the picker to available_countries
SERVICE_UNAVAILABLEService not offered in the countryHide the tile for that country
SERVICE_NOT_EXECUTABLEComing soon / maintenanceRender the tile as unavailable
QUOTE_MISMATCH / QUOTE_EXPIREDOrder does not match the locked quoteRe-quote, then re-submit
INSUFFICIENT_FUNDSWallet cannot cover amount + feePrompt for funding or another method
PIN_REQUIREDStep-up neededCollect PIN, re-send with X-Transaction-Authorization
PROVIDER_FAILUREFulfilment rail errorRetry-safe via the same idempotency key