NEWScrapingAnt MCP for Claude Code, Cursor & Windsurf — try it free →
Skip to main content
Version: v2

Migrate ZenRows Fetch HTML requests to ScrapingAnt

This guide covers independent synchronous HTML GET requests to ZenRows Fetch, formerly Universal Scraper API. Move one request to ScrapingAnt's /v2/general, preserve the HTML your parser needs, then increase traffic gradually. For plan selection and mode costs, see the ZenRows alternative comparison.

What changes when you switch:

  • Concurrency: ZenRows plans cap concurrent requests (for example 20 on Build and 100 on Growth). Paid ScrapingAnt plans have no plan concurrency cap; Free allows one active request. Keep a bounded worker pool sized to your targets and budget.
  • Countries: every country in ScrapingAnt's proxy_country list works on both datacenter and residential proxies at the normal credit cost, with no geotargeting surcharge. ZenRows country selection requires Premium Proxies.
  • Billing: ZenRows does not bill failed requests, but counts target 404/410 responses as successful. ScrapingAnt does not bill a request when its API returns a non-200 status, and bills an API 200 even if the target page inside it is a 403, 404 or otherwise unusable. Behavior, pools and success rates still differ, so canary before moving traffic.

Mappings were checked against the official references on October 18, 2026. Running the commands below makes provider requests and may consume credits.

Eligibility checklist​

  • Your current call is synchronous Fetch returning HTML, and your code controls its queue and parser.
  • Each GET is safe to repeat, including any browser interactions. A browser click can change state even when the outer request is GET.
  • You can choose browser and proxy settings explicitly, rather than depending on mode=auto escalation.
  • Each job finishes within ScrapingAnt's 60-second maximum API timeout. ZenRows Fetch requests can run for up to 180 seconds; jobs that need more than 60 seconds have no equivalent here and should stay on their current integration.
  • You have checked the managed API country list for your required locations and plan access.
  • You can adapt response headers, target-status handling and error classification, and budget using the mode-specific credit costs.

Plan a separate migration for persistent IP sessions (session_id), automatic escalation, CDP Browser Sessions, screenshots/PDFs, Batch/Crawl, structured extraction or network-capture JSON. These features are not covered by a parameter rename. ScrapingAnt's extended endpoint can expose XHR data, but its schema and capture behavior are not a parity contract for ZenRows json_response.

Replace the endpoint and credentials​

Store each key in its corresponding environment variable. Keep credentials out of source control, request URLs in logs and exception reports. The ScrapingAnt examples send the key in the x-api-key request header, so it never appears in the request URL; the query parameter of the same name also works, and the header takes precedence if both are sent (see errors).

Before: manual ZenRows Fetch, plain HTTP

curl --get --max-time 180 'https://api.zenrows.com/v1/' \
--data-urlencode "apikey=$ZENROWS_API_KEY" \
--data-urlencode 'url=https://example.com/?page=2&sort=new' \
--data-urlencode 'js_render=false' \
--data-urlencode 'premium_proxy=false'

After: ScrapingAnt HTML API, plain HTTP

curl --get --max-time 75 'https://api.scrapingant.com/v2/general' \
-H "x-api-key: $SCRAPINGANT_API_KEY" \
--data-urlencode 'url=https://example.com/?page=2&sort=new' \
--data-urlencode 'browser=false' \
--data-urlencode 'proxy_type=datacenter'

--get keeps this a GET and --data-urlencode encodes the nested target URL once. The client deadlines differ deliberately: ZenRows documents a processing window of up to 180 seconds, while ScrapingAnt's timeout defaults to and cannot exceed 60 seconds (range 5–60). Keep the client limit longer than the API timeout; the 75-second value adds 15 seconds of transport overhead. A client timeout does not prove the server stopped processing.

Map settings deliberately​

ZenRows manual FetchScrapingAnt API v2Migration decision
apikeyx-api-key headerUse your ScrapingAnt key, not the old value.
urlurlPreserve the full target URL. Let the HTTP client encode query values once.
js_render=false (default)browser=falseSet explicitly. ScrapingAnt defaults to browser=true.
js_render=truebrowser=trueCheck the returned DOM and the rendering credit rate.
premium_proxy=falseproxy_type=datacenterExplicit plain/default routing.
premium_proxy=trueproxy_type=residentialA functional proxy class, not identical IP pools, fingerprinting or anti-bot results.
premium_proxy=true + proxy_countryproxy_type=residential + proxy_countryZenRows only accepts a country with Premium Proxies, so this is the faithful mapping. ScrapingAnt also serves every listed country on datacenter with no surcharge, a cheaper class worth canarying separately. Normalize to uppercase and validate against the current list and your rollout allowlist; do not silently drop an unsupported country.
wait_forwait_for_selectorRequires browser=true; test the selector and resulting content. It changes the page readiness condition.
block_resources (enabled by default)block_resource (opt-in, repeatable)ScrapingAnt blocks nothing unless you list resource types, for example block_resource=image&block_resource=font, with browser=true. Rendered output and timing can change when blocking is added or removed. See the parameter reference.
wait (milliseconds)No direct parameterA fixed delay differs from waiting for a selector. If necessary, rewrite as an awaited browser snippet, within the execution deadline.
js_instructionsRewrite as js_snippetDirect API snippets must be Base64-encoded JavaScript, not instruction JSON. Recheck ordering, waits, selectors and side effects.
custom_headers=true + supported request headersTarget headers prefixed with ant-For example, Ant-Referer: https://example.com/. Recheck which headers each service permits; do not forward credentials intended for a different host. See headers.
Cookie header with custom_headers=truecookies query parameterSend only target-specific name/value pairs, such as preference=compact;locale=en, encoded by your client. Cookie attributes and authentication/session portability are not implied. See cookies.
original_status / allowed_status_codesInspect Ant-page-status-codeThese are not equivalent API switches; adapt the target-status checks described below.
mode=auto, session_id, output/extraction optionsSeparate designDo not discard them silently or infer behavior from the settings above.

For country validation, first confirm that the code is supported in the current API reference. Then keep an application allowlist of the countries you actually verified. For example, if US is an approved rollout location for a job that used premium_proxy=true&proxy_country=us:

# Add this to the Python function's params construction below only after review.
params["proxy_type"] = "residential" # Or "datacenter" for a separate, cheaper canary.
country = "us".upper()
approved_countries = {"US"} # Your rollout allowlist, not a complete service list.
if country not in approved_countries:
raise ValueError("Country is not approved for this rollout")
params["proxy_country"] = country

The service can reject an unavailable/unsupported country with API 422. Treat it as a configuration decision; do not retry unchanged input or fall back to another location without checking the output requirement.

For JavaScript rendering with residential routing, set browser=true and proxy_type=residential. Add wait_for_selector only with the browser enabled. Review cost before canarying:

ZenRows Fetch modeZenRows creditsScrapingAnt modeScrapingAnt credits
Basic request1Datacenter, browser=false1
js_render=true5Datacenter, browser=true10
premium_proxy=true10Residential, browser=false25
js_render=true + premium_proxy=true25Residential, browser=true125

ScrapingAnt also offers residential browser page source (browser=true&return_page_source=true) at 50 credits, and datacenter page source at 2. Both bill successful requests only, as described in the summary above. The two credit units have different prices, so compare them only through the price per credit on your plan. See ScrapingAnt costs and ZenRows pricing.

Rewrite browser instructions​

For a read-only page that reveals more content, a ZenRows instruction sequence might be:

[{"click":".read-more"},{"wait":500}]

One possible ScrapingAnt rewrite is:

import base64

snippet = """
const button = document.querySelector('.read-more');
if (!button) throw new Error('Missing .read-more button');
button.click();
await new Promise(resolve => setTimeout(resolve, 500));
"""
params["browser"] = "true"
params["js_snippet"] = base64.b64encode(snippet.encode("utf-8")).decode("ascii")

This is a rewrite to review on your page, not an automatic instruction translator. The click above must only reveal content; do not use the retry examples for a form submission, purchase or other state-changing interaction. Base64 encodes the rewritten JavaScript, not the ZenRows JSON. For readiness before capture, prefer a meaningful selector condition where appropriate. A selector's presence alone does not prove that all fields or asynchronous updates are complete. See snippet execution for lifecycle and time constraints.

Adapt response and error handling​

/v2/general returns the fetched body, or browser-rendered HTML when enabled. A successful outer API response is HTTP 200; check Ant-page-status-code for the target's status. Target headers are exposed under Ant-original-header-*, and Ant-credits-cost records the request's credit charge. Do not keep code that expects ZenRows Zr-Final-Url, Zr-Cookies, X-Request-Id, concurrency or cost headers to have the same names or meaning; no ScrapingAnt request-ID header is documented, and target cookies are available as cookies in the extended JSON below.

If you need a JSON envelope, /v2/extended uses the same input parameters but returns html, text, cookies, status_code, headers, xhrs and iframes. Its status_code is the target's status, separate from the outer API HTTP status. A parser expecting the HTML body must switch to the html field. See extended responses; do not treat its JSON as ZenRows network-capture JSON.

Observed resultAction
API 200, target 200Validate required fields and content; a 200 can still be an unwanted page. Record credits without logging the key.
API 200, target 403/404/500Apply your target-response policy. This is not an API authentication or detection status.
API 403Read JSON detail: wrong token, exhausted quota or IP restriction. Stop and fix access; no unchanged retries.
API 422 / 405Correct parameters, plan restrictions or method first.
API 409Free-plan concurrency: serialize the queue. The examples allow a small delayed retry budget for an overlap.
API 423Target-site detection. Use a small retry budget for safe reads; if persistent, stop and review settings deliberately.
API 404ScrapingAnt could not reach the site or timed out (unreachable site). The examples stop: check reachability, snippets and the API timeout before resending.
API 500Temporary server failure. The examples retry within a small budget.
API 3xx or other statusesNot part of the documented contract. The examples stop without following redirects, so the key is never resent to another host.
Network timeoutOutcome is unknown. Avoid immediately multiplying requests; inspect history/outcome and resume deliberately.

The examples below use the same policy as the other migration guides: they retry only API 409, 423 and 500, at most three total attempts, with increasing jittered delays and unchanged modes. They stop on other statuses, including API 404 and redirects, and on transport failures. See the full error reference and safe retries. Attempts that end in an API error are not charged; bound the queue and total workload budget as well as each request's attempts.

Python: bounded HTML GET​

Use Python 3.10+ and install Requests (tested with 2.32: python -m pip install requests==2.32.5). Save this as fetch_html.py. Set SCRAPINGANT_API_KEY locally before running it. The default request below is plain HTTP with datacenter routing; edit the explicit params only after checking the mapping and costs above.

import os
import random
import time

import requests


def fetch_html(target, api_key):
params = {
"url": target,
"browser": "false",
"proxy_type": "datacenter",
}
retryable = {409, 423, 500}
for attempt in range(3):
try:
response = requests.get(
"https://api.scrapingant.com/v2/general",
params=params,
headers={"x-api-key": api_key},
timeout=(5, 75), # Connect and read timeouts, in seconds.
allow_redirects=False, # Never resend the x-api-key header to a redirect target.
)
except requests.RequestException:
# Avoid exposing a credential-bearing request/exception in logs.
raise RuntimeError("Transport failure; check outcome before resuming") from None
try:
if response.status_code in retryable:
if attempt == 2:
raise RuntimeError(f"API retry budget exhausted ({response.status_code})")
elif response.status_code != 200:
# Inspect detail privately against /errors; never log a raw response.
raise RuntimeError(f"API rejected request ({response.status_code}); check /errors")
else:
target_status = response.headers.get("Ant-page-status-code")
# Requests assumes ISO-8859-1 for text/* without a charset; default to UTF-8 instead.
has_charset = "charset=" in response.headers.get("Content-Type", "").lower()
encoding = response.encoding if has_charset and response.encoding else "utf-8"
if target_status != "200":
raise RuntimeError(f"Unexpected target status: {target_status or 'missing'}")
return {
"html": response.content.decode(encoding, errors="replace"),
"credits": response.headers.get("Ant-credits-cost"),
}
finally:
response.close()
time.sleep((2 ** attempt) + random.uniform(0, 0.25))


if __name__ == "__main__":
result = fetch_html("https://example.com/?page=2&sort=new", os.environ["SCRAPINGANT_API_KEY"])
print(result["html"]) # Feed your parser; assert required fields before accepting it.

Requests' read timeout measures inactivity, not an absolute whole-job deadline. These per-attempt timeouts and three-attempt limit bound normal retries; a production scheduler should also enforce a total elapsed-time and credit budget. Do not requeue a timeout blindly or layer this loop over an SDK retry policy without counting total attempts.

JavaScript: bounded HTML GET​

Use Node.js 22+ with its built-in fetch. Save this as fetch-html.mjs, set SCRAPINGANT_API_KEY, and run node fetch-html.mjs. The abort timer stays active while reading the response body.

import { setTimeout as delay } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';

export async function fetchHtml(target, apiKey) {
const url = new URL('https://api.scrapingant.com/v2/general');
url.search = new URLSearchParams({
url: target,
browser: 'false',
proxy_type: 'datacenter',
}).toString();
const retryable = new Set([409, 423, 500]);
for (let attempt = 0; attempt < 3; attempt++) {
let response, body;
try {
response = await fetch(url, {
headers: { 'x-api-key': apiKey },
signal: AbortSignal.timeout(75_000),
redirect: 'manual', // Never resend the x-api-key header to a redirect target.
});
body = await response.text();
} catch {
throw new Error('Transport failure; check outcome before resuming');
}
if (retryable.has(response.status)) {
if (attempt === 2) throw new Error(`API retry budget exhausted (${response.status})`);
await delay(1000 * (2 ** attempt) + Math.random() * 250);
continue;
}
if (response.status !== 200) {
// Inspect detail privately against /errors; never log a raw response.
throw new Error(`API rejected request (${response.status}); check /errors`);
}
const targetStatus = response.headers.get('Ant-page-status-code');
if (targetStatus !== '200') {
throw new Error(`Unexpected target status: ${targetStatus || 'missing'}`);
}
return { html: body, credits: response.headers.get('Ant-credits-cost') };
}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const key = process.env.SCRAPINGANT_API_KEY;
if (!key) throw new Error('Set SCRAPINGANT_API_KEY');
const result = await fetchHtml('https://example.com/?page=2&sort=new', key);
console.log(result.html); // Feed your parser and validate required fields.
}

Both examples accept only target HTTP 200 as an application choice. If a target 404 is useful to your workflow, change that policy explicitly; do not classify it as a ScrapingAnt API 404. The examples intentionally make no automatic proxy/browser escalation and do not preserve a persistent IP session.

Canary and rollback​

  1. Inventory: record current ZenRows manual modes, country needs, selectors, parser fields, expected target statuses and any session/auto-mode dependencies. Stop if the eligibility checklist is not met.
  2. Freeze acceptance criteria: choose representative allowed URLs and expected fields; set your maximum credit cost per useful result, latency budget, error threshold, request count and total spend limit before making provider requests. Include static and rendered pages separately if you use both.
  3. Validate offline first: replay saved HTML and mocked API/target statuses through the parser and error path.
  4. Canary deliberately: keep ZenRows available. Route a small bounded traffic slice to ScrapingAnt with fixed modes and a conservative worker limit. Record outer status, target status, required-field checks, elapsed time and Ant-credits-cost, without credentials or sensitive target data. Free-plan checks must be sequential and use eligible domains.
  5. Expand after acceptance: compare useful results and cost against the frozen criteria. Raise workers gradually within target and workload budgets. A timeout may leave work processing, so avoid overlapping replacement requests.
  6. Rollback on a failed criterion: stop new ScrapingAnt scheduling, account for in-flight work, and send subsequent safe jobs through the retained ZenRows route. Diagnose parser, access, country or mode differences before resuming. Do not automatically replay state-changing interactions.

After cutover, recheck the published plans and comparison, monitor useful responses rather than HTTP 200 alone, and retire the old credential only after the rollback window closes. Never copy keys into diagnostics or support messages.

Official references​