How do I add a backup to Oxylabs' new Web API?
Keep Oxylabs for scale and send UpRock the requests its Web API gives up on, like a timeout, a rate limit or a 500 after its own retries, which Oxylabs doesn't charge for. Respect the rest, because a 400 can mean a restricted target and a 403 means your plan can't use it.
Updated · Read as markdown
What Oxylabs does well
Oxylabs is one of the largest proxy and web data providers, claiming 177 million IPs across 195 locations. On 7 October 2026 it launched a new Web API, with /scrape and /search endpoints behind one Bearer key and one shared balance across all its products; the older Web Scraper API is now "Classic". The new /scrape handles rendering, proxy rotation and access challenges and returns markdown, HTML, AI-parsed JSON or screenshots. Around it: residential and datacenter proxies, Web Unblocker, AI Studio tools, OxyCopilot for building parsers, a free scheduler on Classic, and MCP servers for both APIs.
For large, long-running data jobs with an account team behind them, Oxylabs is built for it. Keep it.
What a generic page costs now
Oxylabs' new pricing page leads with "/Scrape from $0.50/1K results". That figure is the Amazon row. For any other site ("custom domain scraping"), these are the prices per 1,000 pages, checked 21 Oct 2026:
| Plan | Monthly | Without JavaScript | With JavaScript |
|---|---|---|---|
| Free | $5 of welcome credit | $1.00 | $1.50 |
| Starter | $30 | $1.00 | $1.50 |
| Basic | $100 | $0.90 | $1.40 |
| Flexible | $500 | $0.80 | $1.30 |
| Enterprise | from $2,500 | $0.75 | $1.22 |
| UpRock | prepaid credits | $0.50 | $0.50 |
AI parsing adds $3 per 1,000 parsed pages. Unused balance expires at the end of each billing cycle, the service pauses when the balance runs out, and self-service purchases are non-refundable.
Reading a Web API failure
The new Web API doesn't return the target page's own status code. It answers with its own HTTP status, and its docs say which ones are worth retrying:
| Oxylabs status | What it means | What to do |
|---|---|---|
200, with results[0].markdown | It got the page | Use it |
| 408 | The realtime request timed out | Send to UpRock |
| 429 | Rate limit, or your balance ran out | Send to UpRock |
500 (state: faulted) | It failed after too many retries; not charged | Send to UpRock |
| 503 | The service is unavailable | Send to UpRock |
| 401 | A missing or invalid key | Fix the key (the code sends it to UpRock meanwhile) |
| 400 | A bad request body, or a restricted target | Respect it: fix the request, or leave the target alone |
| 403 | Your plan isn't entitled to this target or source | Respect it |
Oxylabs and UpRock side by side
| Oxylabs | UpRock | |
|---|---|---|
| Price per 1,000 generic pages | $0.75 to $1.50 (table above) | $0.50 by card or USDC, $0.40 with UPT |
| Failed requests | Not charged (faulted); the docs don't say about requests you give up on | Not documented yet |
| Residential proxies | $6 to $2.50 per GB | Included: every page is one credit |
| Where requests come from | Residential IPs mostly from providers such as Honeygain, whose users consent and are paid | Devices whose owners opted in |
| Countries | 195 locations claimed; country code on the Web API, city and coordinates for some Classic sources | 63, or a region |
| Who can sign up | Everyone answers a KYC questionnaire; a quarter or more of enquiries are rejected | Anyone with an API key |
| Search, parsing, scheduling | Yes | No. One page per call |
| MCP server | For the Web API (self-hosted) and a hosted one for Classic | Yes, remote (streamable HTTP) |
| Video | YouTube, TikTok and Bilibili downloads and metadata ($2 per GB) | Search public video by moment, $1 per 1,000 searches |
Where Oxylabs is better: scale and coverage, finer location targeting on some sources, search and parsing in the same API, and a large company behind it. Where UpRock fits: the requests the Web API gives up on, a lower price per generic page, no KYC, and searching inside video rather than downloading it.
The fallback, in TypeScript and Python
Every version call webapi.oxylabs.io/v1/scrape for markdown with JavaScript in the country you pass, with the 150-second timeout Oxylabs suggests. A 200 with markdown is used; 408, 429, 500 and 503 (and 401, so a bad key doesn't stop your agent) go to UpRock; 400, 403 and 404 raise an error. They call the HTTP API directly, because Oxylabs' Python SDK covers only the Classic API and there's no official Node SDK. UpRock gets one POST /v1/crawl request with your key in an Authorization: Bearer header; the MCP and CLI tabs do the same through UpRock's MCP server (TypeScript) or CLI (Python).
// Node 18+ env: OXYLABS_WEB_API_KEY, UPROCK_API_KEY// Oxylabs Web API (launched 7 Oct 2026). There's no official Node SDK for it, so this calls// the HTTP API directly.// Statuses worth sending to a second network: Oxylabs timed out (408), you hit a rate limit// or ran out of balance (429), it gave up after its own retries (500, not charged), or it's// unavailable (503). 401 means a bad key; a fallback keeps your agent working meanwhile.// 400 (bad request, or a restricted target), 403 (your plan can't use this target) and// 404 (unknown route) are answers: don't route around them.const TRY_UPROCK = new Set([401, 408, 429, 500, 503]);async function viaUpRock(url: string, country: string) { // One call: UpRock loads the page on a real device in that country, tries a fresh device // every 7 s (up to 3 tries) and returns markdown. Failed calls aren't billed. const res = await fetch("https://edge.uprock.com/v1/crawl", { method: "POST", headers: { Authorization: `Bearer ${process.env.UPROCK_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url, country }), signal: AbortSignal.timeout(90_000), }); const page = await res.json(); if (!res.ok) throw new Error(`UpRock ${res.status} ${page.code}: ${page.error}`); return page.markdown ?? "";}export async function read(url: string, country = "US") { try { const res = await fetch("https://webapi.oxylabs.io/v1/scrape", { method: "POST", headers: { Authorization: `Bearer ${process.env.OXYLABS_WEB_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url, output: ["markdown"], location: country, run_js: true, }), signal: AbortSignal.timeout(150_000), // Oxylabs suggests 150 s }); if (res.ok) { const markdown = (await res.json()).results?.[0]?.markdown; if (markdown) return { via: "oxylabs", markdown }; console.warn("Oxylabs returned no markdown; trying UpRock"); } else if (TRY_UPROCK.has(res.status)) { console.warn(`Oxylabs ${res.status}; trying UpRock`); } else { throw new Error(`Oxylabs ${res.status}: ${(await res.text()).slice(0, 200)}`); } } catch (err) { if ((err as Error).message.startsWith("Oxylabs ")) throw err; console.warn( `Oxylabs unreachable or timed out (${(err as Error).message}); trying UpRock`, ); } return { via: "uprock", markdown: await viaUpRock(url, country) };}// node read.ts https://developer.mozilla.org/en-US/docs/Web/HTTP DEconst [url = "https://developer.mozilla.org/en-US/docs/Web/HTTP", country = "US"] = process.argv.slice(2);const page = await read(url, country);console.log(page.via, page.markdown.slice(0, 200));# pip install requests env: OXYLABS_WEB_API_KEY, UPROCK_API_KEY# Oxylabs Web API (launched 7 Oct 2026). The official Python SDK covers only the Classic# Web Scraper API, so this calls the HTTP API directly.import osimport sysimport requests# Statuses worth sending to a second network: Oxylabs timed out (408), you hit a rate limit# or ran out of balance (429), it gave up after its own retries (500, not charged), or it's# unavailable (503). 401 means a bad key; a fallback keeps your agent working meanwhile.# 400 (bad request, or a restricted target), 403 (your plan can't use this target) and# 404 (unknown route) are answers: don't route around them.TRY_UPROCK = {401, 408, 429, 500, 503}def via_uprock(url: str, country: str) -> str: # One call: UpRock loads the page on a real device in that country, tries a fresh device # every 7 s (up to 3 tries) and returns markdown. Failed calls aren't billed. res = requests.post( "https://edge.uprock.com/v1/crawl", headers={"Authorization": f"Bearer {os.environ['UPROCK_API_KEY']}"}, json={"url": url, "country": country}, timeout=90, ) page = res.json() if not res.ok: raise RuntimeError(f"UpRock {res.status_code} {page.get('code')}: {page.get('error')}") return page.get("markdown", "")def read(url: str, country: str = "US") -> dict: try: res = requests.post( "https://webapi.oxylabs.io/v1/scrape", headers={ "Authorization": f"Bearer {os.environ.get('OXYLABS_WEB_API_KEY')}" }, json={ "url": url, "output": ["markdown"], "location": country, "run_js": True, }, timeout=150, # Oxylabs suggests 150 s ) except requests.RequestException as err: print( f"Oxylabs unreachable or timed out ({err}); trying UpRock", file=sys.stderr ) else: if res.ok: results = res.json().get("results") or [{}] if results[0].get("markdown"): return {"via": "oxylabs", "markdown": results[0]["markdown"]} print("Oxylabs returned no markdown; trying UpRock", file=sys.stderr) elif res.status_code in TRY_UPROCK: print(f"Oxylabs {res.status_code}; trying UpRock", file=sys.stderr) else: raise RuntimeError(f"Oxylabs {res.status_code}: {res.text[:200]}") return {"via": "uprock", "markdown": via_uprock(url, country)}if __name__ == "__main__": # python read.py https://developer.mozilla.org/en-US/docs/Web/HTTP DE url = sys.argv[1] if len(sys.argv) > 1 else "https://developer.mozilla.org/en-US/docs/Web/HTTP" country = sys.argv[2] if len(sys.argv) > 2 else "US" page = read(url, country) print(page["via"], page["markdown"][:200])// npm i @modelcontextprotocol/sdk (Node 18+) env: OXYLABS_WEB_API_KEY, UPROCK_API_KEY// Oxylabs Web API (launched 7 Oct 2026). There's no official Node SDK for it, so this calls// the HTTP API directly.import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";const uprock = new Client({ name: "oxylabs-fallback", version: "1.0.0" });await uprock.connect( new StreamableHTTPClientTransport( new URL("https://mcp.uprock.ai/mcp"), { requestInit: { headers: { Authorization: `Bearer ${process.env.UPROCK_API_KEY}` } } }, ),);// Statuses worth sending to a second network: Oxylabs timed out (408), you hit a rate limit// or ran out of balance (429), it gave up after its own retries (500, not charged), or it's// unavailable (503). 401 means a bad key; a fallback keeps your agent working meanwhile.// 400 (bad request, or a restricted target), 403 (your plan can't use this target) and// 404 (unknown route) are answers: don't route around them.const TRY_UPROCK = new Set([401, 408, 429, 500, 503]);const textOf = (content: unknown) => (content as { type: string; text?: string }[]).find((c) => c.type === "text")?.text ?? "";async function viaUpRock(url: string, country: string) { const res = await uprock.callTool( { name: "crawl_fetch", arguments: { url, country } }, undefined, { timeout: 300_000 }, ); if (res.isError) throw new Error(`UpRock: ${textOf(res.content)}`); const uri = JSON.parse(textOf(res.content)).content?.markdown?.resource; if (!uri) throw new Error(`UpRock: no markdown for ${url}`); return textOf( (await uprock.callTool({ name: "resource_fetch", arguments: { uri } })).content, );}export async function read(url: string, country = "US") { try { const res = await fetch("https://webapi.oxylabs.io/v1/scrape", { method: "POST", headers: { Authorization: `Bearer ${process.env.OXYLABS_WEB_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url, output: ["markdown"], location: country, run_js: true, }), signal: AbortSignal.timeout(150_000), // Oxylabs suggests 150 s }); if (res.ok) { const markdown = (await res.json()).results?.[0]?.markdown; if (markdown) return { via: "oxylabs", markdown }; console.warn("Oxylabs returned no markdown; trying UpRock"); } else if (TRY_UPROCK.has(res.status)) { console.warn(`Oxylabs ${res.status}; trying UpRock`); } else { throw new Error(`Oxylabs ${res.status}: ${(await res.text()).slice(0, 200)}`); } } catch (err) { if ((err as Error).message.startsWith("Oxylabs ")) throw err; console.warn( `Oxylabs unreachable or timed out (${(err as Error).message}); trying UpRock`, ); } return { via: "uprock", markdown: await viaUpRock(url, country) };}// node read.ts https://developer.mozilla.org/en-US/docs/Web/HTTP DEconst [url = "https://developer.mozilla.org/en-US/docs/Web/HTTP", country = "US"] = process.argv.slice(2);try { const page = await read(url, country); console.log(page.via, page.markdown.slice(0, 200));} finally { await uprock.close();}# pip install requests; npm i -g uprock env: OXYLABS_WEB_API_KEY, UPROCK_API_KEY# Oxylabs Web API (launched 7 Oct 2026). The official Python SDK covers only the Classic# Web Scraper API, so this calls the HTTP API directly.import jsonimport osimport subprocessimport sysimport requests# Statuses worth sending to a second network: Oxylabs timed out (408), you hit a rate limit# or ran out of balance (429), it gave up after its own retries (500, not charged), or it's# unavailable (503). 401 means a bad key; a fallback keeps your agent working meanwhile.# 400 (bad request, or a restricted target), 403 (your plan can't use this target) and# 404 (unknown route) are answers: don't route around them.TRY_UPROCK = {401, 408, 429, 500, 503}def via_uprock(url: str, country: str) -> str: run = subprocess.run( ["uprock", "ai", "crawl", url, "--country", country, "--content"], capture_output=True, text=True, timeout=330, ) if run.returncode != 0: raise RuntimeError(f"UpRock: {run.stderr.strip()}") return json.loads(run.stdout).get("inlined_markdown", "")def read(url: str, country: str = "US") -> dict: try: res = requests.post( "https://webapi.oxylabs.io/v1/scrape", headers={ "Authorization": f"Bearer {os.environ.get('OXYLABS_WEB_API_KEY')}" }, json={ "url": url, "output": ["markdown"], "location": country, "run_js": True, }, timeout=150, # Oxylabs suggests 150 s ) except requests.RequestException as err: print( f"Oxylabs unreachable or timed out ({err}); trying UpRock", file=sys.stderr ) else: if res.ok: results = res.json().get("results") or [{}] if results[0].get("markdown"): return {"via": "oxylabs", "markdown": results[0]["markdown"]} print("Oxylabs returned no markdown; trying UpRock", file=sys.stderr) elif res.status_code in TRY_UPROCK: print(f"Oxylabs {res.status_code}; trying UpRock", file=sys.stderr) else: raise RuntimeError(f"Oxylabs {res.status_code}: {res.text[:200]}") return {"via": "uprock", "markdown": via_uprock(url, country)}if __name__ == "__main__": # python read.py https://developer.mozilla.org/en-US/docs/Web/HTTP DE url = sys.argv[1] if len(sys.argv) > 1 else "https://developer.mozilla.org/en-US/docs/Web/HTTP" country = sys.argv[2] if len(sys.argv) > 2 else "US" page = read(url, country) print(page["via"], page["markdown"][:200])Tested 21 Oct 2026 against the live UpRock API with an invalid Oxylabs key: the Web API answered 401 with an empty body, as its docs describe, and both versions fell back to UpRock. Without an account, the routing of the other statuses follows Oxylabs' error catalog. On 6 Nov 2026 we ran the HTTP and MCP versions again with an UpRock API key in an Authorization header: with an invalid Oxylabs key, each fell back to UpRock and got the page.
FAQ
What does Oxylabs charge for an ordinary web page?
On its new Web API, a generic page ("custom domain scraping") costs $1.00 per 1,000 without JavaScript and $1.50 with it on the Starter plan, down to $0.75 and $1.22 on Enterprise. The "from $0.50 per 1,000" headline on its pricing page matches the Amazon rows, not generic pages.
Does Oxylabs charge for failed requests?
Not on the new Web API, whose docs say faulted scrapes don't consume quota. Its docs don't say whether a request you give up on yourself is billed, so give it the 150 seconds Oxylabs suggests rather than a short timeout.
Aren't Oxylabs' residential IPs also people's devices?
Yes. Oxylabs' handbook says most of its pool comes from providers such as Honeygain, whose users consent and are paid. UpRock's devices also belong to people who opted in and earn UPT. They're separate networks, which is what makes one a backup for the other.
Can anyone sign up for Oxylabs?
Oxylabs says every customer answers a KYC questionnaire and that at least a quarter of enquiries are rejected. Some targets are restricted and answer 400, and some open up after a short KYC. UpRock needs only an API key and fetches public pages only.
Which country options does each offer?
Oxylabs claims IPs in 195 locations; its new Web API takes a two-letter country code, and the Classic API also takes cities, states and coordinates for some sources. UpRock covers 63 countries, or a region.
Other use cases to explore
- Compare two networks of opted-in devices: what's actually different when both providers run on consenting people's devices.
- Retry the jobs a batch gives up on: poll an async batch, keep what finished, and send the rest to a second network.
- See a page as it looks in another country: one store page from five countries, with five prices, two languages and a cookie banner in one country only.
- Search public video by moment: find the second something is shown or said, across platforms.
References
- Oxylabs, Web API scrape pricing, checked 21 Oct 2026
- Oxylabs docs, Changelog (Web API launch, 7 Oct 2026), checked 21 Oct 2026
- Oxylabs docs, Web API /scrape, checked 21 Oct 2026
- Oxylabs docs, Errors and status codes, checked 21 Oct 2026
- Oxylabs docs, Credit-based billing, checked 21 Oct 2026
- Oxylabs, Residential proxy pricing, checked 21 Oct 2026
- Oxylabs, Residential Proxy Pool handbook (how IPs are sourced), checked 21 Oct 2026
- Oxylabs, KYC and safety, checked 21 Oct 2026
- Oxylabs docs, Restricted targets (Web Scraper API Classic), checked 21 Oct 2026
- Oxylabs docs, MCP, checked 21 Oct 2026
- UpRock, everything in one file (API, CLI, MCP, pricing), checked 21 Oct 2026
Get started
Add UpRock in one paste.
Paste this into Claude Code, Codex, Cursor or any agent with a terminal. It reads our docs, sets up the MCP server or CLI, and tells you the cost before it spends anything.
Set up UpRock for me: read https://uprock.ai/agents.md, connect the UpRock MCP server, and run one test crawl. Tell me what it costs before you spend anything.