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

# Live and test modes

> Mode is a property of the key you signed with, never a field in the body.

There is no request field for "test mode". Mode is a property of **which API
key authenticated the request**.

Each tenant is issued a live credential set and, separately, a test set. Each
is bound to its own tenant record with its own `is_test` flag. AptaPay
resolves mode server-side from the authenticated key; it is never read from
the body.

<Warning>
  If your request body includes a `mode` field that disagrees with the key you
  signed with, AptaPay rejects it with `400 mode_mismatch` rather than
  silently honouring either. This is deliberate — a body field that could
  flip a live charge to test would be a way to fake settlements.
</Warning>

## What differs between modes

|                | Live                 | Test                                                    |
| -------------- | -------------------- | ------------------------------------------------------- |
| Credentials    | `ak_live_…` set      | Separate `ak_test_…` set                                |
| Callback URL   | `callback_url`       | `callback_url_test`                                     |
| Amount ceiling | Corridor limits only | Also capped at **500.00** in the currency's minor units |
| Money          | Real                 | **Also real** — see below                               |

## Test mode is not a sandbox

This is the point most integrations get wrong.

<Danger>
  The upstream provider has no sandbox. "Test mode" for AptaPay means a **real
  provider
  account with a low-balance wallet**, not a consequence-free environment.
  Test-mode transactions move real money in small amounts.
</Danger>

Consequences for how you use it:

* **Do not load-test against test-mode credentials.** You will drain a real
  wallet and hit real provider rate limits.
* Expect the ceiling to be low. Test-mode amounts are capped at
  `500.00` — that is `50000` minor units in a 2-decimal currency, or `500` in
  UGX, RWF, XAF and XOF.
* Failures in test mode are real provider failures, which makes them a
  genuinely good rehearsal for your error handling.

For testing your own integration logic — signing, retries, webhook
handling — see [Testing](/guides/testing).

## Keeping the two apart

Use separate secret storage per mode and select by environment, never by a
runtime flag that a request could influence:

```typescript config.ts theme={null}
const isProduction = process.env.NODE_ENV === "production";

export const aptapay = {
  appId: process.env.APTAPAY_APP_ID!,
  keyId: process.env.APTAPAY_KEY_ID!,
  apiKey: isProduction
    ? process.env.APTAPAY_LIVE_API_KEY!
    : process.env.APTAPAY_TEST_API_KEY!,
  signingSecret: isProduction
    ? process.env.APTAPAY_LIVE_SIGNING_SECRET!
    : process.env.APTAPAY_TEST_SIGNING_SECRET!,
};
```

Every webhook carries `data.is_test_mode`, so your handler can assert the
mode matches the environment it is running in and refuse anything
unexpected.
