Payment and access lifecycle

Paid Wi-Fi purchase sequenceThe server creates an invoice and payment, confirms payment from a signed Abliner webhook, creates a time-bound grant, and authenticates it through RADIUS after UAM login.Guest browserselects plansubmits phoneGo APIinvoice + paymentidempotency keyAblinerUSSD payment promptPostgreSQLpending invoiceCudy / RADIUSaccess only after paidPOST purchasesigned deposittransaction.completedpaid invoice + encrypted grantpoll with X-Purchase-KeyUAM CHAP → RADIUS Access-Accept + Session-Timeout

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:

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:

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:

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

EventInvoicePaymentGrant
Purchase createdpendingpendingNone
transaction.completedpaidcompletedCreated once
transaction.failedfailedfailedNone
Unrecognized eventUnchangedUnchangedUnchanged
Grant expired by timeStill paidStill completedRejected by RADIUS after valid_until
Admin disconnect acceptedStill paidStill completedRevoked

There is no refund workflow, chargeback workflow, reconciliation poller, or automatic payment-status query in the current API.