Platform documentation
Everything you need to integrate your application with Finova Payments and start collecting TIPs payments.
Overview
Finova Payments sits in front of TIPs and routes payment webhooks to your app. Each invoice you issue gets a unique Lipa Number bound to a settlement entity. When the customer pays, the router persists the raw payload, dedupes by transid, signs a clean payload with your whsec_, and POSTs it to your callback URL. Funds accumulate per entity and are paid out on your schedule.
Account & first login
- Your admin creates your account in Django admin with an email + temporary password.
- You sign in at
/login. You'll be forced to pick a new password. - You enrol an SMS 2FA number (Tanzania E.164 format e.g.
+255787123456). - Every subsequent login requires email + password + a fresh 6-digit SMS code.
Cannot get the temp password? Ask your administrator — passwords are never sent through the dashboard.
API keys (server-to-server)
Your dashboard session uses JWT cookies. Your backend talks to us with a Stripe-style API key:
sk_live_<keyid>.<secret> sk_test_<keyid>.<secret>- Sign in to the dashboard.
- Open API Keys in the sidebar.
- Click Create key. Pick a name, env, and scopes.
- Copy the full
sk_live_…from the green banner. It is shown only once. - Store it in your backend's secrets manager. Set it on every request as
Authorization: Bearer sk_live_…
Available scopes
- apps:read
- apps:write
- entities:read
- entities:write
- till_alias:read
- till_alias:create
- webhooks:read
- webhooks:write
- ledger:read
- settlements:read
- settlements:write
- api_keys:read
- api_keys:write
sk_test_… while building. Live disbursement is gated on KYC — see below. OpenAPI specification
The platform publishes a machine-readable OpenAPI document that follows OpenAPI Specification Version 3.1.1.
GET /openapi.jsonUse this document with Swagger UI, Swagger Editor, SDK generators, or contract tests. The schema covers the authenticated /v1/ API, public health check, inbound payment webhook, and settlement-machine callback.
Register your app
An app is your downstream application. It has one callback URL and one signing secret. Multiple entities settle under it.
POST /v1/apps/ Authorization: Bearer sk_live_… Content-Type: application/json { "app_id": "SCHOOLFEES", "name": "School Fee System", "callback_url": "https://yourapp.example.com/webhooks/router" }The response contains whsec_…. Copy it once and store it in your backend secrets. It signs every inbound webhook from us.
To rotate: POST /v1/apps/<app_id>/rotate-webhook-secret/. Both old and new secrets are valid for a 24-hour overlap window.
Settlement entities
An entity is who gets paid — a school, a vendor, a branch. Each entity has its own settlement details (bank or mobile money), KYC status, payout schedule, and optional fee/reserve overrides.
POST /v1/apps/SCHOOLFEES/entities/ { "entity_id": "ST-MARYS", "name": "St Marys Primary", "settlement_method": "mobile_money", "msisdn": "+255712111222", "mobile_provider": "MPESA", "payout_schedule": "daily", "payout_min_amount": "10000", "payout_currency": "TZS" }Bank-settled entities pass "settlement_method": "bank" with bank_name, bank_account_number, bank_account_name, and optionally bank_branch / bank_swift.
Issue a Lipa Number per invoice
The platform fee is added on top of merchant_amount — your customer pays the fee. The entity receives the full merchant amount at settlement.
POST /v1/till-aliases/ Authorization: Bearer sk_live_… Idempotency-Key: alias-INV-2026-00481 { "app_id": "SCHOOLFEES", "entity_id": "ST-MARYS", "name": "F. Ngoiya — Term 1", "memo": "INV-2026-00481", "merchant_amount": "500000", "currency": "TZS" }Response gives you the Lipa Number and expected_total. Display the Lipa Number and the total to the customer:
{ "till_alias": { "alias": "63675386", "app_id": "SCHOOLFEES", "entity_id": "ST-MARYS", "name": "F. Ngoiya — Term 1", "memo": "INV-2026-00481", "expected_total": "502500.00", "currency": "TZS", "is_active": true, "created_at": "..." } }Always send an Idempotency-Key. If your HTTP client retries on timeout we return the original response instead of allocating a second Lipa Number.
Receive payment webhooks
When the customer pays, we POST to your callback_url with:
POST {your callback_url} Content-Type: application/json X-Webhook-Id: 4821 X-Webhook-Timestamp: 1715000000 X-Webhook-Signature: t=1715000000,v1=<HMAC-SHA256> { "webhook_id": 4821, "transid": "504-APCTZ…", "amount": "502500", "currency": "TZS", "phone": "255787…", "app_id": "SCHOOLFEES", "memo": "INV-2026-00481", "utilityref": "63675386", "result": "SUCCESS", "received_at": "…" }Verify the signature
- Reject if
|now − t| > 300seconds. - Compute
HMAC-SHA256(whsec_, "<t>." + raw_body). - Compare against
v1withhmac.compare_digest. - Return HTTP 200 within 10 seconds.
A reference Python verifier ships in the SDK as payments.verify.WebhookVerifier.
transid. Settlement & payouts
Each entity settles on its own schedule (daily, weekly, monthly, or manual). The router computes a batch, optionally holds a reserve, and submits a signed payload to our settlement machine for actual disbursement.
GET /v1/settlements/?entity_id=ST-MARYS— list batchesGET /v1/settlements/{id}/lines/— drill into ledger entriesPOST /v1/settlements/{id}/retry-submission/— re-attempt failed batchGET /v1/reserves/balance/?entity_id=ST-MARYS— see held reservesPOST /v1/reserves/chargeback/— record a refund debit
KYC required before go-live
You can build and test against sk_test_ keys without KYC. Live disbursement is blocked until each settlement entity passes KYC. Submit the documents below for each entity you intend to receive payouts to.
For the operating company (account holder)
- Certificate of incorporation / business licence
- TIN (Tax Identification Number) certificate
- VAT certificate if VAT-registered
- Memorandum & articles of association
- Board resolution authorising the use of Finova Payments and naming the signatories
- Beneficial owners (≥10%): full names, IDs, residential addresses
- Directors' national IDs or passports
- Recent utility bill or bank statement showing the registered address
Per settlement entity
- Legal name, trading name, physical address
- Authorised contact: full name, role, email, mobile
- For bank settlement: bank name, branch, SWIFT, account number, account name — plus a recent bank statement (≤3 months) confirming the account
- For mobile money settlement: registered MSISDN + a screenshot of the Lipa Number or merchant account showing the account name
- Sample invoice or receipt template you will issue to your end customer
Compliance attestations
- AML / CTF declaration (signed)
- Sanctions screening consent
- Data processing agreement (we sign one with you)
- Maximum expected monthly volume + average transaction size
- List of jurisdictions you operate in
How to submit
- Create the entity in the dashboard with KYC status
pending. - Email scanned PDFs of every document above to
kyc@your-domain.example, referencing theapp_idandentity_idin the subject line. - Our compliance team reviews within 3–5 business days.
- Approved entities flip to
kyc_status: "approved". Livesk_live_calls against that entity begin working. - Re-submission required if any of the underlying facts change (new bank account, new beneficial owner, etc.).
POST /v1/till-aliases/ targeting that entity returns 403 entity_not_approved. Test keys ignore this gate so you can finish integration in parallel. Error format
All endpoints return JSON. Errors look like:
{ "error": "human-readable message", "detail": "...optional..." }| Status | Meaning |
|---|---|
| 400 | Bad request — body invalid or business rule violated |
| 401 | Missing / invalid / revoked API key or JWT |
| 403 | Authenticated but lacking scope or accessing another tenant's resource |
| 404 | Resource not found (or not owned by caller) |
| 409 | Idempotency-Key reused with different body, or unique-key clash |
| 429 | Per-key rate limit exceeded |
| 500 | Persisting raw webhook failed — TIPs retries on their side |
Support
Stuck or unsure? Email support@your-domain.example with your app_id and the request_id we return in the X-Request-Id header. We can trace any request in our audit log.
