Skip to main content
The Batch Scraper API is in public beta, available to every ZenRows account. You need an active ZenRows API key. The ZenRowsBatchClient shown here ships under the zenrows/batch subpath, separate from the SDK’s synchronous scraping client, ZenRows.
Use this guide to submit batch scraping jobs, track them, and collect results with the official ZenRows Node.js SDK. The client is fully typed, with request and response models generated from the API’s OpenAPI spec and exposed as camelCase (job.jobId, run.data.stats.successful, row.resultUrl).
Prefer raw HTTP, or work in another language? The REST API guide covers the same use cases against the plain REST API. For the complete reference, see the SDK repository on GitHub.

Install and Authenticate

While the batch client is being published to npm, install the SDK from the beta branch:
This branch install is temporary. The batch client is not on npm yet; once it is published, npm install zenrows will include it.

Quickstart: Submit, Wait, Get Results

submitRegular takes one params object: a task source (urls, bare strings or per-task objects, or fileInputId) plus optional job fields. It returns a JobRef. job.run.wait() blocks until the current run is terminal and returns its RunHandle, whose .data snapshot is ready. results() is an async stream you can iterate with for await or .forEach.
Need full control over the request body? client.submitJob({...}) accepts the raw (camelCase) wire shape and returns the same JobRef. For very large submissions that return 202 Accepted, pass waitForIngest: true so results pages are complete before you read them.

Configuring the Scrape

zenrowsParams sets scraping options for every URL in the job. The parameter keys themselves stay in their API form (js_render, premium_proxy, and so on).
Other common keys include js_instructions, wait, block_resources, json_response, css_extractor, autoparse, outputs, original_status, allowed_status_codes, and session_id. ZenRows derives the output format from these and records it on each result. Custom request headers (custom_headers) are not supported yet.

Per-URL Options and Correlating Results

To set a per-task externalId, metadata, or zenrowsParams, pass objects instead of bare strings. Per-task zenrowsParams override the job-level ones on a key clash.
The API returns your externalId unchanged on every result, so you can match results back to your own records. It must match ^[A-Za-z0-9._-]+$ and be 128 characters or fewer; the SDK validates this before sending.

Downloading Content

results() gives you metadata plus a presigned resultUrl per task, and the SDK downloads bodies straight from that URL with no extra API round-trip. Run these on the current run (job.run.*) or any specific run (client.getRun(jobId, runId)).
downloadAllResults is a server-side zip capped at 1 GiB per run. For larger runs, or one file per task, use downloadToDir, which fetches bodies client-side with no size limit.

Open Jobs: Streaming URLs in Batches

When you don’t have every URL up front, open a job with submitOpen, add tasks in batches, and close it. The run stays active and completes once it finishes the final batch.
Setting lastBatch: true (or calling job.close()) locks the job, and the API rejects further adds. CSV file input is not supported for open jobs; use a closed job for CSV.

Large Inputs: CSV Upload

For lists too large to inline, upload a CSV once and reference it. uploadCsv allocates the slot, PUTs your file to the presigned URL, and returns a fileInputId you pass to a submit call.
Each CSV row becomes a task. Only the url and optional externalId columns are read, and the job-level zenrowsParams apply to all of them. Limits: 50 MB / 100,000 rows.

Listing and Pagination

The SDK offers both single-page calls (listJobs, listRuns, listResults) and auto-paginating async streams that yield loaded handles.
Each iterator is an AsyncStream, so you can also use .forEach(...) instead of for await.

Reruns and Retrying Failures

Each rerun creates a brand-new run on the same job.

Scheduled Jobs

Submit a recurring job with a typed schedule. The schedule is a discriminated union: { every } interval, { at, tz } one-shot, { times, tz } daily, { times, days, tz } weekly, or { times, dates, tz } monthly. The compiler enforces exactly one shape and a required tz (except for every).
A job carries two facets that match the API’s two scopes. job.schedule.* controls the recurring schedule, and job.run.* controls the current run. This disambiguates the API’s two distinct “pause” actions:
For a specific historical run, use client.run(jobId, runId), a RunRef with read and download operations only.

Completion Notifications: Webhooks

Attach a webhook at submit time, and ZenRows POSTs a run.completed event to your HTTPS URL when a run finishes. Set signature: true for HMAC-signed deliveries (see Signed Webhooks); it defaults to unsigned. You can also manage it later with client.putJobWebhook(...) / client.deleteJobWebhook(...), and probe a receiver with client.testWebhook(...).
Deliveries are at-least-once, so deduplicate on the X-ZenRows-Event-Id header.
No webhook receiver? Just poll for completion with job.run.wait() or run.wait().

Bulk Export (ZIP)

Bundle an entire run’s result bodies into a single server-side zip: downloadAllResults starts the export, waits for it, and streams the file to disk.
The server-side zip is capped at 1 GiB per run. For larger runs, or one file per task, use downloadToDir, which fetches bodies client-side with no size limit. For manual control, client.startResultsExport(...) and client.waitForExport(...) expose the two steps separately.

Estimating Cost

Estimate usage before submitting with client.estimateCost. It takes the same body as submitJob and runs client-side with no API call. The tiers are 1 (base), 5 (js_render), 10 (premium_proxy), 25 (both), and a dynamic 1 to 25 for mode=auto.
An estimate assumes each URL succeeds once; it is a single number unless the job uses mode=auto, which yields a range.

Checking Actual Cost

Once a run finishes, read the actual usage it consumed from run.data.stats.spend ({ credits, cost }).
Only successful requests are charged, so realized spend is at most the estimate. Per-task spend is indicative and often absent on successful rows, so rely on the run-level rollup for actuals. Your ZenRows account statement remains the authoritative record for billing.

Signed Webhooks: HMAC Keys

Signed deliveries (see Webhooks) need an active HMAC key for your organization. Rotation uses two slots, active plus candidate, so you can roll keys without downtime.
Your receiver verifies the X-Signature: t=<unix>,v1=<hex>,kid=<active> header by recomputing HMAC-SHA256(secret, "<t>.<raw_body>") and comparing the result to v1, selecting the secret by kid.

Error Handling

Every non-2xx surfaces as BatchApiError. Its code property carries the stable code from the RFC 7807 body.
Task-level failures don’t throw. They appear as failed rows in results(), each with an error object you can read through row.error.code and row.error.detail.

Timeouts and Retries

Transient failures (429, 502, 503, 504, and network errors) are retried automatically with jittered exponential backoff, honoring Retry-After. Retries apply only to idempotent requests: GET / PUT / DELETE, plus POSTs that carry an idempotencyKey (submit and rerun). Tune both through the constructor.

Idempotency

Pass idempotencyKey to a submit or rerun to guard against accidentally creating duplicate jobs. Each key can be used once: a later call that reuses the same key raises a 409 conflict, whether or not the body is identical, and the API does not store or replay the original result. It is therefore not a safe-retry token, so if a call fails midway, check whether the job was created before retrying with a new key.
The full Batch surface (jobs, runs, tasks, results, content, history, file inputs, and HMAC keys) is reachable through methods on ZenRowsBatchClient. See the SDK repository on GitHub.