Payment and access lifecycle
1. Plan and phone validation
The public API returns active plans ordered by configured sort order and price. Each plan contains an ID, display name, description, TZS price, duration in seconds, and an optional data limit. The guest app shows name, duration, and price.
Before creating a payment, the API requires:
- Wi-Fi access configuration to be ready: RADIUS host and secrets are set, client CIDRs parse, and a router with a NAS ID and local network is registered.
- Abliner API key and webhook secret to be configured.
- A Tanzania phone number that normalizes to a 12-digit
255...value. - An active plan with a price of at least TZS 500 and positive duration.
The API validates the phone format and length, not whether the number is allocated or able to receive a prompt.
2. Idempotent purchase request
The guest client creates a random request key and sends it in Idempotency-Key. The server stores it as unique on the payment record. If the client retries with the same key and same plan, number, and amount, the server reuses the existing invoice and triggers the provider call with the same idempotency key. Reusing a key for different details returns HTTP 409.
The API inserts the guest, invoice, and pending payment in one database transaction, then calls Abliner. If the provider call fails, the invoice remains stored and pending so support can investigate; the client receives a gateway error and is told to check the phone before retrying.
3. Abliner mobile-money request
The Go adapter posts to {ABLINER_API_BASE_URL}/api/v1/deposits with a JSON body containing amount, currency: "TZS", method: "mobile", normalized phone, invoice number as reference, and API webhook callback URL.
The request includes:
Authorization: Bearer <ABLINER_API_KEY>Idempotency-Keyx-abliner-timestamp: Unix secondsx-abliner-signature: HMAC-SHA256 hex oftimestamp + "." + exact JSON body
The provider response must be HTTP 2xx and have top-level status: "success". The adapter stores the provider transaction ID, reference, and response payload. The synchronous response does not mark an invoice paid.
4. Signed callback and invoice transition
Abliner must call POST https://api-towers.camelcreatives.com/webhooks/abliner. The API validates x-webhook-timestamp and x-webhook-signature against the exact raw request body with HMAC-SHA256. Timestamps more than five minutes from server time fail validation. The optional x-webhook-id header replaces the body event ID for de-duplication.
The handler stores each event ID once. It handles transaction.completed as a completed payment and transaction.failed as failed. Other event types are recorded and acknowledged without changing invoice state. For handled events, the invoice reference must match the invoice number, and amount and currency must match the original invoice.
For a completed transaction, the API updates the payment and invoice and inserts an access grant in the same transaction. A repeated event ID returns 204 without creating a second grant. If grant creation fails, the database transaction rolls back and a non-2xx response asks the provider to retry.
5. Access grant
The API creates a random username and password using cryptographic randomness. It stores:
- Username (unique)
- Bcrypt password hash for RADIUS comparison
- AES-GCM encrypted password so the paid guest can receive it for UAM login
- Grant validity from the payment callback time through the selected plan duration
- Invoice and guest linkage
ACCESS_CREDENTIAL_KEY is SHA-256-derived into the AES-GCM key. Keep it stable and backed up. Rotating it without a migration makes existing stored passwords unreadable.
6. Poll and connect
The browser polls GET /api/v1/public/purchases/{invoiceID} every four seconds, for up to 45 attempts, with X-Purchase-Key. The endpoint returns payment state and only returns credentials if the invoice is paid, the grant is active and unexpired, and the key matches the payment’s original idempotency key.
When the guest arrived through UAM, the app posts the purchase key and router challenge values to POST /api/v1/public/purchases/{invoiceID}/uam-login. The API validates the gateway IP against CUDY_LOCAL_NETWORK, creates the CoovaChilli CHAP response, and returns a local AP /logon URL. The AP then authenticates the grant against RADIUS.
Invoice/payment state summary
| Event | Invoice | Payment | Grant |
|---|---|---|---|
| Purchase created | pending | pending | None |
transaction.completed | paid | completed | Created once |
transaction.failed | failed | failed | None |
| Unrecognized event | Unchanged | Unchanged | Unchanged |
| Grant expired by time | Still paid | Still completed | Rejected by RADIUS after valid_until |
| Admin disconnect accepted | Still paid | Still completed | Revoked |
There is no refund workflow, chargeback workflow, reconciliation poller, or automatic payment-status query in the current API.