# How do I retry a ScraperAPI batch's failed jobs on another network?

Submit your URLs to ScraperAPI's async batch endpoint as usual, and when its own retries give up on a job, or it's still running at your deadline, send that URL to UpRock. Country matters too, because ScraperAPI's two cheapest plans can target only the US or the EU, not individual countries, while UpRock takes any of 63 countries on every request.

Updated 21 Oct 2026. Web page: https://uprock.ai/compare/scraperapi

## On this page

- [What ScraperAPI does well](#what-scraperapi-does-well)
- [Where a batch leaves work undone](#where-a-batch-leaves-work-undone)
- [What each plan can target, and what it costs](#what-each-plan-can-target-and-what-it-costs)
- [ScraperAPI and UpRock side by side](#scraperapi-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 ScraperAPI does well

ScraperAPI is a scraping API that handles proxies, retries and rendering behind one endpoint. Beyond single pages, it has an async API that keeps working on a job for up to 24 hours, batches of up to 50,000 URLs, webhooks, structured-data endpoints for Amazon, Google, Walmart and others, DataPipeline for scheduled runs, a crawler, an AI parser, markdown and text output, and rendering instructions for clicks and scrolls. It integrates with LangChain, LlamaIndex, n8n and Zapier and has an official MCP server.

For large batches you can hand off and collect later, ScraperAPI is built for the job. Keep it.

## Where a batch leaves work undone

ScraperAPI retries each request itself for up to 70 seconds before giving up with a 500, and its FAQ says to expect 1 to 3% of requests to fail anyway. In an async batch, those show up as jobs that end as `failed`, or that finish with a non-2xx `statusCode`. Some jobs also just take a long time: the async API will keep going for up to 24 hours.

A fallback closes both gaps. Poll each job's `statusUrl` (no key needed), keep the ones that finished with a 2xx, skip 404 and 410 (the site's answer, and billed), and send the rest, plus anything still running at your deadline, to UpRock.

## What each plan can target, and what it costs

ScraperAPI bills in credits, and what a country costs depends on your plan (checked 21 Oct 2026):

| Plan | Price, credits | Geotargeting | Plain page | Rendered | Premium + rendered |
|---|---|---|---|---|---|
| Hobby | $49, 100,000 | [US and EU only](https://docs.scraperapi.com/control-and-optimization/geotargeting/standard-geo) | $0.49 | $4.90 | $12.25 |
| Startup | $149, 1 million | US and EU only | $0.15 | $1.49 | $3.73 |
| Business | $299, 3 million | 68 countries | $0.10 | $1.00 | $2.49 |
| Advanced (largest self-serve) | $1,975, 21.5 million | 68 countries | $0.09 | $0.92 | $2.30 |
| [Premium geotargeting](https://docs.scraperapi.com/control-and-optimization/geotargeting/premium-geo) | | 155 more countries, with `premium=true` (10 credits) | | | |
| UpRock | | 63 countries, or a region, every request | $0.50 | $0.50 | $0.50 |

Prices are per 1,000 pages. A plain request is 1 credit, rendering or premium proxies 10, both 25, and ultra premium with rendering 75. Some domains cost more whatever you choose: e-commerce sites 5 credits, Google and Bing 25, LinkedIn 30. Unused credits don't roll over.

## ScraperAPI and UpRock side by side

| | ScraperAPI | UpRock |
|---|---|---|
| Price per 1,000 pages | [$0.09 to $36.75](https://www.scraperapi.com/pricing/) depending on plan, options and domain | $0.50 by card or USDC, $0.40 with UPT |
| Failed requests | [Not charged](https://www.scraperapi.com/pricing/) (200 and 404 are) | Not documented yet |
| Async batches | [Up to 50,000 URLs](https://docs.scraperapi.com/asynchronous-api/batch-requests), worked on for up to 24 hours, webhooks | No. One page per call |
| Country | US and EU only on Hobby and Startup; 68 countries from Business up; 155 more with premium | Every request: 63 countries, or a region |
| Where requests come from | Its proxy pools; premium is a private pool of residential and mobile IPs | Devices whose owners opted in |
| Structured data, crawler, AI parser | Yes | No |
| MCP server | [Official](https://docs.scraperapi.com/integrations/llm-integrations/mcp-server), hosted or local | Yes, remote (streamable HTTP) |
| Video | No (YouTube pages as text) | Search public video by moment, $1 per 1,000 searches |
| To try it | 5,000 credits over 7 days, then a free plan with 1,000 a month | Prepaid credits |

Where ScraperAPI is better: big async batches with webhooks, structured data for major sites, failed requests that cost nothing, very cheap plain pages on bigger plans, and a free trial. Where UpRock fits: the jobs a batch gives up on or takes too long with, a specific country on any plan, rendered pages at one flat price, and video.

## The fallback, in TypeScript and Python

Every version submit a batch to `async.scraperapi.com/batchjobs` with markdown output, poll each job every five seconds until a deadline (two minutes here), keep the finished 2xx results, skip 404 and 410, and send every other URL to UpRock in parallel. If the batch can't be submitted at all (a bad key, no credits, too many requests), every URL goes to UpRock. They call ScraperAPI's HTTP API directly, because its Node SDK has no async methods and neither SDK has a batch method. 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
// Submit a batch to ScraperAPI's async API, then send the jobs that failed (or are still
// running at the deadline) to UpRock.
// Node 18+   env: SCRAPERAPI_KEY, UPROCK_API_KEY

const ASYNC = "https://async.scraperapi.com";

type Job = {
  id: string;
  status: string;
  statusUrl: string;
  url: string;
  response?: { statusCode: number; body: string };
};

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 ?? "";
}

async function submitBatch(urls: string[]): Promise<Job[]> {
  const res = await fetch(`${ASYNC}/batchjobs`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      apiKey: process.env.SCRAPERAPI_KEY,
      urls,
      apiParams: { output_format: "markdown" },
    }),
  });
  // 401 bad key, 403 out of credits, 429 too many concurrent requests.
  if (!res.ok)
    throw new Error(`ScraperAPI ${res.status}: ${(await res.text()).slice(0, 200)}`);
  return res.json();
}

export async function scrapeAll(urls: string[], country = "US", waitMs = 120_000) {
  const pages = new Map<string, { via: string; markdown: string }>();
  let pending: Job[] = [];
  try {
    pending = await submitBatch(urls);
  } catch (err) {
    console.warn(`${(err as Error).message}; sending every URL to UpRock`);
  }
  const retry: string[] = pending.length ? [] : [...urls];
  const deadline = Date.now() + waitMs;
  while (pending.length && Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, 5_000));
    const still: Job[] = [];
    for (const job of pending) {
      const state: Job = await (await fetch(job.statusUrl)).json(); // the status URL needs no key
      const code = state.response?.statusCode ?? 0;
      if (state.status === "running") still.push(job);
      else if (state.status === "finished" && code >= 200 && code < 300) {
        pages.set(job.url, { via: "scraperapi", markdown: state.response!.body });
      } else if (code === 404 || code === 410) {
        console.warn(`${job.url}: the page is gone (${code})`); // billed, and not worth a retry
      } else retry.push(job.url); // failed after ScraperAPI's own retries
    }
    pending = still;
  }
  retry.push(...pending.map((j) => j.url)); // still running at the deadline
  const retried = await Promise.allSettled(retry.map((u) => viaUpRock(u, country)));
  retried.forEach((r, i) => {
    if (r.status === "fulfilled")
      pages.set(retry[i], { via: "uprock", markdown: r.value });
    else console.warn(`UpRock also failed for ${retry[i]}: ${r.reason}`);
  });
  return pages;
}

// node batch.ts DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org
const [country = "US", ...urls] = process.argv.slice(2);
for (const [url, page] of await scrapeAll(urls, country)) {
  console.log(page.via.padEnd(10), url, page.markdown.length, "chars");
}
```

### Python (HTTP)

```python
# Submit a batch to ScraperAPI's async API, then send the jobs that failed (or are still
# running at the deadline) to UpRock.
# pip install requests   env: SCRAPERAPI_KEY, UPROCK_API_KEY
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor

import requests

ASYNC = "https://async.scraperapi.com"


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 submit_batch(urls: list[str]) -> list[dict]:
    res = requests.post(
        f"{ASYNC}/batchjobs",
        timeout=60,
        json={
            "apiKey": os.environ.get("SCRAPERAPI_KEY"),
            "urls": urls,
            "apiParams": {"output_format": "markdown"},
        },
    )
    # 401 bad key, 403 out of credits, 429 too many concurrent requests.
    if not res.ok:
        raise RuntimeError(f"ScraperAPI {res.status_code}: {res.text[:200]}")
    return res.json()


def scrape_all(
    urls: list[str], country: str = "US", wait_s: int = 120
) -> dict[str, dict]:
    pages, retry, pending = {}, [], []
    try:
        pending = submit_batch(urls)
    except Exception as err:
        print(f"{err}; sending every URL to UpRock", file=sys.stderr)
        retry = list(urls)

    deadline = time.time() + wait_s
    while pending and time.time() < deadline:
        time.sleep(5)
        still = []
        for job in pending:
            state = requests.get(
                job["statusUrl"], timeout=30
            ).json()  # the status URL needs no key
            code = (state.get("response") or {}).get("statusCode", 0)
            if state["status"] == "running":
                still.append(job)
            elif state["status"] == "finished" and 200 <= code < 300:
                pages[job["url"]] = {
                    "via": "scraperapi",
                    "markdown": state["response"]["body"],
                }
            elif code in (404, 410):
                print(
                    f"{job['url']}: the page is gone ({code})", file=sys.stderr
                )  # billed
            else:
                retry.append(job["url"])  # failed after ScraperAPI's own retries
        pending = still
    retry += [job["url"] for job in pending]  # still running at the deadline

    with ThreadPoolExecutor(max_workers=8) as pool:
        futures = {u: pool.submit(via_uprock, u, country) for u in retry}
    for url, future in futures.items():
        try:
            pages[url] = {"via": "uprock", "markdown": future.result()}
        except Exception as err:
            print(f"UpRock also failed for {url}: {err}", file=sys.stderr)
    return pages


if __name__ == "__main__":
    # python batch.py DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org
    country, *urls = sys.argv[1:] or ["US"]
    for url, page in scrape_all(urls, country).items():
        print(page["via"].ljust(10), url, len(page["markdown"]), "chars")
```

### TypeScript (MCP)

```ts
// Submit a batch to ScraperAPI's async API, then send the jobs that failed (or are still
// running at the deadline) to UpRock.
// npm i @modelcontextprotocol/sdk   (Node 18+)   env: SCRAPERAPI_KEY, UPROCK_API_KEY
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const ASYNC = "https://async.scraperapi.com";
const uprock = new Client({ name: "scraperapi-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}` } } },
  ),
);

type Job = {
  id: string;
  status: string;
  statusUrl: string;
  url: string;
  response?: { statusCode: number; body: string };
};

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,
  );
}

async function submitBatch(urls: string[]): Promise<Job[]> {
  const res = await fetch(`${ASYNC}/batchjobs`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      apiKey: process.env.SCRAPERAPI_KEY,
      urls,
      apiParams: { output_format: "markdown" },
    }),
  });
  // 401 bad key, 403 out of credits, 429 too many concurrent requests.
  if (!res.ok)
    throw new Error(`ScraperAPI ${res.status}: ${(await res.text()).slice(0, 200)}`);
  return res.json();
}

export async function scrapeAll(urls: string[], country = "US", waitMs = 120_000) {
  const pages = new Map<string, { via: string; markdown: string }>();
  let pending: Job[] = [];
  try {
    pending = await submitBatch(urls);
  } catch (err) {
    console.warn(`${(err as Error).message}; sending every URL to UpRock`);
  }
  const retry: string[] = pending.length ? [] : [...urls];
  const deadline = Date.now() + waitMs;
  while (pending.length && Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, 5_000));
    const still: Job[] = [];
    for (const job of pending) {
      const state: Job = await (await fetch(job.statusUrl)).json(); // the status URL needs no key
      const code = state.response?.statusCode ?? 0;
      if (state.status === "running") still.push(job);
      else if (state.status === "finished" && code >= 200 && code < 300) {
        pages.set(job.url, { via: "scraperapi", markdown: state.response!.body });
      } else if (code === 404 || code === 410) {
        console.warn(`${job.url}: the page is gone (${code})`); // billed, and not worth a retry
      } else retry.push(job.url); // failed after ScraperAPI's own retries
    }
    pending = still;
  }
  retry.push(...pending.map((j) => j.url)); // still running at the deadline
  const retried = await Promise.allSettled(retry.map((u) => viaUpRock(u, country)));
  retried.forEach((r, i) => {
    if (r.status === "fulfilled")
      pages.set(retry[i], { via: "uprock", markdown: r.value });
    else console.warn(`UpRock also failed for ${retry[i]}: ${r.reason}`);
  });
  return pages;
}

// node batch.ts DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org
const [country = "US", ...urls] = process.argv.slice(2);
try {
  for (const [url, page] of await scrapeAll(urls, country)) {
    console.log(page.via.padEnd(10), url, page.markdown.length, "chars");
  }
} finally {
  await uprock.close();
}
```

### Python (CLI)

```python
# Submit a batch to ScraperAPI's async API, then send the jobs that failed (or are still
# running at the deadline) to UpRock.
# pip install requests; npm i -g uprock   env: SCRAPERAPI_KEY, UPROCK_API_KEY
import json
import os
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor

import requests

ASYNC = "https://async.scraperapi.com"


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 submit_batch(urls: list[str]) -> list[dict]:
    res = requests.post(
        f"{ASYNC}/batchjobs",
        timeout=60,
        json={
            "apiKey": os.environ.get("SCRAPERAPI_KEY"),
            "urls": urls,
            "apiParams": {"output_format": "markdown"},
        },
    )
    # 401 bad key, 403 out of credits, 429 too many concurrent requests.
    if not res.ok:
        raise RuntimeError(f"ScraperAPI {res.status_code}: {res.text[:200]}")
    return res.json()


def scrape_all(
    urls: list[str], country: str = "US", wait_s: int = 120
) -> dict[str, dict]:
    pages, retry, pending = {}, [], []
    try:
        pending = submit_batch(urls)
    except Exception as err:
        print(f"{err}; sending every URL to UpRock", file=sys.stderr)
        retry = list(urls)

    deadline = time.time() + wait_s
    while pending and time.time() < deadline:
        time.sleep(5)
        still = []
        for job in pending:
            state = requests.get(
                job["statusUrl"], timeout=30
            ).json()  # the status URL needs no key
            code = (state.get("response") or {}).get("statusCode", 0)
            if state["status"] == "running":
                still.append(job)
            elif state["status"] == "finished" and 200 <= code < 300:
                pages[job["url"]] = {
                    "via": "scraperapi",
                    "markdown": state["response"]["body"],
                }
            elif code in (404, 410):
                print(
                    f"{job['url']}: the page is gone ({code})", file=sys.stderr
                )  # billed
            else:
                retry.append(job["url"])  # failed after ScraperAPI's own retries
        pending = still
    retry += [job["url"] for job in pending]  # still running at the deadline

    with ThreadPoolExecutor(max_workers=8) as pool:
        futures = {u: pool.submit(via_uprock, u, country) for u in retry}
    for url, future in futures.items():
        try:
            pages[url] = {"via": "uprock", "markdown": future.result()}
        except Exception as err:
            print(f"UpRock also failed for {url}: {err}", file=sys.stderr)
    return pages


if __name__ == "__main__":
    # python batch.py DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org
    country, *urls = sys.argv[1:] or ["US"]
    for url, page in scrape_all(urls, country).items():
        print(page["via"].ljust(10), url, len(page["markdown"]), "chars")
```

Tested 21 Oct 2026 against the live UpRock API with an invalid ScraperAPI key: the batch endpoint answered 401, and both versions sent every URL to UpRock. Without an account, the polling path follows ScraperAPI's documented job format. On 6 Nov 2026 we ran the HTTP and MCP versions again with an UpRock API key in an `Authorization` header: with an invalid ScraperAPI key, each fell back to UpRock and got the page.

## FAQ

### How often do ScraperAPI requests fail?

ScraperAPI's own FAQ says to expect 1 to 3% of requests to fail. That's after its automatic retries, which run for up to 70 seconds before it answers with a 500. Those are the requests a fallback is for.

### Does ScraperAPI charge for failed requests?

No. Its pricing page says you're charged only for successful responses (200 and 404) and for requests you cancel after 70 seconds. Its terms add that requests to sites unreachable because of geo-restrictions are chargeable.

### Which countries can ScraperAPI target?

It depends on the plan. Hobby and Startup support only US and EU targeting, not individual countries. Business and higher get 68 country codes, and premium geotargeting adds 155 more but needs premium=true, which costs 10 credits a request.

### Why use the async batch API at all?

For lots of URLs, it does the waiting for you. ScraperAPI keeps working on each job for up to 24 hours, takes up to 50,000 URLs per batch and stores results for at least 24 hours. The fallback on this page adds a deadline, so jobs that are still running don't hold your pipeline up.

### Does ScraperAPI have an MCP server?

Yes, an official one, hosted at mcp.scraperapi.com/mcp or installed locally.

## Other use cases to explore

- [Add a fallback to a crawler you run](https://uprock.ai/compare/apify#hand-crawlees-failed-requests-to-uprock): hand the requests Crawlee gives up on to a device in the country you choose.
- [See what a country costs elsewhere](https://uprock.ai/compare/zenrows#what-a-page-costs-with-and-without-a-country): another provider where choosing a country means paying for premium proxies.
- [Estimate what 1,000 pages will cost](https://uprock.ai/compare/jina-reader#tokens-or-pages-what-1000-reads-cost): token counts for six real pages, and the length where a flat per-page price is cheaper.
- [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 add a backup to Oxylabs' new Web API?](https://uprock.ai/compare/oxylabs.md): Which Oxylabs Web API failures to send to a second network and which to respect, what a generic page really costs after its October 2026 pricing change, and tested fallback code.
- [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. [ScraperAPI, Pricing (plans, credit costs, billing rules)](https://www.scraperapi.com/pricing/), checked 21 Oct 2026
2. [ScraperAPI docs, Credits and request costs](https://docs.scraperapi.com/getting-started/quick-start/credits-and-requests-costs), checked 21 Oct 2026
3. [ScraperAPI docs, Async batch requests](https://docs.scraperapi.com/asynchronous-api/batch-requests), checked 21 Oct 2026
4. [ScraperAPI docs, Async job handling](https://docs.scraperapi.com/asynchronous-api/job-handling), checked 21 Oct 2026
5. [ScraperAPI docs, Standard geotargeting](https://docs.scraperapi.com/control-and-optimization/geotargeting/standard-geo), checked 21 Oct 2026
6. [ScraperAPI docs, Premium geotargeting](https://docs.scraperapi.com/control-and-optimization/geotargeting/premium-geo), checked 21 Oct 2026
7. [ScraperAPI docs, API status codes](https://docs.scraperapi.com/responses-and-formats/api-status-codes), checked 21 Oct 2026
8. [ScraperAPI docs, General FAQ](https://docs.scraperapi.com/resources/faq/general), checked 21 Oct 2026
9. [ScraperAPI docs, MCP server](https://docs.scraperapi.com/integrations/llm-integrations/mcp-server), checked 21 Oct 2026
10. [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
