browser_ session
One browser page you keep across several API calls. Open a session, take a snapshot to see what the page offers, act on the refs that snapshot handed back, look again — and pay for the login once instead of once per call.
Use Cases
Log In Once, Read Many Pages
Sign in on the first call, then navigate and read behind the login for as long as the session lives — without repeating the login every time
Explore an Unfamiliar App
Snapshot a page you have never seen, read its interactive elements out of the response, and act on them in the next call instead of guessing CSS selectors
Agent Loops
Give an agent a see-then-act loop: the snapshot tree is the observation, the refs are the action space, and the next snapshot is the feedback
Multi-Step Wizards
Walk a checkout or an onboarding flow one step per call, checking what the page became before choosing the next action
Debugging a Failing Chain
When a one-shot action chain fails on a guessed selector, open a session and step through it, taking a screenshot between steps
Reading After the Interaction
Extract markdown, HTML, text or metadata from the live DOM as it stands after everything the session has clicked
Endpoint
/api/v1/tools/browser_sessionParameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
operation | string | Required | - | Which step of the session this call performs: `open`, `snapshot`, `act`, `read`, `screenshot`, `close` or `list`. Every operation except `open` and `list` needs `session_id`. The operation also sets the price — see **Operations and what they cost** below. Example: snapshot |
session_id | string | Optional | - | The id `operation: "open"` returned, carried on every later call in the session. Required by `snapshot`, `act`, `read`, `screenshot` and `close`. An id that is unknown, expired, closed, or belongs to another account all give the same "Session not found" — ids are not enumerable, so the error never tells you which of the four you hit. Example: 9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c |
url | string | Optional | - | The page the session opens on. Required by `open` and ignored by the other operations — once a session exists you move it with a `navigate` action inside `act`. The URL passes the SSRF guard and the target's robots.txt before a browser is launched, and so does every in-session navigation. Example: https://app.example.com/login |
stealth | boolean | Optional | false | `open` only. Run the session in the stealth Chromium engine at its `medium` profile instead of the standard browser pool. It is slower to start, and it renders JavaScript rather than solving challenges. The session keeps this setting for its whole life. Example: false |
ttl | number | Optional | 600 | `open` only. How long the session may live at all, in seconds (30-3600). The session closes on this clock whatever you are in the middle of, so size it to the work you have planned. Example: 600 |
activity_ttl | number | Optional | 300 | `open` only. How long the session may sit idle between calls, in seconds (10-3600). Every operation restarts this clock; whichever clock expires first closes the session and frees its page. Example: 300 |
viewport | object | Optional | - | `open` only. The browser window the session runs in: `{ width, height }`, width 800-1920 and height 600-1080. Defaults to the browser pool's own size. Example: {"width": 1440, "height": 900} |
timeout | number | Optional | 30000 | Time budget in milliseconds for the browser work this call does (10000-120000): the first navigation on `open`, the action chain on `act`. Keep it inside the ~25s REST window. Example: 30000 |
respect_robots | boolean | Optional | true | Respect the target site's robots.txt. Omit it and the compliant default (`true`) applies: a URL disallowed for `CrawlForge` is refused before the browser opens and this endpoint charges nothing for it, and every `navigate` action inside `act` is checked the same way. It applies to the call it is sent on and is never remembered by the session, so an override has to be repeated as deliberately as it was made. Set it to `false` only for a target you have your own agreement with — the override is recorded against your API key. Example: true |
interactive_only | boolean | Optional | true | `snapshot` only. List just the elements that take a pointer or the keyboard, which are the ones that get a ref. Set it to false to list headings and landmarks as well — useful for finding your way around a page, but those nodes never get a ref. Example: true |
max_nodes | number | Optional | 200 | `snapshot` only. Caps how many nodes the tree lists (1-1000) — under the default `interactive_only` that is one ref each. The result reports `truncated: true` when the cap stopped the walk. Example: 200 |
actions | array | Optional | - | `act` only. 1-20 browser actions run against the page the session already holds, in the same vocabulary [scrape_with_actions](/docs/api-reference/tools/scrape-with-actions) takes — `wait`, `click`, `type`, `press`, `scroll`, `screenshot`, `select`, `hover`, `navigate`, `snapshot` — with their type-specific fields. Point `selector` at a `@e1` ref from the session's last snapshot instead of a guessed CSS selector. `executeJavaScript` is refused on the hosted API: the script would run in a browser on our infrastructure rather than on your machine. Example: [{"type": "type", "selector": "@e1", "text": "user@example.com"}, {"type": "click", "selector": "@e3"}] |
continue_on_error | boolean | Optional | false | `act` only. Keep running the remaining actions when one fails instead of stopping the chain. The per-action results say which ones ran. Example: false |
formats | array | Optional | ["markdown"] | `read` only. What to extract from the page as it stands right now: `markdown`, `html`, `text`, `json` (title, metadata and structured data). Content comes from the live DOM, so it reflects the session's cookies and everything it has clicked — not a fresh fetch of the URL. Example: ["markdown", "json"] |
full_page | boolean | Optional | false | `screenshot` only. Capture the whole scrollable page instead of just the viewport. Example: false |
format | string | Optional | png | `screenshot` only. Image format: `png` or `jpeg`. Example: png |
quality | number | Optional | 80 | `screenshot` only. JPEG quality, 0-100. PNG ignores it. Example: 80 |
selector | string | Optional | - | `screenshot` only. Capture one element instead of the page — a CSS selector, or an `@e1` ref from the session's last snapshot. Example: @e2 |
max_inline_chars | number | Optional | 40000 | `snapshot`, `act` and `read` only — the operations that return page content. Largest result to return inline, in characters of its JSON (1,000-10,000,000). Over it the response carries `preview`, `result_handle`, `total_chars`, `truncated: true` and `expires_at`, and [read_result](/docs/api-reference/tools/read-result) reads the rest for 1 credit per call; the fields measured are `content.markdown`, `content.text`, `content.html` and `snapshot.tree`. Stored results are kept for 1 hour. The session is untouched by this — the page stays open and the next operation still sees all of it. Example: 40000 |
redact_pii | boolean | object | Optional | false | `snapshot`, `act` and `read` only; the other operations return bookkeeping and are left untouched. Remove personal data from the text this call returns, before the result is stored or sent back — a logged-in page whose snapshot `tree` and extracted content carry the account holder's details is what this is for. `true` is shorthand for `{ mode: "fast" }` — all four regex classes, tagged. Whenever you ask for redaction the response carries `redaction: { entities, count, mode }` inside `data`, even when nothing matched (`count: 0`), so "found nothing" is never mistaken for "the parameter was ignored". Redaction runs **before** the result is stored, so an oversized result read back later with [read_result](/docs/api-reference/tools/read-result) is already redacted. Two deliberate limits: addresses (`url`, `link`, `href`, `canonical_url`) are never redacted, and counters derived from the text (`content_length`, `word_count`, `character_count`) describe it as extracted, before redaction. Example: true |
Operations and what they cost
Each call is billed on its own operation, so a cheap look does not pay for an expensive open. Nothing runs for free: close and list still cost 1 credit each.
url and returns a sessionId plus both expiry clocks. The dearest operation because it is the one that starts a browser — strictly more work than a one-shot scrape.@e1, @e2, … in document order. This is the "look" half of the loop, and it is what the next call aims at.scrape, because it is the same content extraction.crawlforge://screenshot/{id} resource URI rather than inline base64, and this REST endpoint passes that URI through without resolving it — so for now the bytes are readable only from the CrawlForge MCP server.How long a session lives, and how many you get
A session lives in the execution backend's memory, on a single instance. It does not survive a redeploy or an instance restart, so treat a sessionId as short-lived and be ready for the next call to answer "Session not found" — re-open and carry on. Sessions are short-lived by design: ttl (default 600s, range 30-3600) is the absolute clock, activity_ttl (default 300s, range 10-3600) the idle one, and whichever fires first closes the session. A REST API key may hold one session at a time. A second open while one is still live is refused with a named error, not queued — a session slot frees minutes from now, so waiting would simply hang the call. The hosted backend holds three sessions in total across every customer, which is why the per-key limit is one: close when you are done instead of letting the TTL run out, and the next open is yours.
Targeting elements by ref
A snapshot puts a stable ref on each interactive element it finds, and the session keeps those refs for as long as it keeps the page — so the loop is open, snapshot, act on @e1 / @e2 in a separate call, snapshot again. That is the whole point of a session: a one-shot chain has to name its selectors before it has seen the page. A ref is valid only on the document its snapshot was taken from: any navigation — a navigate action, or a click that loads a new page — invalidates every ref, and acting on a stale one fails loudly with an error telling you to take a new snapshot, never by silently clicking the wrong thing.
browser_session or scrape_with_actions?
Both drive a real browser and take the same action vocabulary. The difference is whether you already know what the page looks like.
open, snapshot, act, act, read, close — costs 9 credits against 5 for a one-shot chain that is far likelier to fail on a guessed selector.Persistent logins are not available here
A session's cookies and localStorage live and die with the session. There is no profile parameter on this API and no way to save a logged-in browser for the next session, so a login has to be performed again each time you open one. Saved login profiles are a later, locally-gated feature — do not plan a hosted workflow around them.
Request Examples
# 1. Open the session (3 credits). The id comes back as data.sessionId.
curl -X POST https://crawlforge.dev/api/v1/tools/browser_session \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation": "open",
"url": "https://app.example.com/login",
"ttl": 600
}'
# {"success": true, "data": {"sessionId": "9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c", ...}}
# 2. Look at the page before touching it (1 credit).
curl -X POST https://crawlforge.dev/api/v1/tools/browser_session \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation": "snapshot",
"session_id": "9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c"
}'
# data.snapshot.tree comes back as:
# [document] "Sign in"
# @e1 [textbox] "Email"
# @e2 [textbox] "Password"
# @e3 [button] "Sign in"
# 3. Act on those refs in a SEPARATE call (1 credit). Same page, same cookies.
curl -X POST https://crawlforge.dev/api/v1/tools/browser_session \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation": "act",
"session_id": "9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
"actions": [
{"type": "type", "selector": "@e1", "text": "user@example.com"},
{"type": "type", "selector": "@e2", "text": "secret123"},
{"type": "click", "selector": "@e3"},
{"type": "wait", "duration": 1000}
]
}'
# 4. Read the page the login landed on (2 credits).
curl -X POST https://crawlforge.dev/api/v1/tools/browser_session \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation": "read",
"session_id": "9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c",
"formats": ["markdown"]
}'
# 5. Close it rather than waiting for the TTL (1 credit). 8 credits in all.
curl -X POST https://crawlforge.dev/api/v1/tools/browser_session \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation": "close",
"session_id": "9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c"
}'Response Example
{ "success": true, "data": { "success": true, "operation": "snapshot", "sessionId": "9f2c1e6a-4b7d-4c3e-8a5f-1d2e3f4a5b6c", "url": "https://app.example.com/login", "stealth": false, "expiresAt": 1789459200000, "idleExpiresAt": 1789458900000, "snapshot": { "snapshotId": "a3f19c2d", "url": "https://app.example.com/login", "title": "Sign in", "tree": "[document] \"Sign in\"\n @e1 [textbox] \"Email\"\n @e2 [textbox] \"Password\"\n @e3 [button] \"Sign in\"", "refCount": 3, "nodeCount": 3, "truncated": false, "interactiveOnly": true } }, "credits_used": 1, "credits_remaining": 996, "processing_time": 180}data.operationThe operation this call performed — every response echoes it backdata.sessionIdSend this on every later call in the sessiondata.urlWhere the session's page is right now, which a navigation movesdata.expiresAtWhen the absolute `ttl` clock expires, in milliseconds since the epochdata.idleExpiresAtWhen the idle `activity_ttl` clock expires; every operation pushes it forwarddata.snapshot.treeThe accessibility tree. Each interactive element carries the ref you target in the next calldata.snapshot.refCountHow many elements got a ref in this snapshotdata.snapshot.truncatedTrue when `max_nodes` stopped the walk before the end of the pagecredits_usedCredits deducted for this call — 1 for a snapshot, 3 for an openprocessing_timeTime in ms for this call alone, not the sessionError Handling
Session not found (502 TOOL_ERROR)
The session_id is unknown, expired, already closed, or belongs to another account — all four answer alike, so ids cannot be probed from outside. Sessions also do not survive a restart of the execution backend. Open a new one and carry on; nothing is charged for the failed call.
Session limit reached (502 TOOL_ERROR)
You already hold the maximum of one open session. The call is refused rather than queued, because a session slot frees on its TTL minutes from now. Send operation: "close" for the session you have — operation: "list" tells you its id — or wait for its TTL.
Browser runtime not configured (503 TOOL_NOT_AVAILABLE)
This tool needs a browser automation runtime. When the hosted execution backend is not configured the call returns 503 immediately and no credits are charged.
Execution backend timed out (504 MCP_UPSTREAM_TIMEOUT)
The navigation or the action chain exceeded the execution backend's time budget. Shorten waits, split the chain across two act calls — the session is still there — or retry. Failures are not charged.
Blocked by robots.txt (502 TOOL_ERROR)
The target's robots.txt disallows this URL for CrawlForge, so no browser was launched and nothing was charged. Every navigate action inside act is checked the same way, not just the URL you opened on. Set respect_robots: false to override if you have your own agreement with the target — the override is recorded against your API key.
Invalid request (400 Bad Request)
The operation is missing a parameter it needs — open without url, act without actions, or any operation other than open and list without session_id — or an action failed its own schema. Nothing is charged.
Insufficient Credits (402 Payment Required)
Your account doesn't have enough credits for this operation (up to 3). Purchase more credits or upgrade your plan.
Rate Limit Exceeded (429 Too Many Requests)
You've exceeded your plan's rate limit. Wait a moment or upgrade your plan for higher limits.
Snapshot after anything that changes the page, not just at the start — a click that navigates invalidates every ref, and the fresh tree is 1 credit. When a session is only ever going to run one fixed chain, scrape_with_actions is cheaper and one round trip.
Credit Cost
operation, not per tool: open 3, read 2, and snapshot, act, screenshot, close and list 1 each. 3 credits is the ceiling — what open costs, and what an unrecognised operation is charged. A login-then-read flow (open, snapshot, act, act, read, close) comes to 9 credits.Free Plan: 1,000 one-time trial credits = about 110 login-then-read sessions
Hobby Plan: 5,000 credits/month = about 550 sessions ($19/mo)
Professional Plan: 50,000 credits/month = about 5,500 sessions ($99/mo)
Business Plan: 250,000 credits/month = about 27,000 sessions ($399/mo)
Related Tools
Ready to try browser_session? Sign up for free and get 1,000 credits to start building.