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

# Test mode

> fn_test_ keys: free paths run for real, paid paths serve recorded real sessions, and named sentinels reproduce every failure ending deterministically.

Test mode is a **thin branch at the paid boundary, not a sandbox environment** —
there is no second engine and there are no simulated merchants. That's a feature:
what you integrate against is real.

## Getting a test key

* **Dashboard signup mints the pair.** Self-serve signup at
  [platform.fiveninelabs.com](https://platform.fiveninelabs.com) creates your account
  with a "Default live" and a "Default test" key side by side — accounts created this
  way always have the `fn_test_` twin.
* **Hand-provisioned accounts** (design partners, evaluations) get keys minted per
  key by an operator. If your account came with only a live key, ask your operator
  contact for the test twin — don't develop error handling against real money.

<Note>
  **Stated gap:** there is no API surface to mint a key — `GET /v1/keys` lists,
  `PATCH`/`DELETE` rename and revoke, but nothing creates. An account holding only a
  live key cannot self-serve its test twin today; closing this is an open item on our
  side.
</Note>

On an `fn_test_` key:

* **Free paths run for real.** Parse, estimate, narrow, ask, session reads — the same
  code paths as production, actually executed (discover joins this list when it
  [ships](/guides/extractions)).
* **Paid paths serve recorded real sessions.** A session or extraction create streams
  a **replay of a real production run** appropriate to your request shape — through
  the same SSE path, with the same event shapes, at \$0 (replays are free by
  construction). Internal evidence URLs and operator metadata are scrubbed; the event
  spine is otherwise the real thing.
* **Webhooks deliver for real.** Signed, retried, and logged like production — so the
  whole integration loop, including your webhook handler, wires up and runs in CI
  without spending a cent.

`fn_test_` and `fn_live_` share the account, scopes, caps, and webhook endpoints.
Behavior differs only at the paid boundary — which is the point.

## What test keys never do

Spend money, dispatch a pass, touch a live merchant, or mint ledger entries beyond \$0
replay reads.

## The magic inputs — rehearse every ending

Error handling should be rehearsed, not discovered in production. On an `fn_test_`
key, a `query` (sessions) or `guidance` (extractions) containing one of these
sentinels reliably produces that outcome — same status codes, same envelopes, same
event shapes as production:

| Sentinel             | Outcome                                                                                                                                                                                                                                                                                                                                            |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FN_TEST_402`        | [`402 insufficient_credit`](/errors/insufficient_credit), with balance/reserve context and `top_up_url`                                                                                                                                                                                                                                            |
| `FN_TEST_PROPOSAL`   | The first follow-up turn returns [`409 proposal_required`](/errors/proposal_required) with a priced proposal                                                                                                                                                                                                                                       |
| `FN_TEST_ABSTAIN`    | A streamed run ending `abstain` → status `abstained` (\$0, stated)                                                                                                                                                                                                                                                                                 |
| `FN_TEST_DECLINE`    | A decline to spend, [as production does it](/errors/declined_to_spend): `201` → a streamed `abstain` with the parse's reasoning → status `abstained`, \$0. Pair with an estimate call to rehearse `would_run: false`                                                                                                                               |
| `FN_TEST_SPEND_STOP` | The ceiling reached mid-run: a streamed run ending `search-done` with `stop: "spend-budget"` and one source left `unchecked` → status **`stopped`**. It **delivers rows** — two sources deliver 5 records before the cap, 4 qualified — so `GET …/rows`, `GET …/receipts` and `result.rows` are all non-empty: you keep what was already delivered |
| `FN_TEST_ERROR`      | A streamed run ending `error` → status `error`, reservation released                                                                                                                                                                                                                                                                               |

Sentinels are stable API surface (new ones may appear — the additive rule). They are
**inert on `fn_live_` keys** — matched only under a test key, so a production query
containing the literal string is just a query.

## Examples

Rehearse the out-of-credit path:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/sessions \
  -H "Authorization: Bearer $FIVENINE_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "FN_TEST_402 vinyl records", "max_spend_usd": 2}'
```

Rehearse the proposal flow end to end (create, then the first follow-up turn returns
the 409; confirm it like a real one):

```bash theme={null}
curl https://api.fiveninelabs.com/v1/sessions \
  -H "Authorization: Bearer $FIVENINE_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "FN_TEST_PROPOSAL vinyl records", "max_spend_usd": 2}'
# → { "id": "sess_…", … }  then:
curl https://api.fiveninelabs.com/v1/sessions/sess_REPLACE/turns \
  -H "Authorization: Bearer $FIVENINE_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "also check San Antonio"}'
# → 409 proposal_required, with a priced proposal to confirm
```

Rehearse an honest abstain on the stream:

```bash theme={null}
curl -N https://api.fiveninelabs.com/v1/sessions \
  -H "Authorization: Bearer $FIVENINE_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"query": "FN_TEST_ABSTAIN anything", "max_spend_usd": 2}'
# → …streams, then: {"phase":"abstain","reason":"…"} and status "abstained"
```

## A CI recipe

A complete integration test costs \$0 and touches no live merchant:

1. `GET /v1/me` on the test key — auth and scopes.
2. `POST /v1/sessions/estimate` — the free quote path, run for real.
3. `POST /v1/sessions` with an ordinary query — stream a replayed real session;
   assert your event loop handles the full spine through `session-turn`.
4. One create per sentinel — assert your handling of the 402, the proposal confirm,
   the abstain, the decline (a \$0 abstained run — not an HTTP error), the
   spend-budget stop, and the error terminal.
5. Register a webhook endpoint and drive one replay run to its terminal — assert your
   signature verification and dedupe against the real delivery.
   (`POST /v1/webhooks/{id}/test` will make this a one-call step once it ships — it
   is planned, not yet available.)

If all five pass, the first thing your live key meets in production will be a code
path you've already exercised.
