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 with your business's name and TIN. That creates a draft business and your owner seat — nothing is verified yet, and nothing needs to be. Choose Developer as your sector and the dashboard opens as a developer portal: an Overview of your traffic, then API keys, Webhooks, Logs and a Sandbox. On API keys, make a test key (sk_test_…) — it is shown once. 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 Webhooks page, 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.