Skip to content

Ten minutes to a test charge

PamoPay takes money from anyone in Tanzania with mobile money on your behalf — the payer does not need a PamoPay account — and holds it in your business balance. You talk to one REST API with a secret key; PamoPay talks to your server with signed webhooks. Everything on this site is what the API does today, rendered from its own source. Where something is not built yet, it says so.

1. Get a test key

Sign up at the dashboard. That creates a draft business, your owner seat, and a test key (sk_test_…) shown once on the Developers screen. You can integrate fully against the sandbox with nothing else — no certificate, no bank account, no phone call.

2. Make a charge

bash
export BASE=https://europe-west1-pamopay-fintech.cloudfunctions.net/merchantSandboxApi
export KEY=sk_test_…

curl -sS "$BASE/v1/charges" \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: try-$(date +%s)" \
  -H "Content-Type: application/json" \
  -d '{ "amount_minor": 1500000, "rail": "mpesa", "msisdn": "+255700000001" }'
json
{
  "id": "ch_01HZX4Q7V9M2K3N5P6R8S9T0VW",
  "object": "charge",
  "livemode": false,
  "state": "processing",
  "amount_minor": 1500000,
  "fee_minor": 30000,
  "net_minor": 1470000,
  "currency": "TZS",
  "rail": "mpesa",
  "created": "2026-09-17T09:14:02.118Z",
  "expires_at": "2026-09-17T09:29:02.118Z",
  "simulation": { "payer": "+255700000001", "note": "approves after a few seconds" }
}

Three things to notice. amount_minor is minor units: TZS 15,000 is 1500000. processing means not yet known — never a failure. And +255700000001 is a trigger number: in the sandbox the payer is a table, and that one approves.

3. Read it back

bash
curl -sS "$BASE/v1/charges/ch_01HZX4Q7V9M2K3N5P6R8S9T0VW" -H "Authorization: Bearer $KEY"

A few seconds later state is succeeded. Now send +255700000002 (declines), +255700000004 (never resolves) and +255700000005 (succeeds after ninety seconds). Those last two are the branches you cannot arrange against a real network and the ones most integrations ship wrong.

4. Hear about it

Register a webhook endpoint for test mode on the Developers screen, and PamoPay will POST you charge.succeeded, signed. Verifying it is ten lines in any language. Until you have an endpoint, GET /v1/events holds everything you missed.

5. Go live

When the business is live — a person at PamoPay reads your BRELA certificate and records where to settle — make a live key with the scopes this server needs, change the base URL, and change nothing else. The request and response shapes are identical; the sandbox refuses what production refuses. See Going live.

How it fits together

There are four ways to ask for money, and they produce the same object, a charge:

You want to…UseThe payer…
Charge a phone number you already havePOST /v1/chargesgets a USSD prompt and types their PIN
Show a code at a physical counterPOST /v1/pos/chargesscans a QR with the PamoPay app or their camera
Send someone a linkPOST /v1/payment_linksopens a hosted page and pays there
Issue a bill that may be paid later, partly, by anyonePOST /v1/billstypes a 12-digit control number, or opens the bill's page

A charge has five states:

requires_payment ──► processing ──► succeeded

                          ├──────► failed
                          └──────► expired

Do not ship goods on processing, and do not cancel an order on it either — wait for succeeded, failed or expired, ideally by webhook.

Money is settled net of fees. A payer authorises amount_minor; your balance moves by net_minor; fee_minor is PamoPay's. All three are on every charge, always.

Base URLs

ModeBase URL
Livehttps://europe-west1-pamopay-fintech.cloudfunctions.net/merchantApi
Sandboxhttps://europe-west1-pamopay-fintech.cloudfunctions.net/merchantSandboxApi

A sk_test_ key sent to the live URL is refused with USE_SANDBOX_URL; a sk_live_ key sent to the sandbox is refused with LIVE_KEY_ON_SANDBOX. The two are separate deployments, and the sandbox one physically cannot reach the ledger — test money can never become real money by mistake.

Every code, scope and route on this site is rendered from the API's own source.