Skip to main content
Signing in at platform.fiveninelabs.com mints two keys automatically: fn_live_… (spends your balance) and fn_test_… (never spends — see Test mode). When you’re signed in here, your real keys are inlined into every sample; otherwise export them first:
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.
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:
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:
(parsed.confidence is a string-wrapped float today — a documented divergence; 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.

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

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.
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:
…then long-poll for the answer (the backend-friendly path — 200 always means the run reached a terminal, 202 + Retry-After means poll again):
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). All the ways to wait — stream, long-poll, webhooks — are compared in Streaming, long-poll, or webhooks.

After the run: the receipt

Every charged dollar traces to a signed per-source receipt:
And the full row corpus pages for free, forever (≥ 90 days retention) — re-reading a finished session never re-runs it:

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.