HTTP API reference
API origin: https://api-towers.camelcreatives.com. JSON routes are under /api/v1, except health probes and provider webhooks. The API currently does not implement pagination or list filters; admin collections return at most 200 rows.
Response conventions
Successful JSON responses generally use {"data": ...}. Error responses generally use {"message": "..."}. Some authentication middleware responses use the same JSON text with an HTTP error response. A 204 response has no body.
IDs in paths are UUIDs unless stated otherwise. The API rejects unknown JSON fields on purchase and login payloads. Request bodies are size-limited.
Health routes
| Method | Path | Purpose |
|---|---|---|
| GET | /healthz | Process responds; returns service name |
| GET | /readyz | Database connectivity readiness probe |
| GET | /api/v1/health | Lightweight API health response |
/readyz checks PostgreSQL, not Abliner, SendAfrica, RADIUS reachability, or router status.
Public routes
| Method | Path | Authentication | Purpose |
|---|---|---|---|
| GET | /api/v1/public/plans | None | Active plan list |
| POST | /api/v1/public/purchases | None + idempotency key | Create/retry guest purchase and Abliner prompt |
| GET | /api/v1/public/purchases/{invoiceID} | None for status; purchase key for credentials | Poll invoice state |
| POST | /api/v1/public/purchases/{invoiceID}/uam-login | Purchase key in body | Produce the AP-local UAM login URL |
| POST | /webhooks/abliner | Signed provider headers | Accept and de-duplicate payment event |
List plans
GET /api/v1/public/plans
Returns active plans ordered by sort_order, then price and name.
{
"data": [{
"id": "7cc35a92-e60a-4d06-949a-e36140524de5",
"name": "1 hour",
"description": "",
"price_tzs": 500,
"duration_seconds": 3600
}]
}An optional data_limit_bytes field may be returned if stored. The current access server does not enforce it.
Start a purchase
POST /api/v1/public/purchases
Headers: Content-Type: application/json, Idempotency-Key: <16–96 alphanumeric, underscore, or hyphen characters>.
Body:
{ "plan_id": "7cc35a92-e60a-4d06-949a-e36140524de5", "phone": "+255712345678" }Typical response: HTTP 201 for a new invoice, or HTTP 200 when reusing a key.
{
"data": {
"invoice_id": "7cc35a92-e60a-4d06-949a-e36140524de5",
"invoice_number": "TWR-261006-ABCDEF12",
"status": "pending",
"amount_tzs": 500
}
}The example IDs are illustrative only. The live endpoint returns generated IDs and invoice number. Readiness failures return 503; invalid phone/plan/amount returns 422; an idempotency key reused for different details returns 409; provider rejection/network failure returns 502.
Poll purchase status
GET /api/v1/public/purchases/{invoiceID}
Send X-Purchase-Key to receive the access username and password after payment. Without the matching key the endpoint only reveals basic invoice status and amount. Credentials disappear from this response after expiry or revocation.
Create UAM login URL
POST /api/v1/public/purchases/{invoiceID}/uam-login
{
"purchase_key": "client-generated-key-with-at-least-16-characters",
"uamip": "10.1.30.1",
"uamport": 3990,
"challenge": "00112233445566778899aabbccddeeff",
"userurl": "http://example.com/"
}The API requires a paid, valid, non-revoked grant matching the purchase key. The UAM gateway must be IPv4 and inside CUDY_LOCAL_NETWORK; the challenge must be 16 bytes represented as hex; the optional original URL must be HTTP(S). A successful response is {"redirect_url":"http://10.1.30.1:3990/logon?..."}.
Admin routes
All routes below require the towers_session cookie created by staff login. Session cookies are HttpOnly, SameSite=Lax, secure when ADMIN_ORIGIN uses HTTPS, and expire after 12 hours.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/admin/auth/login | Create staff session |
| GET | /api/v1/admin/auth/me | Current staff identity |
| POST | /api/v1/admin/auth/logout | Revoke session |
| GET | /api/v1/admin/overview | Device, active-session, pending-invoice counts |
| GET | /api/v1/admin/portal/config | Non-secret Cudy readiness and endpoint values |
| GET / POST | /api/v1/admin/plans | List / create access plan |
| PATCH | /api/v1/admin/plans/{id} | Replace plan properties and active state |
| GET / POST | /api/v1/admin/devices | List / register router |
| PATCH | /api/v1/admin/devices/{id} | Update router settings |
| GET | /api/v1/admin/sessions | List recent RADIUS sessions |
| POST | /api/v1/admin/sessions/{id}/disconnect | Send CoA disconnect and revoke grant |
| GET | /api/v1/admin/guests | List guests |
| GET | /api/v1/admin/invoices | List invoices |
| GET | /api/v1/admin/payments | List payments |
| GET | /api/v1/admin/messages | List SMS message records |
| POST | /api/v1/admin/messages | Send SMS through SendAfrica |
Login and logout
POST /api/v1/admin/auth/login
{ "email": "operator@example.com", "password": "provided-out-of-band" }On success, the API sets a cookie and returns the staff ID, email, display name, and role. Invalid credentials return 401. GET auth/me returns the same current identity. Logout deletes the server session and clears the cookie.
Plan request
{
"name": "Evening pass",
"description": "Wi-Fi access for 3 hours",
"price_tzs": 1500,
"duration_seconds": 10800,
"data_limit_bytes": null,
"active": true
}Price must be at least TZS 500, duration must be positive, and any data-limit value must be positive. A plan PATCH replaces the supplied plan values; omitted active defaults to true.
Device request
{
"name": "Outdoor AP",
"model": "AP1300 Outdoor",
"mac_address": "AA:BB:CC:DD:EE:FF",
"local_network": "10.1.30.0/24",
"uam_server": "https://wateja-towers.camelcreatives.com/uam",
"radius_nas_id": "camel-towers-ap01",
"radius_client_ip": "198.51.100.20",
"coa_port": 3799
}radius_nas_id must exactly match the NAS-Identifier sent in RADIUS requests. radius_client_ip is the address the API sends CoA to; it can differ from the AP’s RADIUS source IP. The API does not push configuration to the AP.
Send SMS
{
"recipient": "+255712345678",
"message": "Your Wi-Fi pass has been activated.",
"sender_id": "CAMEL"
}Message length is limited to 1,600 bytes by the current Go implementation. HTTP 202 means SendAfrica accepted the request; it does not mean the handset received it.
Abliner webhook
The callback is POST /webhooks/abliner on the API host. It reads x-webhook-timestamp, x-webhook-signature, optional x-webhook-id, and the provider event body. Invalid signatures return 401, invalid payloads 400, invoice mismatches 422, and transient processing errors 5xx.
CORS
The API allow-list is the exact ADMIN_ORIGIN and PORTAL_ORIGIN values. It allows credentials and standard JSON methods. The public documentation hostname does not call authenticated API endpoints.