ThumaPay is preparing for launch. Licence applications are in progress with the Reserve Bank of Zimbabwe and IPEC. We hold no customer funds and offer no regulated service until authorised. Read our regulatory status.
Developers

A payments API you can hold in your head.

REST over HTTPS, JSON in and out, idempotency on every write, and webhooks that retry until you acknowledge them. Amounts are integers in cents, because floating point has no business near money.

Sandbox from day one Idempotent writes Signed webhooks
# Charge a customer wallet POST /v1/charges Authorization: Bearer sk_live_... Idempotency-Key: 8f14e45f-ea { "amount": 4500, "currency": "USD", "source": "wallet:+263771234567", "reference": "INV-2048" } # 201 Created { "id": "chg_3Nk2...", "status": "succeeded", "amount": 4500, "fee": 54, "tax": 90, "net": 4356 }
Principles

Design decisions you will run into

Money is integers

All amounts are minor units — cents — as integers. No decimals cross the wire, so nothing rounds differently on your side than ours.

Writes are idempotent

Send an Idempotency-Key on every POST. Retry as often as you like; the same key returns the original result instead of charging twice.

Fees are explicit

Every charge response carries amount, fee, tax and net as separate fields. You never have to derive one from the others.

Webhooks retry

Events retry with backoff for 24 hours until you return 2xx. Each carries a signature header you should verify before trusting the body.

Statuses are terminal

A charge ends in succeeded, failed or reversed. Nothing silently changes state after it settles.

Keys are scoped

Publishable keys can only create payment intents. Secret keys never belong in client code or a repository.

Surface area

The endpoints you will actually use

EndpointMethodWhat it does
/v1/chargesPOSTCollect from a wallet, mobile money account or card.
/v1/charges/:idGETRetrieve a charge with its fee, tax and net breakdown.
/v1/refundsPOSTRefund all or part of a charge; fees are returned pro rata.
/v1/payoutsPOSTSend funds to a wallet, mobile money number or bank account.
/v1/payouts/batchesPOSTSubmit up to 5,000 payouts in one call; each line succeeds or fails independently.
/v1/balancesGETAvailable and pending balance, per currency.
/v1/settlementsGETSettlements with the transaction list behind each one.
/v1/eventsGETReplay webhook events you may have missed.
Webhooks

Verify before you trust

Every event carries a ThumaPay-Signature header. Compute an HMAC of the raw body with your endpoint secret and compare in constant time. Reject anything that does not match, and reject timestamps older than five minutes to stop replay.

// Node — verify a webhook const sig = req.headers['thumapay-signature']; const expected = crypto .createHmac('sha256', endpointSecret) .update(req.rawBody) .digest('hex'); if (!crypto.timingSafeEqual( Buffer.from(sig), Buffer.from(expected))) { return res.status(400).end(); }
Questions

Integration notes

Yes. Sandbox keys are issued at the same time as live keys and behave identically, including webhook delivery and failure simulation. No money moves.
Sandbox exposes trigger values that force specific outcomes — insufficient funds, expired verification, recipient not found — so you can build error handling without waiting for a real failure.
Limits are per key and returned on every response in the rate-limit headers. Batch payouts count as one request regardless of line count, which is the point of using them.
Balances are held per currency and never implicitly converted. If a payout currency differs from the balance currency, the call fails rather than silently applying a rate.
Keys are issued once your business account is verified. Start that on the Business page; sandbox access can be granted while verification is still in progress.

Build against it today

Sandbox access does not wait for full verification. Get keys, wire up a charge, and see the fee breakdown in your own response body.