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

MethodPathPurpose
GET/healthzProcess responds; returns service name
GET/readyzDatabase connectivity readiness probe
GET/api/v1/healthLightweight API health response

/readyz checks PostgreSQL, not Abliner, SendAfrica, RADIUS reachability, or router status.

Public routes

MethodPathAuthenticationPurpose
GET/api/v1/public/plansNoneActive plan list
POST/api/v1/public/purchasesNone + idempotency keyCreate/retry guest purchase and Abliner prompt
GET/api/v1/public/purchases/{invoiceID}None for status; purchase key for credentialsPoll invoice state
POST/api/v1/public/purchases/{invoiceID}/uam-loginPurchase key in bodyProduce the AP-local UAM login URL
POST/webhooks/ablinerSigned provider headersAccept 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.

MethodPathPurpose
POST/api/v1/admin/auth/loginCreate staff session
GET/api/v1/admin/auth/meCurrent staff identity
POST/api/v1/admin/auth/logoutRevoke session
GET/api/v1/admin/overviewDevice, active-session, pending-invoice counts
GET/api/v1/admin/portal/configNon-secret Cudy readiness and endpoint values
GET / POST/api/v1/admin/plansList / create access plan
PATCH/api/v1/admin/plans/{id}Replace plan properties and active state
GET / POST/api/v1/admin/devicesList / register router
PATCH/api/v1/admin/devices/{id}Update router settings
GET/api/v1/admin/sessionsList recent RADIUS sessions
POST/api/v1/admin/sessions/{id}/disconnectSend CoA disconnect and revoke grant
GET/api/v1/admin/guestsList guests
GET/api/v1/admin/invoicesList invoices
GET/api/v1/admin/paymentsList payments
GET/api/v1/admin/messagesList SMS message records
POST/api/v1/admin/messagesSend 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.