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.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
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 byTEST_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
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.
Debug a signing failure
Set thedebug_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
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.
2000UGX is two thousand shillings;2000KES 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.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.