# 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 21 Oct 2026. Web page: https://uprock.ai/compare/oxylabs

## On this page

- [What Oxylabs does well](#what-oxylabs-does-well)
- [What a generic page costs now](#what-a-generic-page-costs-now)
- [Reading a Web API failure](#reading-a-web-api-failure)
- [Oxylabs and UpRock side by side](#oxylabs-and-uprock-side-by-side)
- [The fallback, in TypeScript and Python](#the-fallback-in-typescript-and-python)
- [FAQ](#faq)
- [Other use cases to explore](#explore)
- [Related articles](#related)
- [References](#references)
- [Get started](#get-started)

## 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](https://oxylabs.io/products/web-api/scrape/pricing) (table above) | $0.50 by card or USDC, $0.40 with UPT |
| Failed requests | [Not charged](https://developers.oxylabs.io/api-reference/errors-and-status-codes) (faulted); the docs don't say about requests you give up on | Not documented yet |
| Residential proxies | [$6 to $2.50 per GB](https://oxylabs.io/pricing/residential-proxy-pool) | Included: every page is one credit |
| Where requests come from | Residential IPs mostly [from providers such as Honeygain](https://public-files.oxylabs.io/PDFs/OxylabsResidentialProxyPoolHandbook.pdf), 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](https://oxylabs.io/kyc-and-safety); 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](https://developers.oxylabs.io/ai-workflows/mcp) (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).

### TypeScript (HTTP)

```ts
// 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 DE
const [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));
```

### Python (HTTP)

```python
# 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 os
import sys

import 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])
```

### TypeScript (MCP)

```ts
// 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 DE
const [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();
}
```

### Python (CLI)

```python
# 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 json
import os
import subprocess
import sys

import 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](https://uprock.ai/compare/bright-data#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](https://uprock.ai/compare/scraperapi#where-a-batch-leaves-work-undone): poll an async batch, keep what finished, and send the rest to a second network.
- [See a page as it looks in another country](https://uprock.ai/use-cases/page-from-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](https://uprock.ai/video-semantic-search): find the second something is shown or said, across platforms.

## Related articles

- [How do I add a backup crawler to Firecrawl?](https://uprock.ai/compare/firecrawl.md): When to send a page from Firecrawl to UpRock, published prices side by side, and a tested fallback in TypeScript and Python.
- [How do I add a backup for Tavily Extract?](https://uprock.ai/compare/tavily.md): Retry the URLs in Tavily Extract's failed results on a real device, and read pages as they appear in a chosen country. Prices side by side and tested code.
- [How do I add a fallback for Exa contents errors?](https://uprock.ai/compare/exa.md): Read the per-URL statuses in Exa's /contents response and retry the right ones live on a real device. Error tags explained, prices side by side, tested code.
- [How do I add a backup to Jina Reader, and when is it cheaper?](https://uprock.ai/compare/jina-reader.md): Spot a failed read in Reader's JSON (it still answers 200), retry it on a real device, and see what six real pages cost in Reader tokens next to a flat per-page price.
- [How do I add a hosted fallback to Crawl4AI?](https://uprock.ai/compare/crawl4ai.md): Plug UpRock into Crawl4AI's fallback_fetch_function so blocked pages are fetched on a real device in the country you choose, then run through Crawl4AI's own markdown pipeline.
- [How do I add a backup to Bright Data's Web Unlocker?](https://uprock.ai/compare/bright-data.md): Which Web Unlocker errors to retry, which to send to a second network and which to respect, with Bright Data's published prices next to ours and tested fallback code.
- [How do I add a fallback to an Apify or Crawlee crawler?](https://uprock.ai/compare/apify.md): Hand the requests Crawlee gives up on to a second network with failedRequestHandler, see what a fetched page costs on Apify's own Actors, and run the tested code.
- [When should a Browserbase agent read a page without opening a browser?](https://uprock.ai/compare/browserbase.md): What 1,000 page reads cost on Browserbase three ways, when to send a read to UpRock instead (including when every browser is busy), and tested fallback code.
- [How do I cap ScrapingBee's credit cost and send the rest to a backup?](https://uprock.ai/compare/scrapingbee.md): ScrapingBee charges 1 to 75 credits a page depending on the options. Cap it with Auto-Mode and max_cost, watch the country fallback, and send the rest to UpRock with tested code.
- [Spider costs less per page than UpRock. When is a backup still worth it?](https://uprock.ai/compare/spider.md): Spider is cheaper per page, so keep it as your primary. How Spider reports a page it couldn't get (from our own tests), and tested code that sends only those pages to UpRock.
- [How do I add a fallback to Parallel's Extract API?](https://uprock.ai/compare/parallel.md): Make Parallel Extract report a failed live fetch instead of quietly serving an old cached copy, then retry those URLs on UpRock. Prices side by side and tested code.
- [How do I add a backup to ZenRows?](https://uprock.ai/compare/zenrows.md): Which ZenRows error codes to send to a second network and which to respect, what a page from one country costs once premium proxies are required, and tested fallback code.
- [How do I retry a ScraperAPI batch's failed jobs on another network?](https://uprock.ai/compare/scraperapi.md): Send the jobs ScraperAPI's async batch gives up on to UpRock, see which countries each ScraperAPI plan can target, and compare credit costs. Tested code for both.
- [How do I add a backup to Context.dev's scrape API?](https://uprock.ai/compare/context-dev.md): Context.dev reports a failed scrape inside an HTTP 200. Which of its error codes to retry on a second network, what a page costs by plan, and tested fallback code.
- [How do I add a backup to Scrapfly instead of paying for residential?](https://uprock.ai/compare/scrapfly.md): A Scrapfly page costs 1 to 30 credits depending on proxy pool and browser. What that is per 1,000 pages on each plan, which errors to send on, and tested code.
- [How do I keep Crawlbase's site tiers from surprising my bill?](https://uprock.ai/compare/crawlbase.md): Crawlbase bills each site at 1 to 20 times its base rate. What that does to the cost per 1,000 pages, and tested code that moves pricey domains to UpRock.
- [Which unlocker API fits: Bright Data, Oxylabs, Zyte or UpRock?](https://uprock.ai/compare/web-unlocker-apis.md): Bright Data's Web Unlocker, Oxylabs' Web API and Zyte API priced per 1,000 hard pages, with what each bills and when a second network helps.
- [Compare: all articles](https://uprock.ai/compare.md)

## References

1. [Oxylabs, Web API scrape pricing](https://oxylabs.io/products/web-api/scrape/pricing), checked 21 Oct 2026
2. [Oxylabs docs, Changelog (Web API launch, 7 Oct 2026)](https://developers.oxylabs.io/changelog), checked 21 Oct 2026
3. [Oxylabs docs, Web API /scrape](https://developers.oxylabs.io/products/web-api/scrape), checked 21 Oct 2026
4. [Oxylabs docs, Errors and status codes](https://developers.oxylabs.io/api-reference/errors-and-status-codes), checked 21 Oct 2026
5. [Oxylabs docs, Credit-based billing](https://developers.oxylabs.io/dashboard/credit-based-billing), checked 21 Oct 2026
6. [Oxylabs, Residential proxy pricing](https://oxylabs.io/pricing/residential-proxy-pool), checked 21 Oct 2026
7. [Oxylabs, Residential Proxy Pool handbook (how IPs are sourced)](https://public-files.oxylabs.io/PDFs/OxylabsResidentialProxyPoolHandbook.pdf), checked 21 Oct 2026
8. [Oxylabs, KYC and safety](https://oxylabs.io/kyc-and-safety), checked 21 Oct 2026
9. [Oxylabs docs, Restricted targets (Web Scraper API Classic)](https://developers.oxylabs.io/products/web-scraper-api/restricted-targets), checked 21 Oct 2026
10. [Oxylabs docs, MCP](https://developers.oxylabs.io/ai-workflows/mcp), checked 21 Oct 2026
11. [UpRock, everything in one file (API, CLI, MCP, pricing)](https://uprock.ai/llms-full.txt), checked 21 Oct 2026

## Get started

Get an API key: https://uprock.ai/app/api-keys

Paste this into your agent:

```text
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.
```

Setup for every client (MCP, skill, CLI): https://uprock.ai/agents.md
