How do I add a backup to Jina Reader, and when is it cheaper?
Keep Reader for quick reads and retry the pages it couldn't get on UpRock, from the country you need; since Reader still answers HTTP 200 when a site refused it, the check is the httpStatus and warning fields in its JSON. On price, Reader bills by output tokens, so pages under about 10,000 tokens are cheaper on Reader and longer ones are cheaper at UpRock's flat $0.50 per 1,000 pages.
Updated · Read as markdown
What Jina Reader does well
Reader is the shortest path from a URL to markdown: put https://r.jina.ai/ in front of any address and you get the page back as LLM-friendly text, with no SDK and, for light use, no key (20 requests a minute). It reads PDFs and Office files, can caption images, can return HTML, plain text or a screenshot instead, and takes headers for CSS selectors, waiting for an element, cookies and cache control. s.jina.ai does the same for search results, there's an official MCP server, and the code is open source under Apache-2.0.
For short pages it's also very cheap. Keep it.
How a failed read looks
Reader answers HTTP 200 even when the target page didn't. Ask for JSON (Accept: application/json) and look inside data:
{ "code": 200, "data": { "url": "https://en.wikipedia.org/wiki/Zzqx_nonexistent_article_9137", "httpStatus": 404, "warning": "Target URL returned error 404: Not Found", "content": "…" }}That's a real response from 19 Oct 2026, trimmed. The same warning field carries Reader's other notes, such as a page that may need a CAPTCHA or may not have finished loading. Reader's own errors do use HTTP status codes:
| What you see from Reader | What it means | Retry on UpRock? |
|---|---|---|
200 with data.httpStatus 401, 403 or 429 | The target site refused Reader's request | Yes |
200 with a warning about a CAPTCHA or an unfinished load | The content is probably a challenge page or a shell | Yes |
| 403 | Reader is limiting traffic to that site (it does this for heavy anonymous use) | Yes, or use a Reader key |
| 422 | Reader couldn't process the page (a downstream failure) | Yes |
| 429 | You hit Reader's rate limit (20, 500 or 5,000 requests a minute, by key) | Yes, or wait for retryAfter |
| 402 or 409 | Your token balance or budget ran out | Yes, or top up |
| 451 | Reader flagged the content as harmful | No |
200 with data.httpStatus 404 or 410 | The page is gone | No |
Tokens or pages: what 1,000 reads cost
Reader bills the tokens in its output: $50 buys 1 billion tokens ($0.05 per million) and $500 buys 11 billion ($0.045 per million, with the premium key). So the price of a page depends on how long it is. UpRock charges per page, whatever its length.
We read six public pages through Reader with its default settings on 19 Oct 2026 and noted the token count it reported for each:
| Page | Reader tokens | Reader, per 1,000 such pages ($50 pack) | UpRock, per 1,000 pages |
|---|---|---|---|
| example.com | 341 | $0.02 | $0.50 |
| A Cloudflare blog post | 2,600 | $0.13 | $0.50 |
| RFC 9309 (robots.txt) | 6,637 | $0.33 | $0.50 |
Python json module docs | 9,342 | $0.47 | $0.50 |
| Wikipedia, "Web crawler" | 20,478 | $1.02 | $0.50 |
| Apple MacBook Air product page | 24,134 | $1.21 | $0.50 |
The break-even is about 10,000 tokens a page on the $50 pack, 11,100 on the $500 pack, and 8,000 if you pay UpRock in UPT ($0.40 per 1,000). Pages shorter than that are cheaper on Reader; longer ones, like the Wikipedia article and the product page above, are cheaper on UpRock. Trimming Reader's output with X-Target-Selector or X-Retain-Images: none moves the line in Reader's favour.
Jina Reader and UpRock side by side
Reader's prices and rate limits come from jina.ai/reader, checked 24 Oct 2026; the rest from the Reader repository, checked 19 Oct 2026.
| Jina Reader | UpRock | |
|---|---|---|
| Price | $0.05 per million output tokens ($50 pack), $0.045 on the $500 pack; see the table above | $0.50 per 1,000 pages by card or USDC, $0.40 with UPT |
| Without a key | 20 requests a minute | A few trial crawls a day over HTTP (10 on 3 Nov 2026), then a key is required |
| Rate limits with a key | 500 a minute, 5,000 with a premium key | Not published yet |
| Country | X-Proxy: <country>, a residential IP, premium key only | Every request: 63 countries, or a region |
| Where pages are fetched from | Headless Chrome on Google Cloud Run (US and EU regions) | Real desktop and mobile devices whose owners opted in |
| PDFs, image captions, screenshots | Yes | Not documented |
| MCP server | Official, remote | Yes, remote (streamable HTTP) |
| Video | No; video embeds become links | Search public video by moment, $1 per 1,000 searches |
| Open source | Apache-2.0, self-hostable | No. The device runtime (Fuel Engine) is open source |
Where Reader is better: zero setup, a keyless tier, PDFs and images, short pages for fractions of a cent, and self-hosting. Where UpRock fits: the reads that come back with a refusal in data.httpStatus, a country on any key, long pages at a flat price, and video.
The fallback, in Bash, TypeScript and Python
Each version asks Reader for JSON, checks data.httpStatus and data.warning, and only calls UpRock when the read failed in a way a second try can fix. UpRock gets one POST /v1/crawl request with your key in an Authorization: Bearer header; the CLI and MCP tabs do the same through the UpRock CLI (npm i -g uprock) or its MCP server.
#!/usr/bin/env bash# read.sh <url> [country] needs curl and jq# env: UPROCK_API_KEY; JINA_API_KEY is optional (Reader works without one at 20 requests a minute)set -o pipefailurl=$1; country=${2:-US}auth=(); [ -n "$JINA_API_KEY" ] && auth=(-H "Authorization: Bearer $JINA_API_KEY")resp=$(curl -s -w '\n%{http_code}' -H 'Accept: application/json' "${auth[@]}" "https://r.jina.ai/$url")code=${resp##*$'\n'}; body=${resp%$'\n'*}target=$(jq -r '.data.httpStatus // empty' <<<"$body" 2>/dev/null)warning=$(jq -r '.data.warning // empty' <<<"$body" 2>/dev/null)case "$code:$target" in 200:404 | 200:410) echo "The page is gone ($target)" >&2; exit 1 ;; 200:401 | 200:403 | 200:429 | 200:5??) ;; # the site refused Reader: try UpRock 200:*) if ! grep -qiE 'captcha|not yet fully loaded' <<<"$warning"; then jq -r .data.content <<<"$body"; exit 0 fi ;; 400:* | 401:* | 451:*) echo "Reader said $code: $(jq -r '.readableMessage // .message' <<<"$body")" >&2; exit 1 ;;esacecho "Reader: HTTP $code, target ${target:-none} ${warning}. Trying UpRock from $country" >&2curl -s --max-time 90 https://edge.uprock.com/v1/crawl \ -H "Authorization: Bearer $UPROCK_API_KEY" -H 'Content-Type: application/json' \ -d "$(jq -n --arg url "$url" --arg country "$country" '{url: $url, country: $country}')" | jq -r 'if .markdown then .markdown else "UpRock: \(.code): \(.error)\n" | halt_error end'// Node 18+ env: UPROCK_API_KEY, optional JINA_API_KEY// Statuses from the target site (data.httpStatus) and from Reader itself that a second try can fix.const RETRY_TARGET = new Set([401, 403, 429, 500, 502, 503, 504]);const RETRY_READER = new Set([402, 403, 409, 422, 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") { const key = process.env.JINA_API_KEY; const res = await fetch(`https://r.jina.ai/${url}`, { headers: { Accept: "application/json", ...(key ? { Authorization: `Bearer ${key}` } : {}), }, }); const body = await res.json().catch(() => ({})); const target: number | undefined = body.data?.httpStatus; const warning = String(body.data?.warning ?? ""); if (res.ok) { if (target === 404 || target === 410) throw new Error(`The page is gone: ${warning}`); const unfinished = /captcha|not yet fully loaded/i.test(warning); if (!RETRY_TARGET.has(target ?? 200) && !unfinished) return { via: "jina", content: body.data.content as string }; } else if (!RETRY_READER.has(res.status)) { throw new Error( `Reader ${res.status}: ${body.readableMessage ?? body.message ?? ""}`, ); } console.warn( `Reader: HTTP ${res.status}, target ${target ?? "none"} ${warning}. Trying UpRock from ${country}`, ); return { via: "uprock", content: 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.content.slice(0, 200));# pip install requests env: UPROCK_API_KEY, optional JINA_API_KEYimport osimport reimport sysimport requests# Statuses from the target site (data.httpStatus) and from Reader itself that a second try can fix.RETRY_TARGET = {401, 403, 429, 500, 502, 503, 504}RETRY_READER = {402, 403, 409, 422, 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: headers = {"Accept": "application/json"} if os.environ.get("JINA_API_KEY"): headers["Authorization"] = f"Bearer {os.environ['JINA_API_KEY']}" res = requests.get(f"https://r.jina.ai/{url}", headers=headers, timeout=120) try: body = res.json() except ValueError: body = {} data = body.get("data") or {} target, warning = data.get("httpStatus"), data.get("warning") or "" if res.ok: if target in (404, 410): raise LookupError(f"The page is gone: {warning}") unfinished = re.search(r"captcha|not yet fully loaded", warning, re.I) if (target or 200) not in RETRY_TARGET and not unfinished: return {"via": "jina", "content": data.get("content", "")} elif res.status_code not in RETRY_READER: raise RuntimeError( f"Reader {res.status_code}: {body.get('readableMessage') or body.get('message')}" ) print( f"Reader: HTTP {res.status_code}, target {target} {warning}. Trying UpRock from {country}", file=sys.stderr, ) return {"via": "uprock", "content": 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["content"][:200])#!/usr/bin/env bash# read.sh <url> [country] needs curl, jq and the UpRock CLI (npm i -g uprock)# env: UPROCK_API_KEY; JINA_API_KEY is optional (Reader works without one at 20 requests a minute)set -o pipefailurl=$1; country=${2:-US}auth=(); [ -n "$JINA_API_KEY" ] && auth=(-H "Authorization: Bearer $JINA_API_KEY")resp=$(curl -s -w '\n%{http_code}' -H 'Accept: application/json' "${auth[@]}" "https://r.jina.ai/$url")code=${resp##*$'\n'}; body=${resp%$'\n'*}target=$(jq -r '.data.httpStatus // empty' <<<"$body" 2>/dev/null)warning=$(jq -r '.data.warning // empty' <<<"$body" 2>/dev/null)case "$code:$target" in 200:404 | 200:410) echo "The page is gone ($target)" >&2; exit 1 ;; 200:401 | 200:403 | 200:429 | 200:5??) ;; # the site refused Reader: try UpRock 200:*) if ! grep -qiE 'captcha|not yet fully loaded' <<<"$warning"; then jq -r .data.content <<<"$body"; exit 0 fi ;; 400:* | 401:* | 451:*) echo "Reader said $code: $(jq -r '.readableMessage // .message' <<<"$body")" >&2; exit 1 ;;esacecho "Reader: HTTP $code, target ${target:-none} ${warning}. Trying UpRock from $country" >&2uprock ai crawl "$url" --country "$country" --content | jq -r .inlined_markdown// npm i @modelcontextprotocol/sdk (Node 18+) env: UPROCK_API_KEY, optional JINA_API_KEYimport { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";const uprock = new Client({ name: "jina-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 from the target site (data.httpStatus) and from Reader itself that a second try can fix.const RETRY_TARGET = new Set([401, 403, 429, 500, 502, 503, 504]);const RETRY_READER = new Set([402, 403, 409, 422, 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") { const key = process.env.JINA_API_KEY; const res = await fetch(`https://r.jina.ai/${url}`, { headers: { Accept: "application/json", ...(key ? { Authorization: `Bearer ${key}` } : {}), }, }); const body = await res.json().catch(() => ({})); const target: number | undefined = body.data?.httpStatus; const warning = String(body.data?.warning ?? ""); if (res.ok) { if (target === 404 || target === 410) throw new Error(`The page is gone: ${warning}`); const unfinished = /captcha|not yet fully loaded/i.test(warning); if (!RETRY_TARGET.has(target ?? 200) && !unfinished) return { via: "jina", content: body.data.content as string }; } else if (!RETRY_READER.has(res.status)) { throw new Error( `Reader ${res.status}: ${body.readableMessage ?? body.message ?? ""}`, ); } console.warn( `Reader: HTTP ${res.status}, target ${target ?? "none"} ${warning}. Trying UpRock from ${country}`, ); return { via: "uprock", content: 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.content.slice(0, 200));} finally { await uprock.close();}# pip install requests; npm i -g uprock env: UPROCK_API_KEY, optional JINA_API_KEYimport jsonimport osimport reimport subprocessimport sysimport requests# Statuses from the target site (data.httpStatus) and from Reader itself that a second try can fix.RETRY_TARGET = {401, 403, 429, 500, 502, 503, 504}RETRY_READER = {402, 403, 409, 422, 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: headers = {"Accept": "application/json"} if os.environ.get("JINA_API_KEY"): headers["Authorization"] = f"Bearer {os.environ['JINA_API_KEY']}" res = requests.get(f"https://r.jina.ai/{url}", headers=headers, timeout=120) try: body = res.json() except ValueError: body = {} data = body.get("data") or {} target, warning = data.get("httpStatus"), data.get("warning") or "" if res.ok: if target in (404, 410): raise LookupError(f"The page is gone: {warning}") unfinished = re.search(r"captcha|not yet fully loaded", warning, re.I) if (target or 200) not in RETRY_TARGET and not unfinished: return {"via": "jina", "content": data.get("content", "")} elif res.status_code not in RETRY_READER: raise RuntimeError( f"Reader {res.status_code}: {body.get('readableMessage') or body.get('message')}" ) print( f"Reader: HTTP {res.status_code}, target {target} {warning}. Trying UpRock from {country}", file=sys.stderr, ) return {"via": "uprock", "content": 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["content"][:200])Tested 19 Oct 2026 on Reader's keyless tier with the live UpRock API: example.com stayed on Reader, a test URL that always answers 403 went to UpRock, and a missing Wikipedia page stopped as gone. The Bash version runs on the bash 3.2 that ships with macOS. On 6 Nov 2026 we ran the HTTP and MCP versions again with an UpRock API key in an Authorization header: the 403 test URL went from Reader to UpRock.
FAQ
Does Jina Reader return an error when the target page fails?
Not as an HTTP error. When we asked Reader for a Wikipedia page that doesn't exist, the response was HTTP 200 with data.httpStatus set to 404 and data.warning saying "Target URL returned error 404". Check those two fields, not just the response code.
How many Reader tokens does a web page use?
It depends on the page. With Reader's default settings we measured 341 tokens for example.com, 2,600 for a blog post, 9,342 for a Python docs page and 24,134 for a product page (19 Oct 2026). Selectors such as X-Target-Selector, or dropping images, cut that down.
Can Jina Reader fetch from a specific country?
Yes. The X-Proxy header takes a country code and, per Reader's cookbook, exits through a residential IP in that country. It needs a premium key, which comes with the $500 token pack. UpRock takes a country on every request.
Is there an UpRock URL prefix like r.jina.ai?
Not today. UpRock takes one `POST /v1/crawl` request per page, which the fallback on this page leads with, or runs through its MCP server or CLI. Without a key you get a few trial crawls a day over HTTP (10 on 3 Nov 2026); after that it needs an API key.
When should I not bother with a fallback?
When the page is gone (404 or 410) or behind a login. Reader and UpRock both fetch public pages only, so a second try won't help there.
Other use cases to explore
- 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.
- Fetch from countries your crawler doesn't cover: such as South Korea and Indonesia, sent straight to a device in that country.
- Add a fallback to a crawler you host: send only the pages your server can't load to a device in the country you choose.
- Search public video by moment: find the second something is shown or said, across platforms.
References
- Jina AI, Reader (pricing, rate limits, headers), checked 24 Oct 2026
- jina-ai/reader on GitHub (README, Apache-2.0), checked 19 Oct 2026
- jina-ai/reader cookbook (country proxy needs a premium key), checked 19 Oct 2026
- jina-ai/reader architecture (headless Chrome on Google Cloud Run), checked 19 Oct 2026
- Reader API spec (error codes), checked 19 Oct 2026
- Jina AI MCP server, checked 19 Oct 2026
- UpRock, everything in one file (API, CLI, MCP, pricing), checked 24 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.