import type { Metadata } from "next"; import { DocPage } from "@/components/docs/doc-page"; import { A, Code, H2, Li, P, Strong, Ul } from "@/components/docs/prose"; import { Callout } from "@/components/docs/callout"; import { CodeTabs } from "@/components/docs/code-tabs"; export const metadata: Metadata = { title: "Examples", description: "Eight practical recipes: text extraction, JSON APIs through a Canadian residential IP, sticky login flows, POST bodies, headers and cookies, handling blocks, concurrency and debugging.", }; const R1_JS = `import { Fetcha } from "@fetcha/sdk"; const fetcha = new Fetcha({ apiKey: process.env.FETCHA_API_KEY! }); const r = await fetcha.fetch({ url: "https://www.example.ca/products/42", country: "CA", format: "text", }); if (!r.success) throw new Error(\`target answered \${r.status} after \${r.metadata.attempts} attempts\`); console.log(r.text); // readable text: scripts, styles and tags removed console.log(r.headers["last-modified"]);`; const R1_PY = `from fetcha import Fetcha client = Fetcha(api_key=os.environ["FETCHA_API_KEY"]) r = client.fetch("https://www.example.ca/products/42", country="CA", format="text") if not r.success: raise RuntimeError(f"target answered {r.status} after {r.metadata['attempts']} attempts") print(r.text) # readable text print(r.headers.get("last-modified"))`; const R2_JS = `type Listing = { id: string; price: number; currency: string }; const data = await fetcha.json<{ items: Listing[] }>( "https://api.example.ca/v2/listings?city=montreal&page=1", { country: "CA", network: "residential", headers: { Accept: "application/json" } }, ); for (const item of data.items) console.log(item.id, item.price, item.currency);`; const R2_PY = `data = client.json( "https://api.example.ca/v2/listings?city=montreal&page=1", country="CA", network="residential", headers={"Accept": "application/json"}, ) for item in data["items"]: print(item["id"], item["price"], item["currency"])`; const R3_JS = `// 1. One session for the whole flow (15 minutes, Canadian exit) const session = await fetcha.sessions.create({ country: "CA", ttl: 900, label: "login:user-42" }, "login:user-42"); // 2. Log in. No retries: a replayed login POST could lock the account. const login = await fetcha.fetch({ url: "https://www.example.ca/login", method: "POST", session: session.id, headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "email=me%40example.com&password=secret", retries: 0, }); if (!login.success) throw new Error(\`login failed with \${login.status}\`); // 3. Carry the cookies forward and read the protected page with the same identity const jar = Object.fromEntries(login.cookies.map((c) => [c.name, c.value])); const account = await fetcha.fetch({ url: "https://www.example.ca/account", session: session.id, cookies: jar, format: "text", }); console.log(account.text); await fetcha.sessions.close(session.id);`; const R3_PY = `# 1. One session for the whole flow (15 minutes, Canadian exit) session = client.sessions.create(country="CA", ttl=900, label="login:user-42") # 2. Log in. No retries: a replayed login POST could lock the account. login = client.fetch( "https://www.example.ca/login", method="POST", session=session.id, headers={"Content-Type": "application/x-www-form-urlencoded"}, body="email=me%40example.com&password=secret", retries=0, ) if not login.success: raise RuntimeError(f"login failed with {login.status}") # 3. Carry the cookies forward and read the protected page with the same identity jar = {c["name"]: c["value"] for c in login.cookies} account = client.fetch("https://www.example.ca/account", session=session.id, cookies=jar, format="text") print(account.text) client.sessions.close(session.id)`; const R4_JS = `const r = await fetcha.fetch({ url: "https://api.example.com/v1/search", method: "POST", // Objects are JSON-serialised; Content-Type defaults to application/json body: { query: "standing desk", filters: { in_stock: true, max_price: 500 }, page: 1 }, format: "json", retries: 0, // POST: decide about retries yourself }); if (r.success && r.json !== undefined) { console.log((r.json as { total: number }).total); } else { console.log("status", r.status, "body", r.content?.slice(0, 300)); }`; const R4_PY = `r = client.fetch( "https://api.example.com/v1/search", method="POST", # dicts are JSON-serialised; Content-Type defaults to application/json body={"query": "standing desk", "filters": {"in_stock": True, "max_price": 500}, "page": 1}, format="json", retries=0, # POST: decide about retries yourself ) if r.success and r.json is not None: print(r.json["total"]) else: print("status", r.status, "body", (r.content or "")[:300])`; const R5_JS = `const r = await fetcha.fetch({ url: "https://www.example.com/pricing", headers: { // Overrides Fetcha's default desktop Chrome UA "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36", Referer: "https://www.google.com/", "X-Requested-With": "XMLHttpRequest", }, // Sent as a single Cookie header, appended to any Cookie header above cookies: { currency: "CAD", consent: "1" }, locale: "fr-CA", // Accept-Language device: "mobile", // iPhone UA unless you set User-Agent yourself (as above) }); console.log(r.status, r.cookies); // cookies the target set in return`; const R5_PY = `r = client.fetch( "https://www.example.com/pricing", headers={ # Overrides Fetcha's default desktop Chrome UA "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36", "Referer": "https://www.google.com/", "X-Requested-With": "XMLHttpRequest", }, # Sent as a single Cookie header, appended to any Cookie header above cookies={"currency": "CAD", "consent": "1"}, locale="fr-CA", # Accept-Language device="mobile", # iPhone UA unless you set User-Agent yourself (as above) ) print(r.status, r.cookies) # cookies the target set in return`; const R6_JS = `import { Fetcha, FetchaError } from "@fetcha/sdk"; async function fetchOrExplain(url: string) { try { const r = await fetcha.fetch({ url, country: "US", debug: true }); if (r.success) return r; // Fetcha reached the target but every attempt was blocked (or the origin returned an error). const blocked = r.metadata.attempts > 1 || [403, 429].includes(r.status); if (blocked) { console.warn(\`blocked: status \${r.status} after \${r.metadata.attempts} attempts\`, r.metadata.debug); // Options: retry later, try another country, or a fresh session. return await fetcha.fetch({ url, country: "CA" }); } console.warn(\`origin error \${r.status}\`); // plain 404/500: not a block, not retried return r; } catch (e) { if (e instanceof FetchaError && e.code === "TARGET_TIMEOUT") { console.warn("timed out across all attempts", e.requestId); return null; } throw e; } }`; const R6_PY = `from fetcha import FetchaError def fetch_or_explain(url: str): try: r = client.fetch(url, country="US", debug=True) except FetchaError as e: if e.code == "TARGET_TIMEOUT": print("timed out across all attempts", e.request_id) return None raise if r.success: return r # Fetcha reached the target but every attempt was blocked (or the origin returned an error). blocked = r.metadata["attempts"] > 1 or r.status in (403, 429) if blocked: print(f"blocked: status {r.status} after {r.metadata['attempts']} attempts", r.metadata.get("debug")) # Options: retry later, try another country, or a fresh session. return client.fetch(url, country="CA") print(f"origin error {r.status}") # plain 404/500: not a block, not retried return r`; const R7_JS = `// Plan concurrency: Free 5, Developer 25, Growth 100, Business 500. // Read it once from the API instead of hardcoding it. const usage = (await fetcha.usage.current()) as { plan: { concurrency: number } }; const limit = Math.max(1, usage.plan.concurrency - 1); // leave one slot for other workers async function mapWithConcurrency(items: T[], n: number, fn: (t: T) => Promise): Promise { const out: R[] = new Array(items.length); let i = 0; await Promise.all( Array.from({ length: n }, async () => { while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx]!); } }), ); return out; } const urls = ["https://example.com/1", "https://example.com/2" /* … */]; const results = await mapWithConcurrency(urls, limit, async (url) => { try { return await fetcha.fetch({ url, country: "CA", format: "text" }); } catch (e) { if (e instanceof FetchaError && e.code === "CONCURRENCY_LIMIT") { // Another process is using slots: wait briefly and try once more await new Promise((r) => setTimeout(r, 500)); return await fetcha.fetch({ url, country: "CA", format: "text" }); } throw e; } });`; const R7_PY = `from concurrent.futures import ThreadPoolExecutor import time from fetcha import FetchaError # Plan concurrency: Free 5, Developer 25, Growth 100, Business 500. limit = max(1, client.usage()["plan"]["concurrency"] - 1) # leave one slot for other workers def one(url: str): for attempt in range(2): try: return client.fetch(url, country="CA", format="text") except FetchaError as e: if e.code == "CONCURRENCY_LIMIT" and attempt == 0: time.sleep(0.5) # another process is using slots continue raise urls = ["https://example.com/1", "https://example.com/2"] # … with ThreadPoolExecutor(max_workers=limit) as pool: results = list(pool.map(one, urls))`; const R8_JS = `const r = await fetcha.fetch({ url: "https://www.example.com/", country: "US", debug: true }); const m = r.metadata; console.table({ network: m.network, // class that served the final attempt country: m.country, attempts: m.attempts, // > 1 means a retry or escalation happened duration_ms: m.duration_ms, // whole request, all attempts bytes: m.bytes, // all attempts; basis for bandwidth pricing origin_ms: m.timing?.origin_ms, // time to first byte on the final attempt processing_ms: m.timing?.processing_ms, }); // One line per attempt: route alias, outcome, target status, duration for (const a of (m.debug as { attempts: Array> } | undefined)?.attempts ?? []) { console.log(a.provider, a.network, a.outcome, a.status, \`\${a.duration_ms}ms\`); } // e.g. network-a residential blocked 403 1210ms // network-b residential success 200 1888ms console.log("request id for support:", r.request_id);`; const R8_PY = `r = client.fetch("https://www.example.com/", country="US", debug=True) m = r.metadata print({ "network": m["network"], # class that served the final attempt "country": m["country"], "attempts": m["attempts"], # > 1 means a retry or escalation happened "duration_ms": m["duration_ms"], # whole request, all attempts "bytes": m["bytes"], # all attempts; basis for bandwidth pricing "origin_ms": m["timing"]["origin_ms"], "processing_ms": m["timing"]["processing_ms"], }) # One line per attempt: route alias, outcome, target status, duration for a in m.get("debug", {}).get("attempts", []): print(a["provider"], a["network"], a["outcome"], a["status"], f"{a['duration_ms']}ms") # e.g. network-a residential blocked 403 1210ms # network-b residential success 200 1888ms print("request id for support:", r.request_id)`; function Recipe({ title, id, intro, js, py, children }: { title: string; id: string; intro: React.ReactNode; js: string; py: string; children?: React.ReactNode }) { return (

{title}

{intro}

{children}
); } export default function ExamplesPage() { return (

The snippets assume a client created as in the SDKs page: const fetcha = new Fetcha({"{ apiKey }"}) or client = Fetcha(api_key=…). The SDKs are installed from source for now.

The most common call. {`format: "text"`} strips scripts, styles and markup and returns readable text in text, which is what you want for indexing, summarising or feeding a model. } js={R1_JS} py={R1_PY} /> Many sites load their data from a JSON endpoint. Call it directly with {`format: "json"`} and an explicit country; the json helper returns the parsed value. Pinning {`network: "residential"`} is optional today since auto resolves to it, but makes the intent explicit. } js={R2_JS} py={R2_PY} > If the endpoint returns something that is not JSON (an HTML error page, for instance), json is undefined/None. Use fetch instead of{" "} json when you need status and content to diagnose it. Two requests that must look like one visitor: a POST to log in, then a GET of a protected page. A session keeps the exit IP stable; the cookies from the first response are replayed in the second. The login uses retries: 0 because replaying a credential POST is rarely what you want. } js={R3_JS} py={R3_PY} /> Pass an object as body and Fetcha serialises it as JSON with Content-Type: application/json. Pass a string to send anything else (form-encoded, XML, GraphQL) and set the content type yourself. Bodies are ignored for GET and HEAD. } js={R4_JS} py={R4_PY} /> Your headers override Fetcha's defaults one by one. cookies is a convenience over the Cookie header, locale sets{" "} Accept-Language, and {`device: "mobile"`} switches to an iPhone User-Agent unless you provide your own. } js={R5_JS} py={R5_PY} /> A fully blocked target is not an exception: Fetcha returns 200 with success: false, the last blocked page and metadata.attempts{" "} greater than one (the request log records it as TARGET_BLOCKED). Distinguish it from a plain origin error, then decide: retry later, change geography or open a fresh session. } js={R6_JS} py={R6_PY} /> Concurrency is enforced per organization (5 / 25 / 100 / 500 in-flight requests depending on plan). Run a worker pool sized just below the limit instead of firing everything with a bare{" "} Promise.all, and treat CONCURRENCY_LIMIT as “wait for a slot” rather than a failure. See Rate limits. } js={R7_JS} py={R7_PY} >
  • The plan concurrency comes from GET /v1/usage, so the pool adapts when you upgrade.
  • If several services share one organization, split the budget between them or centralise fetching behind a queue.
debug: true adds a per-attempt trace to metadata.debug.attempts. Combined with timing, bytes and attempts it explains why a request was slow or expensive, and request_id lets support see the same trace on their side. } js={R8_JS} py={R8_PY} />
); }