Skip to main content
There is one deployed AptaPay host, because the upstream provider has no sandbox. Test mode is therefore a separate credential on the same host, not a separate environment.

Mode comes from the key

Your mode is resolved from the API key you present. It is never a field you send, and never a header you set.
Test mode is not a sandbox. It is a real provider account with a low-balance wallet. Test-mode transactions move real money in small amounts. Do not load-test against it, and do not treat a failure there as a simulation.

A disagreeing body mode is rejected

If you send a mode field in a request body and it disagrees with the mode your key resolves to, the request is refused with 400 mode_mismatch. It is not silently honoured in either direction.
400 mode_mismatch
A caller that thinks it is in test mode while holding a live key has a bug worth surfacing loudly — silently honouring either side is how a real debit gets stamped as test. The simplest fix is to stop sending mode at all and let the key decide.

The test-mode ceiling

Because there is no sandbox, a hard per-transaction ceiling is the permanent substitute for the safety net a sandbox would provide. It is set by TEST_MODE_MAX_AMOUNT_MINOR and enforced in the gateway before any provider call, so an over-ceiling request is refused with nothing sent. The ceiling is expressed in the same minor-unit convention as every other amount, so remember that UGX, RWF, XAF and XOF are zero-decimal. See Money and minor units.
Design your test cases around small amounts. If you need to exercise a large-amount code path, assert on the refusal rather than raising the ceiling.

The Postman collection

The collection is generated, and it signs every request for you.
Generate the collection and environments
That writes the collection plus a local and a production environment. Import the collection and the environment you want, then set four environment values: app_id, key_id, api_key and signing_secret. A collection-level pre-request script rebuilds the canonical string, HMAC-SHA256s it, and sets the four X-AptaPay-* headers, so requests work as imported rather than returning 401. It also fills Idempotency-Key on money-moving POSTs, but only when you have not set one yourself — so you can pin a value to test a deliberate replay.
Keep signing_secret as a Postman secret variable in an environment, never in the collection. A collection is a file people share.

Debug a signing failure

Set the debug_signing environment variable to true. The pre-request script then prints the canonical string to the Postman console. This is the fastest way to find a signing mismatch, because the server can only ever answer 401 — it cannot tell you which of the ten canonical fields you got wrong without also telling an attacker.
What debug_signing prints
Compare that line by line against what your own client builds. See Authentication for the full scheme.

Things that will bite you

  • A skewed clock reads as a bad secret. The timestamp window is asymmetric, so a machine a few minutes fast gets 401s that look like a credential problem.
  • A nonce cannot be reused inside the window. The script mints a fresh one per send, so duplicating a tab is fine — replaying a captured raw request is not.
  • Amounts are minor units with a per-currency exponent. 2000 UGX is two thousand shillings; 2000 KES is twenty.

The API playground

The playground in this documentation site is configured for simple display: it shows the request shape and copyable examples, but it does not send live requests.
The playground cannot compute HMAC signatures. Every AptaPay request must be signed with your signing secret, which never leaves your server, so a browser-side playground could only ever produce a 401. Use the Postman collection or your own signed client to make real calls.

A testing checklist

1

Prove your signing first

Call GET /health with a signed request before anything else. It moves no money and needs no body, so a 200 there means your canonical string, secret and clock are all correct.
2

Read the corridor matrix

Call GET /v1/reference/corridors and assert your target route exists. A route that does not exist is a 422, and finding that out in a test is cheaper than in a checkout.
3

Exercise the pending path

Assert that your code treats 201 with status: "pending" as “not yet paid”. The most common integration bug is granting value on the 201. See Webhooks.
4

Replay an idempotency key deliberately

Pin an Idempotency-Key and send the same request twice. You should get the original response verbatim and exactly one transaction. See Idempotency.
5

Force the refusals you plan to handle

Send an over-ceiling amount, an under-minimum payout, and an unsupported corridor. Each is refused before any provider call, so these are safe to run repeatedly. See Errors.

Next steps

Authentication

The canonical string, and how to triage a 401.

Test and live modes

How mode is resolved and what it changes.