curl --request POST \
--url https://sandbox-api.sakawave.example/v1/bill-payments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"enquiryId": "01K2Z3AB4W5X6Y7Z8ABCDEFGHJ",
"accountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000.00",
"externalReference": "bill_20260814_001",
"customerName": "Ada Okafor",
"phoneNumber": "+2348012345678",
"email": "ada@example.com"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
enquiryId: '01K2Z3AB4W5X6Y7Z8ABCDEFGHJ',
accountId: '01K2Z3AB4D5E6F7G8H9JKMNPQR',
amount: '5000.00',
externalReference: 'bill_20260814_001',
customerName: 'Ada Okafor',
phoneNumber: '+2348012345678',
email: 'ada@example.com'
})
};
fetch('https://sandbox-api.sakawave.example/v1/bill-payments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/bill-payments"
payload = {
"enquiryId": "01K2Z3AB4W5X6Y7Z8ABCDEFGHJ",
"accountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000.00",
"externalReference": "bill_20260814_001",
"customerName": "Ada Okafor",
"phoneNumber": "+2348012345678",
"email": "ada@example.com"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4X5Y6Z7A8BCDEFGHJK",
"externalReference": "bill_20260814_001",
"paymentReference": "SKWV-BIL-01K2Z3AB4X5Y6Z7A8BCDEFGHJK",
"providerTransactionId": "CP-88213471",
"status": "confirmed",
"subCategory": "utility",
"amount": "5000.00",
"charge": "100.00",
"commission": "0.00",
"totalDebit": "5100.00",
"currency": "NGN",
"group": {
"name": "Electricity",
"slug": "DISCO"
},
"biller": {
"name": "Example Disco",
"slug": "EXAMPLE_DISCO"
},
"package": {
"name": "Prepaid",
"slug": "EXAMPLE_DISCO_PREPAID"
},
"customerReferenceLast4": "7788",
"customerName": "ADA OKAFOR",
"customerAddress": "14 Marina Road",
"vendStatus": "SUCCESSFUL",
"providerStatusCode": "00",
"tokenData": {
"token": "1234-5678-9012-3456-7890",
"units": "31.4"
},
"failureReason": null,
"createdAt": "2026-08-14T09:15:00.000Z",
"updatedAt": "2026-08-14T09:15:06.000Z"
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}{
"statusCode": 503,
"code": "provider_unavailable",
"message": "We could not issue an account number just now. Try again shortly."
}Pay a bill
Spends an enquiry and pays the biller. The debit happens immediately; delivery of the value can take longer, so the payment often comes back submitted or pending rather than confirmed.
curl --request POST \
--url https://sandbox-api.sakawave.example/v1/bill-payments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"enquiryId": "01K2Z3AB4W5X6Y7Z8ABCDEFGHJ",
"accountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000.00",
"externalReference": "bill_20260814_001",
"customerName": "Ada Okafor",
"phoneNumber": "+2348012345678",
"email": "ada@example.com"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
enquiryId: '01K2Z3AB4W5X6Y7Z8ABCDEFGHJ',
accountId: '01K2Z3AB4D5E6F7G8H9JKMNPQR',
amount: '5000.00',
externalReference: 'bill_20260814_001',
customerName: 'Ada Okafor',
phoneNumber: '+2348012345678',
email: 'ada@example.com'
})
};
fetch('https://sandbox-api.sakawave.example/v1/bill-payments', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/bill-payments"
payload = {
"enquiryId": "01K2Z3AB4W5X6Y7Z8ABCDEFGHJ",
"accountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000.00",
"externalReference": "bill_20260814_001",
"customerName": "Ada Okafor",
"phoneNumber": "+2348012345678",
"email": "ada@example.com"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4X5Y6Z7A8BCDEFGHJK",
"externalReference": "bill_20260814_001",
"paymentReference": "SKWV-BIL-01K2Z3AB4X5Y6Z7A8BCDEFGHJK",
"providerTransactionId": "CP-88213471",
"status": "confirmed",
"subCategory": "utility",
"amount": "5000.00",
"charge": "100.00",
"commission": "0.00",
"totalDebit": "5100.00",
"currency": "NGN",
"group": {
"name": "Electricity",
"slug": "DISCO"
},
"biller": {
"name": "Example Disco",
"slug": "EXAMPLE_DISCO"
},
"package": {
"name": "Prepaid",
"slug": "EXAMPLE_DISCO_PREPAID"
},
"customerReferenceLast4": "7788",
"customerName": "ADA OKAFOR",
"customerAddress": "14 Marina Road",
"vendStatus": "SUCCESSFUL",
"providerStatusCode": "00",
"tokenData": {
"token": "1234-5678-9012-3456-7890",
"units": "31.4"
},
"failureReason": null,
"createdAt": "2026-08-14T09:15:00.000Z",
"updatedAt": "2026-08-14T09:15:06.000Z"
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}{
"statusCode": 503,
"code": "provider_unavailable",
"message": "We could not issue an account number just now. Try again shortly."
}Statuses
| Status | Meaning |
|---|---|
submitted | Sent to the biller. The account has already been debited. |
pending | The biller has not given a final answer. A reconciler keeps asking. |
confirmed | Value delivered. This is the success state — not completed. |
failed | The biller could not deliver. The debit is reversed automatically. |
failed with a failureReason.What comes from where
The enquiry decides most of the payment.customerName is used only when the biller returned no name of its own, and for airtime and data the recipient is the phone number from the enquiry, not the phoneNumber in this request.
amount may be omitted when the enquiry or the package already fixes it. Where it is required and disagrees with the fixed price, the payment is refused rather than adjusted.
Tokens
A biller that issues a credential — a prepaid electricity token, most often — returns it intokenData once the payment is confirmed. Its shape varies by biller. Show it to the customer and store it: it is not re-issued.
Idempotency
externalReference is the key. Reusing it returns the existing payment whatever its status, so a retry after a failure returns the failure — the same contract as transfers, not the re-driving one that cards use. A genuine retry needs a new reference and a new enquiry.
Error codes
| Status | code | Cause |
|---|---|---|
400 | validation_failed | A field failed validation. |
400 | invalid_phone_number | The phone number is not eight to fifteen digits. |
409 | bill_enquiry_expired | The enquiry is older than ten minutes, or has already been spent. Enquire again. |
409 | bill_product_unavailable | The biller or package has been withdrawn from the catalog since the enquiry. |
409 | bill_amount_required | This product needs an explicit amount. |
409 | invalid_bill_amount | The amount is not a positive decimal with at most two places. |
409 | bill_amount_mismatch | The amount disagrees with the enquiry or the package price. |
409 | amount_below_minimum | Below the biller’s minimum vend, which the enquiry returned as minimumAmount. |
409 | bill_enquiry_amount_unavailable | The enquiry priced nothing to pay. |
409 | bill_enquiry_pricing_unavailable | The enquiry has no charge or commission to apply. |
409 | insufficient_funds | The account cannot cover totalDebit. |
409 | bill_payment_account_ineligible | The account is inactive, not debitable, a customer wallet, or not NGN. |
503 | catalog_unavailable | The bills catalog is empty and could not be refreshed from the provider. |
503 | provider_unavailable | The bills provider could not be reached. |
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. |
Authorizations
A secret API key, sent as Authorization: Bearer sk_test_… in sandbox or Bearer sk_live_… in live.
Body
From the enquiry. Valid for ten minutes.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
The NGN account to debit.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
Your idempotency key.
1 - 120Ignored where the biller returned a name of its own.
2 - 200Eight to fifteen digits, optionally with a leading plus. Spaces and hyphens are stripped.
A decimal display amount returned by transfer APIs. Preserve it as a string.
^-?\d+(?:\.\d+)?$"25000.00"
255Response
Successful response.
A 26-character ULID.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
Sakawave’s own reference for the payment.
confirmed is the success state, not completed.
submitted, pending, confirmed, failed What kind of bill this was: airtime, data, utility, cable_tv, betting, and so on.
A decimal display amount returned by transfer APIs. Preserve it as a string.
^-?\d+(?:\.\d+)?$"25000.00"
A decimal display amount returned by transfer APIs. Preserve it as a string.
^-?\d+(?:\.\d+)?$"25000.00"
A decimal display amount returned by transfer APIs. Preserve it as a string.
^-?\d+(?:\.\d+)?$"25000.00"
A decimal display amount returned by transfer APIs. Preserve it as a string.
^-?\d+(?:\.\d+)?$"25000.00"
Bill payments are NGN only.
NGN Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
The last four characters of the meter, account, or phone number paid.
The biller’s own delivery status, upper-cased.
The credential the biller issued, where there is one — a prepaid electricity token, for instance. Shape varies by biller.
"2026-08-14T09:00:00.000Z"
"2026-08-14T09:00:00.000Z"