curl --request POST \
--url https://sandbox-api.sakawave.example/v1/bill-payments/enquiries \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerId": "04117997788",
"billerId": "01KZXPYGX5VZXDH9VHJ4YTPCB9",
"packageId": "01KZXPYGX5VZXDH9VHJ4YTPCC1",
"amount": "5000.00"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
customerId: '04117997788',
billerId: '01KZXPYGX5VZXDH9VHJ4YTPCB9',
packageId: '01KZXPYGX5VZXDH9VHJ4YTPCC1',
amount: '5000.00'
})
};
fetch('https://sandbox-api.sakawave.example/v1/bill-payments/enquiries', 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/enquiries"
payload = {
"customerId": "04117997788",
"billerId": "01KZXPYGX5VZXDH9VHJ4YTPCB9",
"packageId": "01KZXPYGX5VZXDH9VHJ4YTPCC1",
"amount": "5000.00"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4W5X6Y7Z8ABCDEFGHJ",
"billerId": "01KZXPYGX5VZXDH9VHJ4YTPCB9",
"billerSlug": "example-disco",
"packageId": "01KZXPYGX5VZXDH9VHJ4YTPCC1",
"packageSlug": "example-disco-prepaid",
"customer": {
"customerName": "ADA OKAFOR",
"address": "14 Marina Road",
"district": "Lagos Island",
"canVend": true,
"minimumVend": "500.00"
},
"totalDue": null,
"minimumAmount": "500.00",
"charge": "100.00",
"commission": "0.00",
"totalDebit": "5100.00",
"expiresAt": "2026-08-14T09:20:00.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."
}Validate a customer
Asks the biller who this customer is and what they owe. An enquiry is required before a payment and is valid for ten minutes.
curl --request POST \
--url https://sandbox-api.sakawave.example/v1/bill-payments/enquiries \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerId": "04117997788",
"billerId": "01KZXPYGX5VZXDH9VHJ4YTPCB9",
"packageId": "01KZXPYGX5VZXDH9VHJ4YTPCC1",
"amount": "5000.00"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
customerId: '04117997788',
billerId: '01KZXPYGX5VZXDH9VHJ4YTPCB9',
packageId: '01KZXPYGX5VZXDH9VHJ4YTPCC1',
amount: '5000.00'
})
};
fetch('https://sandbox-api.sakawave.example/v1/bill-payments/enquiries', 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/enquiries"
payload = {
"customerId": "04117997788",
"billerId": "01KZXPYGX5VZXDH9VHJ4YTPCB9",
"packageId": "01KZXPYGX5VZXDH9VHJ4YTPCC1",
"amount": "5000.00"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "01K2Z3AB4W5X6Y7Z8ABCDEFGHJ",
"billerId": "01KZXPYGX5VZXDH9VHJ4YTPCB9",
"billerSlug": "example-disco",
"packageId": "01KZXPYGX5VZXDH9VHJ4YTPCC1",
"packageSlug": "example-disco-prepaid",
"customer": {
"customerName": "ADA OKAFOR",
"address": "14 Marina Road",
"district": "Lagos Island",
"canVend": true,
"minimumVend": "500.00"
},
"totalDue": null,
"minimumAmount": "500.00",
"charge": "100.00",
"commission": "0.00",
"totalDebit": "5100.00",
"expiresAt": "2026-08-14T09:20:00.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."
}Why the enquiry exists
A bill is paid to a person, not just to a biller. The enquiry confirms the meter or account exists, returns the name and address the biller holds for it, and prices the payment — charge, commission, andtotalDebit. Show that name to the payer before taking money: it is the only check that the reference was typed correctly.
Naming the product
Sakawave identifiers only. A slug will not resolve here, and neither will an upstream provider’s id — the route to a provider, and whatever that provider needs in its own payload, is resolved on our side. The same body therefore works whichever provider serves the biller, and keeps working when we change one.| Send | Rule |
|---|---|
billerId | Required. The biller’s id from the catalog. |
packageId | Required when the biller has packages, rejected when it has none. |
amount | Required when no package fixes the price; must match a fixed package’s price if sent. |
groupId, groupSlug, billerSlug, packageSlug, productName — is rejected.
Amounts are decimals here
Bill payments use decimal strings —"5000.00" — not the minor units the rest of the API uses. Everything in this service, request and response alike, is in naira and kobo written out.
409 bill_enquiry_expired. Enquire immediately before paying, not when the user opens the form.Error codes
| Status | code | Cause |
|---|---|---|
400 | validation_failed | A field failed validation, or an unrecognised field was sent. |
400 | customer_lookup_failed | The biller could not validate this customer reference. Its own message is passed through. |
400 | bill_package_required | The biller sells packages and none was named. |
400 | bill_package_not_supported | The biller is paid by amount and has no packages. |
400 | bill_amount_required | This product needs an amount. |
400 | bill_amount_mismatch | The amount does not match the fixed price of the package. |
404 | resource_not_found | No such biller, or the package does not belong to it. |
409 | bill_product_unavailable | No active provider currently serves this biller or package. |
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
Sakawave identifiers only. Slugs and provider references are rejected, so the same body works whichever provider serves the biller.
What the biller identifies the customer by: a meter number, an account number, or a phone number for airtime and data.
1 - 200The biller’s Sakawave id, from the catalog.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
The package’s Sakawave id. Required when the biller has packages, rejected when it has none.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
A decimal display amount returned by transfer APIs. Preserve it as a string.
^-?\d+(?:\.\d+)?$"25000.00"
Response
Successful response.
Send this as enquiryId when creating the payment.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
A 26-character ULID.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
Null when the biller sells no packages.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
What the biller returned about the customer, filtered to a safe subset: names, customer type, address, district, whether they can be vended, minimum vend, total due, due date, and available products. Shape varies by biller.
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"
A decimal display amount returned by transfer APIs. Preserve it as a string.
^-?\d+(?:\.\d+)?$"25000.00"
"2026-08-14T09:00:00.000Z"