TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { Metadata } from "next";2import { CodeBlock } from "@/components/ui/code-block";3import { DocPage } from "@/components/docs/doc-page";4import { A, Code, H2, H3, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose";5import { Callout, ComingSoon } from "@/components/docs/callout";6import { Endpoint } from "@/components/docs/endpoint";7import { ParamTable } from "@/components/docs/param-table";8import { CodeTabs } from "@/components/docs/code-tabs";9import { ResponseExample } from "@/components/docs/response-example";10import { fetchTabs } from "@/components/docs/snippets";1112export const metadata: Metadata = {13 title: "Browser",14 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.",15};1617const RENDERED = {18 request_id: "req_5d6e7f8g9h0i1j2k",19 success: true,20 status: 200,21 url: "https://app.example.com/dashboard",22 final_url: "https://app.example.com/dashboard?tab=results",23 content: null,24 content_type: "text/html; charset=utf-8",25 headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },26 cookies: [27 { name: "session", value: "9f3a…", domain: "app.example.com", path: "/" },28 { name: "cf_clearance", value: "…", domain: ".example.com", path: "/" },29 ],30 text: "Results\nRef\tPrice\nA-1041\tCA$ 1,250\n…",31 page: { title: "Dashboard — Results", description: null, canonical: "https://app.example.com/dashboard", lang: "en", og: {}, links_count: 64 },32 screenshot: "iVBORw0KGgoAAAANSUhEUgAA…",33 metadata: {34 network: "residential",35 country: "CA",36 mode: "browser",37 attempts: 1,38 duration_ms: 4930,39 bytes: 612340,40 session: "sess_8f2k1m9d3p7q4r6s",41 cached: false,42 timing: { dns_ms: 17, proxy_connect_ms: 0, tls_ms: 0, origin_ms: 4480, processing_ms: 61, total_ms: 4930 },43 },44};4546const ACTIONS: Array<[string, string, string]> = [47 ["goto", "{ url }", "Navigate to a URL and wait for the load event."],48 ["click", "{ selector }", "Click the first element matching a CSS selector."],49 ["type", "{ selector, text, delay? }", "Focus an element and type text, optionally with a per-key delay."],50 ["scroll", "{ selector? | y? }", "Scroll an element into view, or scroll the page by a number of pixels."],51 ["wait", "{ selector? | ms? }", "Wait for a selector to appear, or for a fixed duration."],52 ["evaluate", "{ script }", "Run JavaScript in the page and capture its return value."],53 ["screenshot", "{ full_page?, selector? }", "Capture a PNG of the viewport, the full page or one element."],54 ["extract", "{ schema }", "Return structured data from the rendered DOM (shares the Extraction schema format)."],55];5657export default function BrowserPage() {58 return (59 <DocPage path="/docs/browser" eyebrow="Core API" title="Browser" description="Render JavaScript-heavy pages in a managed headless Chromium that inherits Fetcha's routing, geography and sessions. Available on every fetch with browser: true, and used automatically when a plain request is blocked by a JavaScript challenge." status="Stable">60 <Endpoint method="POST" path="/v1/fetch { browser: true }" scope="fetch:execute" status="Live" />61 <P>62 Browser rendering is part of the fetch request, not a separate endpoint. The browser runs on Fetcha's infrastructure, connects through the <Strong>same proxy network, country and63 session</Strong> as a plain fetch, loads the page, waits for it to settle and returns the rendered DOM in the usual response document. No <Code>browser:use</Code> scope is needed (the scope64 name still exists for compatibility).65 </P>6667 <H2>Rendered fetch</H2>68 <ParamTable69 rows={[70 { name: "browser", type: "boolean", default: "false", description: <>Render the page in the managed browser instead of fetching it over plain HTTP.</> },71 { 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 <code>false</code> to never render.</> },72 { name: "wait_until", type: '"load" | "domcontentloaded" | "networkidle"', default: '"domcontentloaded"', description: <>Navigation event to wait for. <code>networkidle</code> waits until no network request has been made for 500 ms; use it for pages that fetch their data after load.</> },73 { name: "wait_for", type: "string (CSS selector)", constraints: "≤ 512 chars", description: <>Selector that must be present before capture, after <code>wait_until</code>. Fails with <code>BROWSER_TIMEOUT</code> if it never appears within the timeout.</> },74 { 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.</> },75 { name: "javascript", type: "boolean", default: "true", description: <>Set <code>false</code> to render with scripting disabled (useful to bypass client-side redirects or paywalls that rely on JS).</> },76 { name: "block_resources", type: "boolean", default: "true", description: <>Skip images, fonts and media. Saves bandwidth and time; scripts, stylesheets and XHR still load.</> },77 { name: "screenshot", type: "boolean", default: "false", description: <>Return a PNG of the viewport in <code>screenshot</code> (base64).</> },78 { name: "device", type: '"desktop" | "mobile" | "tablet"', description: <><code>mobile</code> renders with an iPhone viewport and User-Agent; <code>desktop</code> (default) uses 1366 × 768.</> },79 { name: "locale", type: "string", description: <>Sets the browser language and <code>Accept-Language</code>, e.g. <code>{`"fr-CA"`}</code>.</> },80 ]}81 />82 <P>83 All other fetch fields apply unchanged: <Code>country</Code>, <Code>region</Code>, <Code>city</Code>, <Code>network</Code>, <Code>session</Code>, <Code>headers</Code>, <Code>cookies</Code>,{" "}84 <Code>format</Code>, <Code>links</Code>, <Code>timeout</Code>, <Code>retries</Code> and <Code>debug</Code>. <Code>method</Code> and <Code>body</Code> are ignored in the browser; navigation is always a{" "}85 <Code>GET</Code>.86 </P>8788 <H3>Example</H3>89 <P>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.</P>90 <CodeTabs91 tabs={fetchTabs(92 { url: "https://app.example.com/dashboard", browser: true, wait_for: "table.results", wait_ms: 500, country: "CA", session: "sess_8f2k1m9d3p7q4r6s", format: "text", screenshot: true },93 {94 javascript: `console.log(data.metadata.mode, data.page.title); // "browser" "Dashboard — Results"\nawait fs.promises.writeFile("dashboard.png", Buffer.from(data.screenshot, "base64"));`,95 python: `print(data["metadata"]["mode"], data["page"]["title"])\nopen("dashboard.png", "wb").write(base64.b64decode(data["screenshot"]))`,96 },97 )}98 />99 <ResponseExample status={200} title="200 OK · metadata.mode: browser" body={RENDERED} />100101 <H2>What is captured</H2>102 <Ul>103 <Li>104 <Strong>DOM after settle.</Strong> <Code>content</Code> is the serialised document (<Code>document.documentElement.outerHTML</Code>) once <Code>wait_until</Code>, <Code>wait_for</Code> and{" "}105 <Code>wait_ms</Code> are satisfied. <Code>format: text</Code> and <Code>markdown</Code> convert this rendered DOM, so client-side content is included.106 </Li>107 <Li>108 <Strong>Final URL.</Strong> <Code>final_url</Code> reflects server redirects and client-side navigations (<Code>location.replace</Code>, meta refresh, framework routers) that happened before capture.109 </Li>110 <Li>111 <Strong>Status and headers of the main document.</Strong> <Code>status</Code> and <Code>headers</Code> come from the main navigation response, not from sub-resources. A page that loads but shows an112 error inside the app still reports the document's status.113 </Li>114 <Li>115 <Strong>Cookies.</Strong> <Code>cookies</Code> contains the browser's cookie jar for the site after rendering, including cookies set by JavaScript and challenge clearances. Replay them via the{" "}116 <Code>cookies</Code> field or keep the <Code>session</Code>.117 </Li>118 <Li>119 <Strong>Page metadata and links.</Strong> <Code>page</Code> (title, description, canonical, lang, Open Graph, link count) and, with <Code>links: true</Code>, <Code>links[]</Code> are extracted from the120 rendered DOM.121 </Li>122 <Li>123 <Strong>Optional screenshot.</Strong> A PNG of the viewport (1366 × 768 desktop, 390 × 844 mobile), base64-encoded in <Code>screenshot</Code>.124 </Li>125 </Ul>126127 <H2>Captcha solving</H2>128 <P>129 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,130 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 keep131 <Code>timeout</Code> generous (60 s or more) for hard targets. Set <Code>{`solve_captcha: false`}</Code> to opt out on a request. Attempts that needed a token report{" "}132 <Code>captcha_solved: true</Code> in <Code>metadata.debug.attempts[]</Code>.133 </P>134135 <H2>Automatic escalation</H2>136 <P>137 You rarely need to set <Code>browser: true</Code> yourself. With the default <Code>browser_fallback: true</Code>, Fetcha starts every request over plain HTTP because it is faster and cheaper.138 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 attack139 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.140 </P>141 <Ul>142 <Li>143 Escalation counts as one attempt and shares the request's single <Code>timeout</Code> and <Code>retries</Code> budget.144 </Li>145 <Li>146 <Code>metadata.mode</Code> is <Code>browser</Code> when the final attempt was rendered. With <Code>debug: true</Code>, each attempt lists its <Code>mode</Code> and, for blocked ones, the{" "}147 <Code>block_reason</Code>.148 </Li>149 <Li>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.</Li>150 <Li>151 Set <Code>browser_fallback: false</Code> for latency-sensitive calls where a blocked answer is acceptable, or when you handle challenges yourself.152 </Li>153 </Ul>154 <Callout variant="info" title="Learning">155 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.156 </Callout>157158 <H2>Limits and errors</H2>159 <Table dense>160 <THead>161 <Tr>162 <Th>Limit</Th>163 <Th>Value</Th>164 </Tr>165 </THead>166 <TBody>167 <Tr>168 <Td>Concurrent renders per organization</Td>169 <Td mono>8</Td>170 </Tr>171 <Tr>172 <Td>Render time</Td>173 <Td>174 Bounded by the request <Code>timeout</Code> (max 120 s), shared with any HTTP attempts made before escalation175 </Td>176 </Tr>177 <Tr>178 <Td>Viewport</Td>179 <Td mono>1366 × 768 desktop · 390 × 844 mobile</Td>180 </Tr>181 <Tr>182 <Td>Response size</Td>183 <Td>Same 20 MB cap as plain fetches, applied to the serialised DOM. Screenshots are not counted.</Td>184 </Tr>185 </TBody>186 </Table>187 <Table dense>188 <THead>189 <Tr>190 <Th>Situation</Th>191 <Th>Result</Th>192 </Tr>193 </THead>194 <TBody>195 <Tr>196 <Td>197 Page did not reach <Code>wait_until</Code>, <Code>wait_for</Code> never appeared, or the render exceeded the remaining <Code>timeout</Code>198 </Td>199 <Td mono>504 BROWSER_TIMEOUT</Td>200 </Tr>201 <Tr>202 <Td>Browser pool disabled or unavailable</Td>203 <Td mono>400 BROWSER_UNAVAILABLE</Td>204 </Tr>205 <Tr>206 <Td>More than 8 renders in flight for the organization</Td>207 <Td mono>429 CONCURRENCY_LIMIT</Td>208 </Tr>209 <Tr>210 <Td>Target blocked even in the browser, on every attempt</Td>211 <Td>212 <Code>200</Code> with <Code>success: false</Code> and the last page, like any block213 </Td>214 </Tr>215 </TBody>216 </Table>217 <P>218 Rendered requests are logged like any other request: the request log shows <Strong>mode</Strong> per attempt, and bandwidth is metered on the bytes the browser actually transferred (which is219 why <Code>block_resources</Code> is on by default).220 </P>221222 <H2>Tips</H2>223 <Ul>224 <Li>225 Prefer <Code>wait_for</Code> over a large <Code>wait_ms</Code>: it returns as soon as the content exists and fails clearly when it never does.226 </Li>227 <Li>228 Use <Code>{`format: "markdown"`}</Code> or <Code>{`"text"`}</Code> with the browser to get the rendered content without shipping the framework's HTML.229 </Li>230 <Li>231 Combine with a <A href="/docs/sessions">session</A> for logged-in areas: the browser reuses the session's IP and the cookies you pass, and returns the updated jar.232 </Li>233 <Li>234 If the site exposes the JSON endpoint the page calls, fetching it directly with <Code>{`format: "json"`}</Code> is still faster than rendering. See <A href="/docs/examples">Examples</A>.235 </Li>236 </Ul>237238 <H2>Coming soon: browser actions</H2>239 <ComingSoon title="POST /v1/browser is not yet available">240 Multi-step interactions (click, type, scroll, evaluate) in one browser context are planned. <Code>POST /v1/browser</Code> currently returns <Code>400 BROWSER_UNAVAILABLE</Code>. The design below241 is indicative; field names may change before launch. Follow the <A href="/changelog">changelog</A>.242 </ComingSoon>243 <Endpoint method="POST" path="/v1/browser" status="Coming soon" />244 <Table>245 <THead>246 <Tr>247 <Th>Action</Th>248 <Th>Parameters</Th>249 <Th>Description</Th>250 </Tr>251 </THead>252 <TBody>253 {ACTIONS.map(([name, params, desc]) => (254 <Tr key={name}>255 <Td mono>{name}</Td>256 <Td mono>{params}</Td>257 <Td>{desc}</Td>258 </Tr>259 ))}260 </TBody>261 </Table>262 <CodeBlock263 lang="json"264 title="Planned request"265 code={JSON.stringify(266 {267 url: "https://www.example.ca/login",268 country: "CA",269 session: "sess_8f2k1m9d3p7q4r6s",270 actions: [271 { type: "type", selector: "#email", text: "me@example.com" },272 { type: "type", selector: "#password", text: "•••••••" },273 { type: "click", selector: "button[type=submit]" },274 { type: "wait", selector: ".account-summary" },275 { type: "screenshot", full_page: false },276 { type: "extract", schema: { balance: "string", plan: "string" } },277 ],278 },279 null,280 2,281 )}282 />283 </DocPage>284 );285}286