# Test mode

Build and test your integration against the live service without spending money or mailing anything. A `pony_test_` key runs every request in a separate test environment with free test credit and simulated order progress.

Authentication, idempotency, errors and scopes work exactly as they do with a live key.

## Get a test key

Create a test key the same way as a live key, while signed in to the website, with `"test": true`:

```json
{"label": "Billing integration (test)", "scopes": ["letters:read", "letters:write", "orders:read", "orders:send", "orders:cancel", "wallet:read", "grants:read"], "expires_at": "$expires_at", "test": true}
```

`POST /api/account/api-keys` returns the secret once. Test secrets start with `pony_test_` and live secrets with `pony_live_`. `GET /api/account/api-keys` lists both, each with its `environment`, and you revoke them the same way.

## What a test key does

A request made with a `pony_test_` key runs entirely in the isolated `test` environment:

- `GET /identity` and `GET /wallet` report `"environment": "test"`.
- Test records are separate: wallet, credit history, quotes, send intents, sending permissions, orders, drafts, proofs, templates and batches. Live keys, OAuth connections and the website never see them, and test keys never see live records.
- Saved address references from the website are live records, so test letters use inline `recipient` and `sender` addresses.
- No permission setup is needed. A test key sends through an open test permission with no limits or address restrictions. Live keys still need a [sending permission](/docs/authentication.md).
- Final quotes use the configured price with `tax_minor: 0` and `tax_provenance.type: "test_mode"`. No tax or payment provider is called.
- Orders are marked `test_mode: true` and `credit_environment: "test"`. Nothing is printed or mailed, and test orders never reach the print queue, notifications or analytics.
- `POST /wallet/topups` is refused, because test keys can't create real funding links. Use test credit instead.

You don't need real credits to use a test key, so you can finish your integration before you fund your account.

## Add test credit

`POST /test/credits` adds up to $1,000 (100000 cents) of test credit per call. Call it again for more. It needs a test key with `wallet:read` and an `Idempotency-Key` header. Live keys and OAuth tokens get `403`.

```json
{"amount_minor": 50000, "currency": "USD"}
```

The response contains the ledger `transaction` (type `test_credit`) and the test `wallet`.

## Send a test letter

Use exactly the live flow from the [quickstart](/docs/quickstart.md): create a draft (or [render a template](/docs/templates.md)), render a proof, get a final quote and send. `grant_id` can be left out.

```http
POST /api/v1/drafts              -> draft_id, version
POST /api/v1/drafts/{id}/proofs  -> proof_id
POST /api/v1/send-quotes         -> send_intent_id, quote_id, total_minor
POST /api/v1/orders              -> order_id, status "queued", environment "test"
```

## Simulate order progress

`POST /test/orders/{order_id}/advance` moves a test order forward so you can exercise your status handling and [webhooks](/docs/webhooks.md). It needs a test key with `orders:read`. With no body it advances one step. With a `status`, it advances through every step up to that status:

```text
queued -> preparing -> printed -> mailed -> returned
```

```json
{"status": "mailed"}
```

The response has the same shape as `GET /orders/{order_id}`, including `history` and `tracker`. Each simulated step is recorded with the source `test_mode_simulation`.

Orders only move forward. `returned` requires `mailed` first, and cancelled orders can't advance. You can cancel a queued test order with `POST /orders/{order_id}/cancel`, which returns its test credit.

## Going live

Create a live key on **Account → Credits → API keys**, [fund your balance](/docs/concepts.md) and swap the key. Nothing else in your code changes. Test records stay in the test environment and aren't copied over.
