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

# Quickstart

> Three calls: a free quote, an instant $0 replay, then the real run — with the wait announced.

Signing in at [platform.fiveninelabs.com](https://platform.fiveninelabs.com) mints two
keys automatically: `fn_live_…` (spends your balance) and `fn_test_…` (never spends —
see [Test mode](/guides/test-mode)). When you're signed in here, your real keys are
inlined into every sample; otherwise export them first:

```bash theme={null}
export FIVENINE_API_KEY="fn_live_..."
export FIVENINE_TEST_KEY="fn_test_..."
```

<Note>
  **If you only have a live key:** dashboard signup is the only self-serve way to get
  the `fn_test_` twin today — there is no API to mint keys (`GET /v1/keys` lists,
  nothing creates). Hand-provisioned accounts (design partners, evaluations) get keys
  minted by an operator; if yours came without the test key, ask your operator contact
  for it rather than developing against real money. See
  [Test mode → Getting a test key](/guides/test-mode#getting-a-test-key).
</Note>

**Set the right expectation before the first call:** a real session drives real
browsers against real merchant sites and legitimately takes **tens of minutes** —
measured real runs span **11 to 68 minutes** end to end (single-merchant questions at
the low end; runs that retry flaky sources at the high end), with first rows measured
between \~4 and \~17 minutes. That's why this quickstart is sequenced the way it is —
two instant, free successes first, then the real run with the wait announced.

## Step 0 — prove your key works (\$0)

`GET /v1/me` returns the calling key's identity, caps, and balance without spending:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/me \
  -H "Authorization: Bearer $FIVENINE_API_KEY"
```

```json theme={null}
{
  "account_id": "acct_4k1na",
  "key": {
    "id": "key_8snw2",
    "name": "default",
    "prefix": "fn_live_8snw",
    "last4": "2kQx",
    "scopes": ["runs:write", "runs:read", "webhooks:manage", "account:read"],
    "max_spend_usd_per_session": 5,
    "monthly_cap_usd": 100
  },
  "balance_usd": 100,
  "open_reserves_usd": 0,
  "concurrency_limit": 1,
  "fn_version": "2026-08-26"
}
```

Two shape details worth coding to: `key.prefix` is a **12-char fingerprint** (the
mode prefix plus the first 4 chars of the tail — `fn_live_8snw`, not the bare
literal `fn_live_`), so branch on `prefix.startsWith("fn_test_")` or read `mode`
from `GET /v1/keys`; and the two cap fields are **absent when no cap is set** —
budget-guard code should treat `undefined` as "uncapped", not as an error.

## Step 1 — get a quote (instant, free)

`POST /v1/sessions/estimate` is free and always callable — no charge, no balance
required, works on test keys and zero-balance accounts. It returns what the parse
understood, **which merchant domains would be dispatched** (the list, not a count — you
see the aim before money moves), and the estimated cost range:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/sessions/estimate \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "vinyl records under $30 in Austin"}'
```

```json theme={null}
{
  "would_run": true,
  "parsed": { "entity": "record", "geo": "Austin, TX", "confidence": "0.92" },
  "planned_merchants": ["endofanear.shop", "waterlooarecords.com", "breakawayrecords.net"],
  "planned_dispatches": 3,
  "estimated_usd_low": 0.9,
  "estimated_usd_high": 2.1
}
```

(`parsed.confidence` is a string-wrapped float today — a
[documented divergence](/guides/events#the-stable-subset); on events, `confidence` is
a JSON number 0–1, which is the canonical form.)

`would_run: false` means we would decline to spend on this query — stated up front,
before you commit a budget. **This is the pre-spend gate to build on**: if you create
the session anyway, you get a `201` and a run that ends `abstained` at \$0 within
seconds, never a synchronous error. See
[declined\_to\_spend](/errors/declined_to_spend).

## Step 2 — stream a session (instant, \$0, real event shapes)

Your `fn_test_` key streams a **recorded real session** through the same SSE path with
the same event shapes as production — so you can build your whole event loop before
spending a cent. Note the key and the `Accept` header:

```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": "vinyl records under $30 in Austin", "max_spend_usd": 2}'
```

You'll see the full spine immediately: `search-intent` (what the parse understood),
`search-resolve` (the merchant fan-out), `orchestrate` decisions (every dispatch and
decline, with the reason), `search-source` (attributed rows with per-source cost),
`search-answer` (the adjudicated result), and `search-done` (how the run stopped, with
any `unchecked` merchants stated). The vocabulary is documented in
[The event stream](/guides/events).

## Step 3 — the real run (tens of minutes; that's normal)

Same call, live key. **Expect the stream to go quiet for stretches** — between the
fan-out plan and the first harvested rows, the agent is driving real browsers, and the
only traffic is orchestrator decision events and heartbeat pulses. Silence is not a
hang; any event, whatever its phase, means the run is alive. Measured real runs:
first rows in \~4–17 minutes, whole sessions **11–68 minutes** end to end.

```bash theme={null}
curl -N https://api.fiveninelabs.com/v1/sessions \
  -H "Authorization: Bearer $FIVENINE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"query": "vinyl records under $30 in Austin", "max_spend_usd": 2}'
```

`max_spend_usd` is required on every spending call: it's this run's reservation and
hard ceiling. The run can never charge past it, and it settles at measured actuals —
usually below the cap. The `Idempotency-Key` makes a network-timeout retry safe: a
replay returns the original response and never bills twice.

If you'd rather not hold a socket for the better part of an hour, drop the `Accept`
header — the call returns `201` with the session id immediately:

```json theme={null}
{
  "id": "sess_9m2kd",
  "object": "session",
  "status": "running",
  "query": "vinyl records under $30 in Austin",
  "max_spend_usd": 2,
  "created_at": "2026-08-26T14:32:07Z",
  "links": {
    "stream": "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/stream",
    "events": "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/events",
    "result": "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/result",
    "rows": "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/rows",
    "receipts": "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/receipts",
    "console": "https://platform.fiveninelabs.com/sessions/sess_9m2kd"
  }
}
```

…then long-poll for the answer (the backend-friendly path — `200` always means the
run reached a terminal, `202` + `Retry-After` means poll again):

```bash theme={null}
curl "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/result?timeout=240" \
  -H "Authorization: Bearer $FIVENINE_API_KEY"
```

**Use `timeout=240` and loop on the `202`, not one `timeout=600` call.** Stock
Node `fetch` (undici) gives up on response headers after **300 seconds** — a single
600-second block dies at exactly 5 minutes with an opaque `TypeError: fetch failed`
while the run continues server-side. `≤ 240` keeps every mainstream HTTP client
happy; the `202 + Retry-After` loop is the designed pattern
([details](/guides/streaming#long-poll-one-blocking-call)).

All the ways to wait — stream, long-poll, webhooks — are compared in
[Streaming, long-poll, or webhooks](/guides/streaming).

## After the run: the receipt

Every charged dollar traces to a signed per-source receipt:

```bash theme={null}
curl https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/receipts \
  -H "Authorization: Bearer $FIVENINE_API_KEY"
```

And the full row corpus pages for free, forever (≥ 90 days retention) — re-reading a
finished session never re-runs it:

```bash theme={null}
curl "https://api.fiveninelabs.com/v1/sessions/sess_9m2kd/rows?limit=100" \
  -H "Authorization: Bearer $FIVENINE_API_KEY"
```

## Troubleshooting

* **Long-poll dies at exactly 5 minutes in Node?** That's undici's default 300 s
  headers timeout, not the API. Use `timeout ≤ 240` and loop on the `202`, or
  configure an undici `Agent` with `headersTimeout: 0`.
* **Node behind a proxy:** Node's built-in `fetch` ignores `HTTPS_PROXY` env vars. In
  proxied environments (CI, some VPCs), set `NODE_USE_ENV_PROXY=1` or your API calls
  will fail with opaque 403s while `curl` works.
* **Stream dropped mid-run?** The run keeps going server-side. Reconnect to
  `GET …/stream` with a `Last-Event-ID` header, or page `GET …/events?after=` — same
  sequence numbers, nothing lost.
* **Got a `409` on a follow-up turn?** That's not a failure — it's a priced proposal
  waiting for your confirmation. See [Budgets and priced proposals](/guides/proposals).
