# Environments and API keys

There are two environments, **sandbox** and **live**, and you never select between them. The key you send decides, and nothing else can.

## How routing works

<Mermaid chart={`
flowchart LR
    A["X-API-Key<br/>kn_sk_test_…"] --> B{prefix}
    C["X-API-Key<br/>kn_sk_live_…"] --> B
    B -->|test| D["Sandbox<br/>services"]
    B -->|live| E["Live<br/>services"]
    D --> F[("Sandbox<br/>database")]
    E --> G[("Live<br/>database")]
    F -.->|no path between them| G
`} />

Same URL, same paths, same payloads. The prefix on your key routes the request to a different set of service instances backed by a different database.

That separation is physical, not a filter. There is no query a test key can construct that reaches live rows, because the connection it is served on does not point at that database. A leaked test key cannot read a live payment, and a test key pasted into production config fails loudly on data that is not there — rather than quietly moving real money.

## The two keys

| Prefix | Environment | Moves real money | Where the customer's phone rings |
| --- | --- | --- | --- |
| `kn_sk_test_` | Sandbox | No | Nowhere. Outcomes are simulated. |
| `kn_sk_live_` | Live | **Yes** | The real number you charged. |

Ids carry the same signal, so a value in a log is self-describing: `knpay_test_9f2c41a7…` is a sandbox payment, `knpay_9f2c41a7…` is a live one. Treat the rest as opaque — do not parse ids, and do not assume a length.

## Handling keys

**A secret key is a bearer credential.** Anyone holding it can act as your business within its [scopes](/en/concepts/scopes). It belongs in your server's environment, never in a mobile app, a browser bundle, or a repository. Vite and Next inline anything prefixed `VITE_`/`NEXT_PUBLIC_` into the client bundle, which is the most common way a live key ends up public.

**Scope down rather than trust.** A key that only needs to collect should hold `payments:write` and `payments:read` and nothing else. Then a leak is bounded by construction instead of by how quickly you notice.

**Rotate without downtime.** Mint the new key, deploy it, confirm traffic has moved, then revoke the old one. Revoking first means an outage between the two steps.

**One key per consumer.** Separate keys for your checkout server, your reconciliation job and your staging box mean you can revoke one without taking down the others, and the audit trail tells you which system did what.

## Things that surprise people

**There is no environment header.** If you find yourself looking for one, you are looking for a way to make a mistake. The key is the environment.

**Sandbox and live share nothing.** Not payments, not payment links, not invoices, not webhook endpoints. A payment link you created in sandbox does not exist in live, and its `checkout_url` carries `?type=sandbox` so the hosted page reads the right side.

**Test keys still enforce scopes and rate limits.** That is deliberate: sandbox that is more permissive than production teaches you the wrong lesson and fails on launch day.

## Next steps

- [Managing API keys](/en/payments/api-keys) — creating, rotating and revoking in the dashboard
- [Scopes and least privilege](/en/concepts/scopes) — bounding what a key can do
- [Authentication](/en/payments/authentication) — the header, and what a rejection looks like
- [Testing](/en/payments/testing) — simulating outcomes in sandbox
