API reference
Products, plans, billing models, customers, subscriptions, payments, usage, and manual receipts.
All paths are under https://{host}/api/v2. Send X-API-Key and, when there is a body, Content-Type: application/json.
Typical success
{
"success": true,
"data": {},
"message": "..."
}A pricing-plan list returns { "pricing_plans": [] } and does not use success. Branch on the HTTP status, then read data for the id you need next.
Products
Create a product
/api/v2/biller/products/createHTTP 200Creates the product, its first plans, and each plan's billing model. Message: Product created successfully.
Fiber Internet with four packages: Starter, Plus, Pro (fixed, advance; Plus default) and Flex (metered, arrears).
Request body
{
"title": "Fiber Internet",
"description": "Fiber packages for home and office",
"category": "INTERNET",
"callback_url": "https://hub.example/hooks/biller",
"pricing_plans": [
{
"name": "Starter",
"amount": 899,
"billing_period": "MONTHLY",
"is_default": false,
"billing_model": { "model_type": "FIXED", "usage_source": "MANUAL" }
},
{
"name": "Plus",
"amount": 1499,
"billing_period": "MONTHLY",
"is_default": true,
"penalty_max_cap": 200,
"penalty_tiers": [
{
"penalty_type": "PERCENTAGE",
"value": 2,
"min_overdue_days": 7,
"sort_order": 1
}
],
"billing_model": { "model_type": "FIXED", "usage_source": "MANUAL" }
},
{
"name": "Pro",
"amount": 2499,
"billing_period": "MONTHLY",
"is_default": false,
"billing_model": { "model_type": "FIXED", "usage_source": "MANUAL" }
},
{
"name": "Flex",
"amount": 0,
"billing_period": "MONTHLY",
"is_default": false,
"billing_model": {
"model_type": "USAGE_BASED",
"usage_source": "MANUAL",
"usage_unit": "GB",
"price_per_unit": 15
}
}
]
}| Field | Required | Meaning |
|---|---|---|
| `title` | yes | 3 to 100 characters. |
| `description` | no | At most 255 characters. |
| `category` | yes | One of the category values in Core concepts. |
| `callback_url` | no | HTTPS endpoint for events. |
| `image_url` | no | A URL. |
| `pricing_plans` | yes | At least one plan. Each plan needs `billing_model`. |
Other product calls
Related endpoints
| Method | Path | Success | Parameters |
|---|---|---|---|
| GET | `/api/v2/biller/products/{product_id}` | 200 | Path: product UUID. Returns the product and its plans. |
| GET | `/api/v2/biller/products/all` | 200 | Query: `page`, `page_size` (required). Optional: `text`, `category`, `billing_period`, `is_active`. |
| PUT | `/api/v2/biller/products/{product_id}` | 200 | Body: `title`, `description`, `category`, `callback_url`, `is_active`. |
| DELETE | `/api/v2/biller/products/{product_id}` | 200 | Message: `Product deleted successfully`. |
Pricing plans
The first plan is created with the product. Add another with:
/api/v2/biller/products/{product_id}/pricing-plansHTTP 201Pro Annual is twelve months of 100 Mbps, paid in advance (FIXED).
Request body
{
"name": "Pro Annual",
"amount": 24990,
"billing_period": "YEARLY",
"is_default": false,
"billing_model": {
"model_type": "FIXED",
"usage_source": "MANUAL"
}
}| Field | Required | Meaning |
|---|---|---|
| `name` | no | Empty becomes `Default`. At most 100 characters. |
| `amount` | yes | Zero or greater. The charge for a `FIXED` plan. |
| `billing_period` | yes | `DAILY`, `WEEKLY`, `MONTHLY`, or `YEARLY`. |
| `recurring_interval` | no | At least 1. Default 1. |
| `is_default` | no | Whether this is the default offer. |
| `penalty_tiers` | no | Late-fee windows. See Core concepts. |
| `penalty_max_cap` | no | Maximum late fee. |
| `billing_model` | yes | `model_type` and `usage_source` are required. |
Related endpoints
| Method | Path | Success | Notes |
|---|---|---|---|
| GET | `/api/v2/biller/products/{product_id}/pricing-plans` | 200 | Response field `pricing_plans`. |
| PUT | `/api/v2/biller/pricing-plans/{pricing_plan_id}` | 200 | Send only fields you change. |
| DELETE | `/api/v2/biller/pricing-plans/{pricing_plan_id}` | 200 | Deactivates. Last active plan → HTTP 500. |
Billing models
Created inside pricing_plans[].billing_model. Standalone create:
/api/v2/biller/billing-modelsHTTP 200Request body
{
"product_id": "{product_id}",
"pricing_plan_id": "{pricing_plan_id}",
"model_type": "USAGE_BASED",
"usage_source": "MANUAL",
"usage_unit": "GB",
"price_per_unit": 15
}FIXED is advance; USAGE_BASED, TIERED, HYBRID, MANUAL are arrears. For TIERED:
/api/v2/biller/billing-models/{billing_model_id}/tiersTiers body
{
"tiers": [
{ "tier_min": 0, "tier_max": 20, "price_per_unit": 10, "tier_order": 1 },
{ "tier_min": 20, "price_per_unit": 20, "tier_order": 2 }
]
}Related endpoints
| Method | Path | Success | Notes |
|---|---|---|---|
| GET | `/api/v2/biller/products/{product_id}/billing-model` | 200 | The model for the product. |
| GET | `/api/v2/biller/billing-models/{billing_model_id}` | 200 | One model. |
| GET | `/api/v2/biller/billing-models/usage-based` | 200 | Query: `page`, `page_size`. |
| PUT | `/api/v2/biller/billing-models/{billing_model_id}` | 200 | Update model fields. |
| DELETE | `/api/v2/biller/billing-models/{billing_model_id}` | 200 | Removes the model. |
Customers
/api/v2/biller/customersHTTP 201data.id is the customer id.
Request body
{
"first_name": "Abel",
"last_name": "Bekele",
"phone_number": "251911000111",
"email": "abel.bekele@example.com"
}| Field | Required | Meaning |
|---|---|---|
| `phone_number` | yes | 9 to 15 digits. |
| `first_name`, `last_name` | no | 2 to 100 characters. |
| `email` | no | A valid email. |
Related endpoints
| Method | Path | Success | Notes |
|---|---|---|---|
| GET | `/api/v2/biller/customers/{customer_id}` | 200 | Customer plus products, subscriptions, product users. |
| GET | `/api/v2/biller/customers` | 200 | Query: `page` (> 0), `page_size`. Optional `phone_number`. |
| PUT | `/api/v2/biller/customers/{customer_id}` | 200 | Name, phone, or email. |
| DELETE | `/api/v2/biller/customers/{customer_id}` | 200 | A later get is `404`. |
Subscriptions
/api/v2/biller/products-user/createHTTP 201data.id is the product user id.
With an existing customer
Request body
{
"product_id": "{product_id}",
"pricing_plan_id": "{pricing_plan_id}",
"customer_id": "{customer_id}",
"unique_account_id": "HUB-10042",
"phone_number": "251911000111",
"subscribed_at": "2026-10-22",
"send_checkout_notification": false,
"subscription": {}
}Or create the customer in the same call
Request body
{
"product_id": "{product_id}",
"pricing_plan_id": "{pricing_plan_id}",
"unique_account_id": "HUB-10042",
"phone_number": "251911000111",
"subscribed_at": "2026-10-22",
"send_checkout_notification": false,
"customer": {
"first_name": "Abel",
"last_name": "Bekele",
"phone_number": "251911000111"
},
"subscription": {}
}| Field | Required | Meaning |
|---|---|---|
| `product_id`, `pricing_plan_id` | yes | UUIDs from create. |
| `unique_account_id` | yes | Your subscriber id, such as `HUB-10042`. |
| `phone_number` | yes | 9 to 15 digits. |
| `subscribed_at` | yes | `YYYY-MM-DD`. |
| `customer_id` or `customer` | one of them | Existing customer, or a new one. |
| `subscription` | yes | `{}` renews until cancel. `duration` or `ends_at` is fixed term. `trial_ends_at` starts `TRIALING`. |
| `send_checkout_notification` | no | `false` when you deliver the checkout link yourself. |
/api/v2/biller/products-user/cancel/{product_user_id}HTTP 200No body. Cancels the subscription.
/api/v2/biller/products-user/resubscribe/{product_user_id}HTTP 200Resubscribe body
{ "subscribed_at": "2026-10-22" }Related endpoints
| Method | Path | Success | Notes |
|---|---|---|---|
| GET | `/api/v2/biller/products-user/{product_user_id}` | 200 | One subscription. |
| GET | `/api/v2/biller/products-user/all/{product_id}` | 200 | Subscriptions on one product. |
| GET | `/api/v2/biller/products-user/merchant/all` | 200 | Query: `page`, `page_size`. |
| PUT | `/api/v2/biller/products-user/{product_user_id}` | 200 | Account id, subscribed day, or active flag. |
Start checkout
/api/v2/biller/products-user/payment/initiateHTTP 200Requires X-API-Key. data includes checkout_url, amount, due date, and names.
Request body
{
"product_id": "{product_id}",
"product_user_id": "{product_user_id}",
"callback_url": "https://hub.example/hooks/biller"
}Payments
You do not create the recurring bill with a POST. You read it.
Read endpoints
| Method | Path | Success | Notes |
|---|---|---|---|
| GET | `/api/v2/biller/payments` | 200 | Query: `page`, `page_size`. This merchant's bills. |
| GET | `/api/v2/biller/payments/{payment_reference}` | 200 | One bill by reference. |
| GET | `/api/v2/biller/payments/timeline/{payment_id}` | 200 | Lifecycle events for one payment. |
Advance: due_date is period start. Arrears: period end. Both period start and end fields are present either way.
Usage
Record a quantity
/api/v2/biller/usage/recordHTTP 201Post before an arrears period ends.
Request body
{
"product_user_id": "{product_user_id}",
"billing_period_start": "2026-10-01T00:00:00Z",
"billing_period_end": "2026-11-01T00:00:00Z",
"usage_quantity": 12.5,
"usage_unit": "GB",
"source_reference": "usage-2026-10"
}Record a money amount
/api/v2/biller/usage/manualHTTP 201manual_amount is required.
Request body
{
"product_user_id": "{product_user_id}",
"billing_period_start": "2026-10-01T00:00:00Z",
"billing_period_end": "2026-11-01T00:00:00Z",
"usage_unit": "etb",
"manual_amount": 180,
"source_reference": "adjustment-180"
}Read usage
Read endpoints
| Method | Path | Success | Notes |
|---|---|---|---|
| GET | `/api/v2/biller/usage/{product_user_id}` | 200 | Readings for one subscription. |
| GET | `/api/v2/biller/usage/product-user/period` | 200 | Query: ids + `period_start` / `period_end` as `YYYY-MM-DD`. |
| GET | `/api/v2/biller/usage/billing-model/{billing_model_id}` | 200 | Readings for one model. |
Manual payment banks
/api/v2/biller/manual-payment-banksHTTP 200data.id is the bank id. bank_code, bank_name, and bank_account_number are required.
Request body
{
"bank_code": "CBE",
"bank_name": "Commercial Bank of Ethiopia",
"bank_account_number": "1000123456789",
"status": "ACTIVE"
}Related endpoints
| Method | Path | Success | Notes |
|---|---|---|---|
| GET | `/api/v2/biller/manual-payment-banks` | 200 | This merchant's accounts. |
| GET | `/api/v2/biller/manual-payment-banks/active` | 200 | Accounts that can take a new receipt. |
| PUT | `/api/v2/biller/manual-payment-banks/{bank_id}` | 200 | Full bank body: code, name, account number, status. |
Manual payments
/api/v2/biller/manual-payments/submitHTTP 200The bill stays unpaid until approval.
Request body
{
"payment_id": "{payment_id}",
"bank_id": "{bank_id}",
"amount": 100,
"receipt_url": "https://files.example/receipts/cbe-1001.pdf",
"receipt_file_name": "cbe-1001.pdf"
}| Field | Required | Meaning |
|---|---|---|
| `payment_id` | yes | The bill. |
| `bank_id` | yes | One of your active banks. |
| `amount` | yes | The amount on the receipt. |
| `receipt_url` | yes | A URL your staff can open. |
| `receipt_file_name` | no | The file name you want stored. |
Confirm / approve / reject / list
| Method | Path | Body | Result |
|---|---|---|---|
| POST | `/api/v2/biller/manual-payments/confirm` | Same as submit. | Submits and approves. Bill marked paid. |
| POST | `/api/v2/biller/manual-payments/{id}/approve` | None. | Approves pending receipt; bill paid. |
| POST | `/api/v2/biller/manual-payments/{id}/reject` | `{ "reason": "..." }` | `reason` required. Bill stays unpaid. |
| GET | `/api/v2/biller/manual-payments/pending` | Receipts waiting for a decision. | |
| GET | `/api/v2/biller/manual-payments` | This merchant's receipts. |