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

  1. Your admin creates your account in Django admin with an email + temporary password.
  2. You sign in at /login. You'll be forced to pick a new password.
  3. You enrol an SMS 2FA number (Tanzania E.164 format e.g. +255787123456).
  4. 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>
  1. Sign in to the dashboard.
  2. Open API Keys in the sidebar.
  3. Click Create key. Pick a name, env, and scopes.
  4. Copy the full sk_live_… from the green banner. It is shown only once.
  5. 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
Use 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.json

Use 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

  1. Reject if |now − t| > 300 seconds.
  2. Compute HMAC-SHA256(whsec_, "<t>." + raw_body).
  3. Compare against v1 with hmac.compare_digest.
  4. Return HTTP 200 within 10 seconds.

A reference Python verifier ships in the SDK as payments.verify.WebhookVerifier.

Idempotency on your side too. We retry up to 5 times with exponential backoff (1, 2, 4, 8, 16 min). Your handler must be safe to call twice with the same 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 batches
  • GET /v1/settlements/{id}/lines/ — drill into ledger entries
  • POST /v1/settlements/{id}/retry-submission/ — re-attempt failed batch
  • GET /v1/reserves/balance/?entity_id=ST-MARYS — see held reserves
  • POST /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

  1. Create the entity in the dashboard with KYC status pending.
  2. Email scanned PDFs of every document above to kyc@your-domain.example, referencing the app_id and entity_id in the subject line.
  3. Our compliance team reviews within 3–5 business days.
  4. Approved entities flip to kyc_status: "approved". Live sk_live_ calls against that entity begin working.
  5. Re-submission required if any of the underlying facts change (new bank account, new beneficial owner, etc.).
Until KYC is approved, live 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..." }
StatusMeaning
400Bad request — body invalid or business rule violated
401Missing / invalid / revoked API key or JWT
403Authenticated but lacking scope or accessing another tenant's resource
404Resource not found (or not owned by caller)
409Idempotency-Key reused with different body, or unique-key clash
429Per-key rate limit exceeded
500Persisting 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.