Skip to main content
Frontend teams keep asking the same four questions. Here are the honest answers, as the API behaves today.

CORS: open, and deliberate about resume

Every API response carries permissive CORS headers:
So browser-direct calls work from any origin, Last-Event-ID is pre-allowed for stream resume, and the response headers your retry logic needs (Retry-After, fn-request-id) are exposed. This posture is a contract, not an accident — a change would be a dated-version change announced in the changelog. Open CORS is not an invitation to put an API key in a page. It exists so scoped, short-lived credentials (below) can stream directly.

Why EventSource can’t connect

The native browser SSE client cannot set request headers, and every credential on this API is Authorization: Bearer — there is no query-param or cookie auth (?api_key=… returns 401 invalid_key). So EventSource, with its free auto-reconnect and automatic Last-Event-ID bookkeeping, cannot reach the stream at all. Stream with fetch + ReadableStream instead: split on blank lines, accumulate id: / data: fields, track the last id: yourself, and reconnect with a Last-Event-ID header. Pair it with the rebuild-then-attach reload recipe and a hard page reload mid-run survives cleanly — the sequence numbers are shared between the stream and GET …/events, and this combination is verified working browser-direct.

Never ship a live key to a page

An fn_live_ key in browser code (bundle, localStorage, a prop) is fully spend-capable: runs:write starts real runs against your balance, and open CORS means any origin that obtains the key can use it from anywhere. Treat fn_live_ (and fn_test_) exactly like a Stripe secret key: server-side only, always. The pattern to copy is the one the dashboard’s own playground uses — a playground token: a scoped bearer (fnpg_…) that can create at most one session at or under a stated max_spend_usd, then read/stream/stop only that session, and expires in 10 minutes (expiry is the revocation). Losing one to the browser risks a single bounded run, not the account. Today that token is minted only by the platform dashboard for its playground — there is no public API endpoint to mint one yet (a stated gap; an ephemeral-token endpoint is the planned fix). Until it ships, the browser architecture is:
  1. Your backend holds the key. The page never sees fn_live_….
  2. Create server-side, stream through your own route. The backend POSTs /v1/sessions, hands the page the session id, and proxies GET …/stream through a route handler that injects the Authorization header — ~20 lines in any framework; forward the client’s Last-Event-ID header through for resume.
  3. Reads can be proxied cheaply and freely — read GETs are unthrottled, so a thin pass-through for /events and /result costs you nothing.
When the ephemeral-token endpoint ships, step 2 collapses to “mint a token server-side, hand it to the page, stream direct” — the CORS posture above is already built for it.

Test keys in the browser

An fn_test_ key never spends, which makes it tempting for demos — but it still authenticates your whole account (reads, webhook management). Keep it server-side too; use the proxy pattern for demos, with the test key behind it.