import type { Metadata } from "next"; import { CodeBlock } from "@/components/ui/code-block"; import { DocPage } from "@/components/docs/doc-page"; import { A, Code, H2, H3, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose"; import { Callout, ComingSoon } from "@/components/docs/callout"; import { Endpoint } from "@/components/docs/endpoint"; import { ParamTable } from "@/components/docs/param-table"; import { CodeTabs } from "@/components/docs/code-tabs"; import { ResponseExample } from "@/components/docs/response-example"; import { fetchTabs } from "@/components/docs/snippets"; export const metadata: Metadata = { title: "Browser", description: "Managed browser rendering on POST /v1/fetch: browser, browser_fallback, wait_for, wait_ms, wait_until, javascript, block_resources and screenshot; how automatic escalation works, what is captured, limits and errors.", }; const RENDERED = { request_id: "req_5d6e7f8g9h0i1j2k", success: true, status: 200, url: "https://app.example.com/dashboard", final_url: "https://app.example.com/dashboard?tab=results", content: null, content_type: "text/html; charset=utf-8", headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }, cookies: [ { name: "session", value: "9f3a…", domain: "app.example.com", path: "/" }, { name: "cf_clearance", value: "…", domain: ".example.com", path: "/" }, ], text: "Results\nRef\tPrice\nA-1041\tCA$ 1,250\n…", page: { title: "Dashboard — Results", description: null, canonical: "https://app.example.com/dashboard", lang: "en", og: {}, links_count: 64 }, screenshot: "iVBORw0KGgoAAAANSUhEUgAA…", metadata: { network: "residential", country: "CA", mode: "browser", attempts: 1, duration_ms: 4930, bytes: 612340, session: "sess_8f2k1m9d3p7q4r6s", cached: false, timing: { dns_ms: 17, proxy_connect_ms: 0, tls_ms: 0, origin_ms: 4480, processing_ms: 61, total_ms: 4930 }, }, }; const ACTIONS: Array<[string, string, string]> = [ ["goto", "{ url }", "Navigate to a URL and wait for the load event."], ["click", "{ selector }", "Click the first element matching a CSS selector."], ["type", "{ selector, text, delay? }", "Focus an element and type text, optionally with a per-key delay."], ["scroll", "{ selector? | y? }", "Scroll an element into view, or scroll the page by a number of pixels."], ["wait", "{ selector? | ms? }", "Wait for a selector to appear, or for a fixed duration."], ["evaluate", "{ script }", "Run JavaScript in the page and capture its return value."], ["screenshot", "{ full_page?, selector? }", "Capture a PNG of the viewport, the full page or one element."], ["extract", "{ schema }", "Return structured data from the rendered DOM (shares the Extraction schema format)."], ]; export default function BrowserPage() { return (

Browser rendering is part of the fetch request, not a separate endpoint. The browser runs on Fetcha's infrastructure, connects through the same proxy network, country and session as a plain fetch, loads the page, waits for it to settle and returns the rendered DOM in the usual response document. No browser:use scope is needed (the scope name still exists for compatibility).

Rendered fetch

Render the page in the managed browser instead of fetching it over plain HTTP. }, { name: "browser_fallback", type: "boolean", default: "true", description: <>Escalate to the browser automatically when an HTTP attempt is blocked by a JavaScript challenge or anti-bot page. Set false to never render. }, { name: "wait_until", type: '"load" | "domcontentloaded" | "networkidle"', default: '"domcontentloaded"', description: <>Navigation event to wait for. networkidle waits until no network request has been made for 500 ms; use it for pages that fetch their data after load. }, { name: "wait_for", type: "string (CSS selector)", constraints: "≤ 512 chars", description: <>Selector that must be present before capture, after wait_until. Fails with BROWSER_TIMEOUT if it never appears within the timeout. }, { name: "wait_ms", type: "integer (ms)", constraints: "0–30,000", description: <>Extra settle time after the wait condition and selector. Useful for pages that animate content in. }, { name: "javascript", type: "boolean", default: "true", description: <>Set false to render with scripting disabled (useful to bypass client-side redirects or paywalls that rely on JS). }, { name: "block_resources", type: "boolean", default: "true", description: <>Skip images, fonts and media. Saves bandwidth and time; scripts, stylesheets and XHR still load. }, { name: "screenshot", type: "boolean", default: "false", description: <>Return a PNG of the viewport in screenshot (base64). }, { name: "device", type: '"desktop" | "mobile" | "tablet"', description: <>mobile renders with an iPhone viewport and User-Agent; desktop (default) uses 1366 × 768. }, { name: "locale", type: "string", description: <>Sets the browser language and Accept-Language, e.g. {`"fr-CA"`}. }, ]} />

All other fetch fields apply unchanged: country, region, city, network, session, headers, cookies,{" "} format, links, timeout, retries and debug. method and body are ignored in the browser; navigation is always a{" "} GET.

Example

Render a dashboard behind a login, reusing the sticky session that holds the login cookies, wait for the results table, and return readable text plus a screenshot.

What is captured

  • DOM after settle. content is the serialised document (document.documentElement.outerHTML) once wait_until, wait_for and{" "} wait_ms are satisfied. format: text and markdown convert this rendered DOM, so client-side content is included.
  • Final URL. final_url reflects server redirects and client-side navigations (location.replace, meta refresh, framework routers) that happened before capture.
  • Status and headers of the main document. status and headers come from the main navigation response, not from sub-resources. A page that loads but shows an error inside the app still reports the document's status.
  • Cookies. cookies contains the browser's cookie jar for the site after rendering, including cookies set by JavaScript and challenge clearances. Replay them via the{" "} cookies field or keep the session.
  • Page metadata and links. page (title, description, canonical, lang, Open Graph, link count) and, with links: true, links[] are extracted from the rendered DOM.
  • Optional screenshot. A PNG of the viewport (1366 × 768 desktop, 390 × 844 mobile), base64-encoded in screenshot.

Captcha solving

Most JavaScript challenges clear on their own inside the real browser. When Cloudflare asks for an interactive Turnstile verification instead, Fetcha intercepts the widget parameters, obtains a token from a managed human-verification service (2captcha) and hands it to the page, adopting the user agent the token was issued for. This typically adds 10–40 seconds, so keep timeout generous (60 s or more) for hard targets. Set {`solve_captcha: false`} to opt out on a request. Attempts that needed a token report{" "} captcha_solved: true in metadata.debug.attempts[].

Automatic escalation

You rarely need to set browser: true yourself. With the default browser_fallback: true, Fetcha starts every request over plain HTTP because it is faster and cheaper. When an attempt is classified as a JavaScript challenge or an anti-bot interstitial (Cloudflare challenge or Turnstile, DataDome, PerimeterX, Akamai, Kasada, Imperva, AWS WAF, Vercel attack mode, or a soft 200 challenge page), the router escalates: it re-plays the request in the browser through the same route, letting the challenge script run and the clearance cookie be set.

  • Escalation counts as one attempt and shares the request's single timeout and retries budget.
  • metadata.mode is browser when the final attempt was rendered. With debug: true, each attempt lists its mode and, for blocked ones, the{" "} block_reason.
  • Plain 403/429 blocks without a JavaScript challenge are retried on a new IP or a premium route first; the browser is used when the block needs a script to be executed.
  • Set browser_fallback: false for latency-sensitive calls where a blocked answer is acceptable, or when you handle challenges yourself.
Escalations feed the per-domain profile like any other attempt. A site that consistently needs the browser will be rendered directly on later requests, saving the wasted HTTP attempt.

Limits and errors

Limit Value
Concurrent renders per organization 8
Render time Bounded by the request timeout (max 120 s), shared with any HTTP attempts made before escalation
Viewport 1366 × 768 desktop · 390 × 844 mobile
Response size Same 20 MB cap as plain fetches, applied to the serialised DOM. Screenshots are not counted.
Situation Result
Page did not reach wait_until, wait_for never appeared, or the render exceeded the remaining timeout 504 BROWSER_TIMEOUT
Browser pool disabled or unavailable 400 BROWSER_UNAVAILABLE
More than 8 renders in flight for the organization 429 CONCURRENCY_LIMIT
Target blocked even in the browser, on every attempt 200 with success: false and the last page, like any block

Rendered requests are logged like any other request: the request log shows mode per attempt, and bandwidth is metered on the bytes the browser actually transferred (which is why block_resources is on by default).

Tips

  • Prefer wait_for over a large wait_ms: it returns as soon as the content exists and fails clearly when it never does.
  • Use {`format: "markdown"`} or {`"text"`} with the browser to get the rendered content without shipping the framework's HTML.
  • Combine with a session for logged-in areas: the browser reuses the session's IP and the cookies you pass, and returns the updated jar.
  • If the site exposes the JSON endpoint the page calls, fetching it directly with {`format: "json"`} is still faster than rendering. See Examples.

Coming soon: browser actions

Multi-step interactions (click, type, scroll, evaluate) in one browser context are planned. POST /v1/browser currently returns 400 BROWSER_UNAVAILABLE. The design below is indicative; field names may change before launch. Follow the changelog. {ACTIONS.map(([name, params, desc]) => ( ))}
Action Parameters Description
{name} {params} {desc}
); }