01 Introduction
The Olimem Customer Portal API provides a secure integration framework allowing mobile applications, web applications, payment gateways, chatbots, and third-party systems to interact with the municipal billing and receipting platform.
- Customer Authentication
- Account Balance Enquiries
- Statements
- Notifications
- Service Requests
- Meter Reading Submissions
- Outage Management
- Document Distribution
- Payment Status Verification
- Transaction Posting
- Mobile POS Synchronisation
02 Authentication
2.1 Customer Authentication
Login authenticates as the calling council's customer-facing application first, then as the customer — every other Customer API request after that uses a Laravel Sanctum bearer token. Customer API is always council-scoped (no global-key support — see §4 Bot API for the channels that do span multiple clients).
Content-Type: application/json Accept: application/json X-API-KEY: YOUR_COUNCIL_CUSTOMER_KEY X-API-SECRET: YOUR_COUNCIL_CUSTOMER_SECRET
{
"account_number": "12345",
"password": "Password@123"
}
customer_portal, one key per council — issued from the hub's API Clients page) — not the customer. They scope the account_number lookup to one council so the same account number in two different councils can never collide. account_number / password then identify the specific customer within that council. The bearer token returned below is per-customer and is what every other Customer API request uses — it's safe to store in the browser precisely because it only ever represents one resident, not the whole council.{
"success": true,
"token": "1|fdfgdfgdfgdfgdfgdfg",
"user": {
"id": 1,
"name": "John Doe",
"fname": "John",
"lname": "Doe",
"email": "john@example.com",
"phone": "0771234567",
"account_no": "12345"
},
"account": {
"name": "John Doe",
"account": "12345",
"currency": "USD",
"balance": "123.45",
"address": "123 Main Street"
}
}
{
"success": false,
"message": "Invalid account number or password."
}
Authorization: Bearer {TOKEN}
03 Customer API
All endpoints below require the Authorization: Bearer {TOKEN} header obtained from Customer login. Formerly Portal API — renamed, same behavior.
3.1 Logout
{
"success": true,
"message": "Logged out."
}
3.2 Get Customer Profile
{
"success": true,
"user": {
"id": 1,
"name": "John Doe",
"fname": "John",
"lname": "Doe",
"email": "john@example.com",
"phone": "0771234567",
"account_no": "12345"
},
"account": {
"name": "John Doe",
"account": "12345",
"currency": "USD",
"balance": "123.45",
"address": "Bulawayo"
}
}
3.3 Update Profile
{
"fname": "Kelvin",
"lname": "Mtetwa",
"phone": "0771234567",
"current_password": "OldPassword",
"new_password": "NewPassword123",
"new_password_confirmation": "NewPassword123"
}
{
"success": true,
"message": "Profile updated."
}
3.4 Account Balance
{
"success": true,
"account": "12345",
"name": "John Doe",
"currency": "USD",
"balance": "123.45"
}
3.5 Customer Statement
{
"success": true,
"account": "12345",
"currency": "USD",
"balance": "123.45",
"receipts": [
{
"id": 1,
"reference": "REC-0001",
"amount": "50.00",
"date": "28 Jul 2026",
"source": "Customer"
}
]
}
3.6 Notifications
{
"success": true,
"unread_count": 3,
"notifications": [
{
"id": 1,
"type": "info",
"title": "Water Outage",
"message": "Scheduled maintenance.",
"is_read": false,
"created_at": "2026-07-28 10:00"
}
]
}
{
"success": true
}
3.7 Service Requests
{
"category": "water_leak",
"title": "Burst Pipe",
"description": "Water leaking outside property.",
"property_id": 145
}
{
"success": true,
"reference": "SR-ABCD1234",
"message": "Service request submitted."
}
- water_leak
- sewer_burst
- refuse_not_collected
- streetlight_fault
- billing_query
- meter_fault
- road_damage
- other
3.8 Meter Readings
{
"property_id": 145,
"service_id": 5,
"reading_date": "2026-07-28",
"reading_value": 2530,
"meter_number": "MTR-001",
"notes": "Customer reading."
}
{
"success": true,
"id": 10,
"message": "Meter reading submitted successfully."
}
{
"success": true,
"readings": [
{
"id": 1,
"reading_date": "28 Jul 2026",
"reading_value": 2500,
"previous_reading": 2450,
"consumption": 50,
"meter_number": "MTR-001",
"status": "pending"
}
]
}
3.9 Outages
{
"success": true,
"outages": [
{
"id": 1,
"title": "Water Outage",
"description": "Pipe maintenance",
"type": "water",
"area": "CBD",
"severity": "high",
"status": "scheduled"
}
]
}
3.10 Documents
{
"success": true,
"documents": [
{
"id": 1,
"title": "July Invoice",
"category": "Invoice",
"file_name": "invoice.pdf",
"file_type": "pdf",
"file_size": "1.2 MB",
"url": "https://yourdomain.com/storage/invoice.pdf"
}
]
}
04 Bot API
Used by the per-council WhatsApp bot only. Authenticated with an API key/secret pair instead of a bearer token — a per-council key (channel whatsapp), never a bank's global key. Banks have their own separate §4c Bank API, with its own routes and its own credential shape — not shared with this one.
X-API-KEY: YOUR_API_KEY X-API-SECRET: YOUR_API_SECRET
Bot Login
{
"account_number": "12345",
"password": "Password@123"
}
{
"success": true,
"name": "John Doe",
"account": "12345",
"currency": "USD",
"balance": "123.45"
}
Account Lookup
{
"success": true,
"dc_link": 123,
"account": "12345",
"name": "John Doe",
"currency": "USD",
"balance": "123.45"
}
Balance Lookup — same response shape as Account Lookup.
Statement Lookup — same response shape as Customer Statement.
Service Request
{
"account": "12345",
"category": "water_leak",
"title": "Burst Pipe",
"description": "Pipe burst outside property."
}
04b Accounts API
Lists every client (account) in the calling council — account number, name, phone, address, balance. Usable by any front-door key (Banks, WhatsApp, Customer Portal) for the council it belongs to.
X-API-KEY: YOUR_API_KEY X-API-SECRET: YOUR_API_SECRET
- per_page (optional, default 50, max 200)
{
"success": true,
"council": { "code": "RIVERVIEW_TC", "name": "Riverview Town Council" },
"clients": [
{
"account_number": "56114",
"name": "Shanel Connelly",
"phone": "0728723933",
"address": "8953 Herbert Court",
"currency": "USD",
"balance": "113.88"
}
],
"meta": { "current_page": 1, "per_page": 50, "total": 202, "last_page": 5 }
}
04c Bank API
Its own fully separate channel — own routes, own controllers, not shared with Bot API. Banks have full access to any council through a global key (no single council on the credential itself, unlike every other channel) — every other endpoint on the hub rejects a global key outright. A per-council bank key (issued the normal way) still works on every route below too, exactly like any other channel's key.
X-API-KEY: YOUR_GLOBAL_API_KEY X-API-SECRET: YOUR_GLOBAL_API_SECRET
Bank-wide (global key only)
These three see every council at once — there's no council_code involved, and a per-council key gets 401 here.
{
"success": true,
"councils": [
{ "code": "CENTRAL_MUN", "name": "Central Municipality", "type": "Municipality" },
{ "code": "NORTHDALE_RDC", "name": "Northdale Rural District Council", "type": "RDC" },
{ "code": "RIVERVIEW_TC", "name": "Riverview Town Council", "type": "Town Council" },
{ "code": "EASTFIELD_RDC", "name": "Eastfield Rural District Council", "type": "RDC" }
]
}
/api/bank/clients below.- council_code (optional — omit for every council)
- status (optional: pending, posted, failed)
- date (optional, single day)
- from / to (optional, date range)
- per_page (optional, default 50, max 200)
{
"success": true,
"transactions": [
{
"council_code": "RIVERVIEW_TC",
"council_name": "Riverview Town Council",
"reference": "PAY-20260827-001",
"zb_reference": null,
"account_number": "10001",
"customer_name": "John Doe",
"amount": "5.00",
"type": "payment",
"source": "connector",
"paid_by": "John Doe",
"status": "posted",
"sage_reference": "1249.001",
"failure_reason": null,
"created_at": "2026-08-27T10:05:44+00:00",
"posted_at": "2026-08-27T10:06:01+00:00"
}
],
"meta": { "current_page": 1, "per_page": 50, "total": 4, "last_page": 1 }
}
/api/zb/payment/{reference} one reference at a time.- per_page (optional, default 50, max 200)
{
"success": true,
"clients": [
{
"council_code": "EASTFIELD_RDC",
"council_name": "Eastfield Rural District Council",
"account_number": "39480",
"name": "Jane Moyo",
"phone": "0771234567",
"email": "jane@example.com",
"address": "12 Main Street",
"currency": "USD",
"balance": "45.20"
}
],
"meta": { "current_page": 1, "per_page": 50, "total": 2066, "last_page": 42 }
}
council_code/council_name. Issue a global key via php artisan olimem:issue-global-key.?council_code= (strongly recommended — some councils carry 1,000s of services) + optional ?type=Municipal|Normal. Omitting council_code sweeps every council in one paginated response.
?council_code= (same recommendation as bank/services above).
?council_code= (same recommendation) — property id, owner name, address, erf number, for every council or one.
council_code here rather than relying on the no-filter fallback.Single-account (global or per-council key)
Same shape as Bot API's equivalents (§4), on their own /api/bank/* routes. A global key must add council_code — a per-council bank key resolves it from the key itself and can omit it.
{
"council_code": "EASTFIELD_RDC",
"account_number": "12345",
"password": "Password@123"
}
{
"success": true,
"name": "John Doe",
"account": "12345",
"currency": "USD",
"balance": "123.45"
}
- council_code (required for a global key only)
{
"success": true,
"dc_link": 123,
"account": "12345",
"name": "John Doe",
"currency": "USD",
"balance": "123.45"
}
Balance Lookup — same response shape as Account Lookup.
Statement Lookup — same response shape as Bot API's Statement Lookup.
{
"council_code": "EASTFIELD_RDC",
"account": "12345",
"category": "billing_query",
"title": "Disputed charge",
"description": "Customer disputes the water bill amount."
}
04d Services API
Lists the calling council's service/product catalog, as synced up from Sage by the connector — StkItem for Normal-type sales, _ccg_EB_Services for Municipal-type billing. A council with no municipal billing module configured in Sage simply returns zero Municipal rows — not an error. Use the returned id as property_id+service_id (Municipal) or stock_id (Normal) on §6 Transact API.
X-API-KEY: YOUR_API_KEY X-API-SECRET: YOUR_API_SECRET
- type (optional: Municipal, Normal — omit for both)
- per_page (optional, default 100, max 500)
{
"success": true,
"council": { "code": "RIVERVIEW_TC", "name": "Riverview Town Council" },
"services": [
{ "id": 1, "type": "Municipal", "name": "Water Consumption", "description": "Water Cons" },
{ "id": 42, "type": "Normal", "name": "Stationery Pack", "description": null }
],
"meta": { "current_page": 1, "per_page": 100, "total": 2, "last_page": 1 }
}
04e Tenders API
Lists the calling council's payment methods, as synced up from Sage's own dbo.Tender table by the connector (Cash, EcoCash, Card, bank transfer, etc.). Pass the returned id back as tender_id on §6 Transact API to choose how a payment posts — omit it and Municipal payments still fall back to the service's own tariff, unchanged.
X-API-KEY: YOUR_API_KEY X-API-SECRET: YOUR_API_SECRET
- per_page (optional, default 100, max 500)
{
"success": true,
"council": { "code": "RIVERVIEW_TC", "name": "Riverview Town Council" },
"tenders": [
{ "id": 6, "tender_no": "6", "name": "Cash USD" },
{ "id": 7, "tender_no": "7", "name": "ECOCASH (Merchant)-ZiG" }
],
"meta": { "current_page": 1, "per_page": 100, "total": 2, "last_page": 1 }
}
rcpt_service_tr_codes mapping is configured for that service+tender combination.04f Properties API
Lists the calling council's property register, as synced up from Sage's own Properties by the connector — property id, owner name, address, erf number. Use the returned property_id together with a service_id from §4d Services API for a Municipal-type payment on §6 Transact API.
X-API-KEY: YOUR_API_KEY X-API-SECRET: YOUR_API_SECRET
- per_page (optional, default 100, max 500)
{
"success": true,
"council": { "code": "RIVERVIEW_TC", "name": "Riverview Town Council" },
"properties": [
{ "property_id": 3, "owner_name": "John Doe", "address": "12 Main Street", "erf_number": "ERF-100" }
],
"meta": { "current_page": 1, "per_page": 100, "total": 1, "last_page": 1 }
}
owner_name is the client's current billing name (Sage Client.Name), not the deed-registered name, which can go stale.05 Payment Gateway API
Payment Status
- council_code (required for a global/bank key only — per-council keys resolve it from the key itself)
{
"success": true,
"merchant_reference": "ZB-1234",
"zb_reference": "ZB202607280001",
"status": "paid",
"is_paid": true,
"amount": "50.00",
"email": "customer@example.com",
"phone": "0771234567",
"receipt": {
"id": 1,
"reference": "REC-0001"
}
}
06 Transact API
Allows external systems (payment gateways, third-party billers) to post transactions directly into Sage Evolution. type picks which of Sage's two billing shapes the payment belongs to — look the right ids up first via §4d Services API and §4e Tenders API.
X-API-KEY: YOUR_API_KEY X-API-SECRET: YOUR_API_SECRET Accept: application/json Content-Type: application/json
- account (required)
- type (optional: Municipal, Normal — default Municipal)
- property_id + service_id (required if type=Municipal — see Services API)
- stock_id (required if type=Normal — see Services API)
- tender_id (optional — see Tenders API)
- amount (required)
- currency (optional: ZWG, USD, ZAR — the currency the payer paid in; omit to use the account's own currency)
- reference (required, unique)
- description, paid_by (optional)
{
"account": "12345",
"type": "Municipal",
"property_id": 1001,
"service_id": 1,
"tender_id": 6,
"amount": 100.00,
"reference": "PAY-20260728-001",
"description": "Online Payment",
"paid_by": "John Doe"
}
{
"account": "12345",
"type": "Normal",
"stock_id": 42,
"tender_id": 7,
"amount": 25.00,
"reference": "PAY-20260728-002"
}
{
"council_code": "EASTFIELD_RDC",
"account": "12345",
"amount": 100.00,
"reference": "PAY-20260728-001",
"description": "Online Payment"
}
{
"success": true,
"receipt_id": 123,
"reference": "REC-0001",
"message": "Transaction posted successfully."
}
reference must be unique per transaction — retrying with the same reference must not post the payment twice. There is no currency field — a payment always posts in the account's own synced currency.tender_id is optional and Municipal payments still work without it (falls back to the service's own tariff, unchanged from before). Normal payments have no fallback — without a resolvable tender/service TR-code mapping configured on the council's side, the payment is accepted but fails to post into Sage.07 Sync API
Used by the Android receipting application to pull reference data and push offline receipts once connectivity returns.
GET /api/sync/stk-items GET /api/sync/clients GET /api/sync/accounts GET /api/sync/tenders GET /api/sync/currencies GET /api/sync/currency-hist GET /api/sync/price-list-names GET /api/sync/price-list-prices GET /api/sync/tr-codes GET /api/sync/properties GET /api/sync/services GET /api/sync/property-services GET /api/sync/tender-configurations GET /api/sync/service-trcodes
Offline Receipt Synchronisation
{
"clientLink": 1001,
"customerCurrencyId": 1,
"type": "Municipal",
"userId": 1,
"openBalance": 500.00,
"paid_by": "John Doe",
"posted": true,
"createdAt": "2026-07-28 10:00:00",
"reference": "ANDROID-0001",
"source": "Android",
"items": [
{
"is_cash": true,
"createdAt": "2026-07-28 10:00:00",
"type": "Municipal",
"clientLink": 1001,
"trcode": 1,
"amount": 100.00,
"tendered_amount": 100.00,
"change": 0,
"taxAcc": 1,
"taxTypeID": 1,
"description": "Water Charges",
"stockId": 1,
"iot": 0,
"refundTrcode": 0,
"isBillable": true,
"currencyID": 1,
"userId": 1
}
]
}
reference must be unique per receipt so a retried sync never double-posts.Mobile Device Login
{
"email": "user@council.co.zw",
"password": "Password@123",
"deviceUUID": "123456789",
"deviceName": "Samsung Tablet",
"deviceModel": "SM-T500"
}
08 Miscellaneous Endpoints
Current authenticated user (Sanctum session).
{
"status": "working",
"app": "Olimem Customer Portal"
}
09 Error Handling
{
"success": false,
"message": "Error Description"
}
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 422 | Validation Error |
| 500 | Internal Server Error |
10 Security Recommendations
- Enforce HTTPS for all environments.
- Rotate API keys periodically.
- Store API secrets securely.
- Implement IP whitelisting.
- Enable request auditing.
- Use JWT / Sanctum token expiration.
- Encrypt sensitive data.
- Rate limiting — done: 120 req/min per API key on every machine-client route, 60 req/min per customer on Customer API routes (keyed by identity, not IP, so councils sharing this server never throttle each other).
- Enable API monitoring.
11 Integration Flow
12 Appendix
- USD
- ZWG
- Bearer Token
- API Key
- API Secret
application/json