Errors
Error response shapes, HTTP status codes, auth failures, and troubleshooting.
Response format
Success
{
"success": true,
"data": {},
"message": "Product created successfully"
}Handler-built error
{
"success": false,
"error": {
"type": "UNAUTHORIZED",
"message": "Invalid API key"
}
}Shared middleware error
{
"success": false,
"error": {
"type": "validation_bad_input",
"message": "validation failed"
}
}On the middleware shape, type is the last segment of the internal code (validation_bad_input, resource_not_found, forbidden). Trust HTTP status; some not-found/bad-input cases are HTTP 500. Match message as well as status.
Unexpected 500 shape
{
"err": {},
"message": "Internal Server Error"
}Log status, error.type, and error.message. Do not log the API key.
HTTP status codes
200
Read, update, delete, product create, bank create, receipt submit, checkout initiate.
201
Customer create, added pricing plan, subscription create, usage create.
400
Malformed JSON, invalid ids/phones, missing required receipt or usage fields.
401
Missing, malformed, or unknown credentials.
403
No payment permission, key/merchant mismatch, or receipt for another merchant's bill.
404
This merchant does not have that product, customer, model, or payment.
500
Unexpected failure, plus ownership/validation cases called out below.
Authentication errors
| HTTP | `error.type` | Message contains | Meaning |
|---|---|---|---|
| 401 | `UNAUTHORIZED` | `Authentication token missing` | No API key. Route looked for dashboard login. |
| 401 | `UNAUTHORIZED` | `API key is required` | Initiate without an API key. |
| 401 | `UNAUTHORIZED` | `Invalid API key format` | Header is not `public:secret`. |
| 401 | `UNAUTHORIZED` | `Invalid API key` | Unknown key or wrong secret. |
| 401 | `UNAUTHORIZED` | `Invalid authentication token` | `Authorization: Bearer` is not a valid dashboard login. |
| 401 | `UNAUTHORIZED` | `Merchant not found` | Key does not resolve to a merchant. |
| 403 | `FORBIDDEN` | `not valid for this merchant status` | Sandbox/test or active/production pairing failed, or merchant pending/inactive. |
| 403 | `FORBIDDEN` | `payment processing permission` | Key is not allowed to take payments. |
Validation and ownership
| Situation | HTTP | `error.type` | Message contains |
|---|---|---|---|
| Product body is not JSON | 400 | `INVALID_REQUEST` | the parser error |
| Product create with no pricing plan | 500 | `INTERNAL_ERROR` | `at least one pricing plan is required` |
| Product id is not a UUID | 400 | `INVALID_REQUEST` | `Invalid product ID` |
| Product belongs to another merchant | 404 | `NOT_FOUND` | `Product not found` |
| Customer phone is not 9–15 digits | 400 | `validation_bad_input` | `validation failed` |
| Customer belongs to another merchant | 404 | `resource_not_found` | |
| Deactivate the last active plan | 500 | `validation_bad_input` | `cannot deactivate the last active pricing plan` |
| Another merchant reads or checks out the subscription | 500 | `INTERNAL_ERROR` | `product not found` or `does not belong to merchant` |
| Unknown payment reference | 404 | `resource_not_found` | `payment not found` |
| Usage quantity negative / period does not move forward | 500 | `INTERNAL_ERROR` | `cannot be negative` or `billing period end must be after start` |
| Usage body not JSON, or `manual_amount` missing | 400 | `VALIDATION_ERROR` | `manual_amount is required` when that field is the problem |
| Arrears bill with no usage report | `usage data not found for billing period` | ||
| Manual model with no amount for the period | `manual amount not provided for billing period` | ||
| Bank or receipt body missing required fields | 400 | `BAD_USER_INPUT` | |
| Receipt for another merchant's bill | 403 | `forbidden` | `does not belong to merchant` |
Troubleshooting
Common symptoms
401 on every call
Header name is X-API-Key, value is public:secret, and the secret matches the key issued for this merchant.
403 immediately
Merchant is sandbox while the key is production (or the reverse), or the key cannot take payments. Pending merchants cannot call the biller.
The arrears bill never appears
Subscription must be ACTIVE or INCOMPLETE, the period must have ended, and usage or manual_amount for that period must already be stored.
The advance bill appears before the month starts
Expected for FIXED. Due date is period start; the scheduler looks ahead by about one period.
Checkout says the billing window has not started
The bill for that cycle does not exist yet. For Flex, wait until the period ends and gigabytes are posted.
bill:paid arrived twice
Return 200 on the second delivery and do not post the invoice again.