> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pay.aptahq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test your integration

> Test and live keys, the test-mode amount ceiling, the Postman collection, and debugging signing.

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.

<Warning>
  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.
</Warning>

|                         | Test key                     | Live key                          |
| ----------------------- | ---------------------------- | --------------------------------- |
| Host                    | `https://api.pay.aptahq.com` | `https://api.pay.aptahq.com`      |
| Moves real money        | Yes, in small amounts        | Yes                               |
| Per-transaction ceiling | `TEST_MODE_MAX_AMOUNT_MINOR` | Your corridor and velocity limits |

### 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.

```json 400 mode_mismatch theme={null}
{
  "code": 400,
  "message": "Mode mismatch",
  "error": {
    "code": "mode_mismatch",
    "details": { "declared": "test", "key_mode": "live" }
  }
}
```

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](/concepts/money).

<Note>
  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.
</Note>

## The Postman collection

The collection is generated, and it signs every request for you.

```bash Generate the collection and environments theme={null}
cd functions && npm run postman
```

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.

<Warning>
  Keep `signing_secret` as a Postman **secret** variable in an environment, never in the collection. A collection is a file people share.
</Warning>

### 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.

```text What debug_signing prints theme={null}
v1
{APP_ID}
{KEY_ID}
{HOST}
{METHOD}
{PATH}
{CANONICAL_QUERY}
{TIMESTAMP}
{NONCE}
{BODY_HASH}
```

Compare that line by line against what your own client builds. See [Authentication](/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 `401`s 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.

<Warning>
  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.
</Warning>

## A testing checklist

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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](/concepts/webhooks).
  </Step>

  <Step title="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](/concepts/idempotency).
  </Step>

  <Step title="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](/concepts/errors).
  </Step>
</Steps>

## Next steps

<Columns cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    The canonical string, and how to triage a 401.
  </Card>

  <Card title="Test and live modes" icon="toggle-left" href="/concepts/modes">
    How mode is resolved and what it changes.
  </Card>
</Columns>
