Skip to main content
This guide walks you through setting up the ZenRows® Batch Scraper API. You’ll confirm your access, locate your API key, authenticate, submit your first batch job, and download the results end to end.
The Batch Scraper API is currently in private beta. Access is granted per account, so you need an invitation before you can submit jobs. Your existing Universal Scraper API key works without any extra setup.

Initial Setup

Follow these steps to get ready:
1

Sign in to ZenRows

Visit the Registration Page to create an account, or log in if you already have one. The Batch Scraper API uses the same API key as the Universal Scraper API, which you’ll find in your dashboard.
2

Confirm private-beta access

The Batch Scraper API is invite-only during the beta. If requests return 401 unauthenticated with a valid key, your account isn’t enabled yet. If this is the case, reach out to the ZenRows team to request access.
3

Note the base URL

All REST requests go to https://async.api.zenrows.com/v1.

Authentication

Authenticate every request with your API key in the X-API-Key header. For security, the key travels in a header and never in the URL.
X-API-Key: YOUR_ZENROWS_API_KEY
Store the key in an environment variable so your code can read it instead of hard-coding it:
export ZENROWS_API_KEY="YOUR_ZENROWS_API_KEY"

Submit Your First Job

This minimal example submits a closed job (one where you know all URLs up front) with JavaScript rendering enabled. Replace YOUR_ZENROWS_API_KEY with your key.
## pip install requests
import requests, json

headers = {'X-API-Key': 'YOUR_ZENROWS_API_KEY', 'Content-Type': 'application/json'}
payload = {
    'type': 'regular',
    'status': 'closed',
    'zenrows_params': {'js_render': 'true'},
    'tasks': [
        {'url': 'https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie'},
        {'url': 'https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket'},
    ],
}
response = requests.post('https://async.api.zenrows.com/v1/jobs', headers=headers, data=json.dumps(payload))
job = response.json()
print('submitted', job['job_id'], job['latest_run']['status'])
// npm install axios
const axios = require('axios');

axios({
    url: 'https://async.api.zenrows.com/v1/jobs',
    method: 'POST',
    headers: { 'X-API-Key': 'YOUR_ZENROWS_API_KEY' },
    data: {
        type: 'regular',
        status: 'closed',
        zenrows_params: { js_render: 'true' },
        tasks: [
            { url: 'https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie' },
            { url: 'https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket' },
        ],
    },
})
    .then(response => console.log('submitted', response.data.job_id, response.data.latest_run.status))
    .catch(error => console.log(error));
import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.core5.http.ContentType;

public class FirstJob {
    public static void main(final String... args) throws Exception {
        String body = "{"
            + "\"type\":\"regular\",\"status\":\"closed\","
            + "\"zenrows_params\":{\"js_render\":\"true\"},"
            + "\"tasks\":["
            + "{\"url\":\"https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie\"},"
            + "{\"url\":\"https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket\"}"
            + "]}";
        String response = Request.post("https://async.api.zenrows.com/v1/jobs")
                .addHeader("X-API-Key", "YOUR_ZENROWS_API_KEY")
                .bodyString(body, ContentType.APPLICATION_JSON)
                .execute().returnContent().asString();
        System.out.println(response);
    }
}
<?php
$payload = json_encode([
    'type' => 'regular',
    'status' => 'closed',
    'zenrows_params' => ['js_render' => 'true'],
    'tasks' => [
        ['url' => 'https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie'],
        ['url' => 'https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket'],
    ],
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://async.api.zenrows.com/v1/jobs');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_ZENROWS_API_KEY', 'Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
echo $response . PHP_EOL;
curl_close($ch);
?>
package main

import (
    "bytes"
    "io"
    "log"
    "net/http"
)

func main() {
    payload := []byte(`{
        "type": "regular",
        "status": "closed",
        "zenrows_params": { "js_render": "true" },
        "tasks": [
            { "url": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie" },
            { "url": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket" }
        ]
    }`)
    req, err := http.NewRequest("POST", "https://async.api.zenrows.com/v1/jobs", bytes.NewBuffer(payload))
    if err != nil {
        log.Fatalln(err)
    }
    req.Header.Set("X-API-Key", "YOUR_ZENROWS_API_KEY")
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatalln(err)
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    log.Println(string(body))
}
# gem install faraday
require 'faraday'
require 'json'

payload = {
    type: 'regular',
    status: 'closed',
    zenrows_params: { js_render: 'true' },
    tasks: [
        { url: 'https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie' },
        { url: 'https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket' }
    ]
}
conn = Faraday.new
res = conn.post('https://async.api.zenrows.com/v1/jobs') do |req|
    req.headers['X-API-Key'] = 'YOUR_ZENROWS_API_KEY'
    req.headers['Content-Type'] = 'application/json'
    req.body = payload.to_json
end
print(res.body)
curl -X POST "https://async.api.zenrows.com/v1/jobs" \
  -H "X-API-Key: YOUR_ZENROWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "regular",
    "status": "closed",
    "zenrows_params": { "js_render": "true" },
    "tasks": [
      { "url": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie" },
      { "url": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket" }
    ]
  }'
The response includes a job_id and the initial run status. Poll GET /jobs/{id} until the run reaches a terminal state (finished and no longer changing), then collect the results.

Get Your Results

Once the run is complete, list the successful tasks and download each one’s scraped content. Every result row carries a result_url, a presigned link to the scraped page that is valid for 2 hours. Replace JOB_ID with the job_id from the submit response.
## pip install requests
import requests

headers = {'X-API-Key': 'YOUR_ZENROWS_API_KEY'}
page = requests.get('https://async.api.zenrows.com/v1/jobs/JOB_ID/results',
                    headers=headers, params={'status': 'successful'}).json()
for row in page['results']:
    html = requests.get(row['result_url']).text  # the scraped page
    print(row.get('external_id') or row['task_id'], len(html), 'chars')
// npm install axios
const axios = require('axios');

const page = (await axios.get('https://async.api.zenrows.com/v1/jobs/JOB_ID/results', {
    headers: { 'X-API-Key': 'YOUR_ZENROWS_API_KEY' },
    params: { status: 'successful' },
})).data;
for (const row of page.results) {
    const html = (await axios.get(row.result_url)).data; // the scraped page
    console.log(row.external_id ?? row.task_id, String(html).length, 'chars');
}
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.fluent.Request;

public class GetResults {
    public static void main(final String... args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode page = mapper.readTree(
                Request.get("https://async.api.zenrows.com/v1/jobs/JOB_ID/results?status=successful")
                        .addHeader("X-API-Key", "YOUR_ZENROWS_API_KEY")
                        .execute().returnContent().asString());
        for (JsonNode row : page.path("results")) {
            String html = Request.get(row.path("result_url").asText())
                    .execute().returnContent().asString(); // the scraped page
            String id = row.path("external_id").asText(row.path("task_id").asText());
            System.out.println(id + " " + html.length() + " chars");
        }
    }
}
<?php
$ch = curl_init('https://async.api.zenrows.com/v1/jobs/JOB_ID/results?status=successful');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_ZENROWS_API_KEY']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$page = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($page['results'] as $row) {
    $html = file_get_contents($row['result_url']); // the scraped page
    echo ($row['external_id'] ?? $row['task_id']) . ' ' . strlen($html) . ' chars' . PHP_EOL;
}
?>
package main

import (
    "encoding/json"
    "io"
    "log"
    "net/http"
)

func main() {
    req, _ := http.NewRequest("GET", "https://async.api.zenrows.com/v1/jobs/JOB_ID/results?status=successful", nil)
    req.Header.Set("X-API-Key", "YOUR_ZENROWS_API_KEY")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatalln(err)
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    var page struct {
        Results []struct {
            TaskID     string `json:"task_id"`
            ExternalID string `json:"external_id"`
            ResultURL  string `json:"result_url"`
        } `json:"results"`
    }
    json.Unmarshal(body, &page)
    for _, row := range page.Results {
        r, _ := http.Get(row.ResultURL) // the scraped page
        html, _ := io.ReadAll(r.Body)
        r.Body.Close()
        name := row.ExternalID
        if name == "" {
            name = row.TaskID
        }
        log.Println(name, len(html), "chars")
    }
}
# gem install faraday
require 'faraday'
require 'json'

page = JSON.parse(Faraday.get('https://async.api.zenrows.com/v1/jobs/JOB_ID/results?status=successful') { |req|
    req.headers['X-API-Key'] = 'YOUR_ZENROWS_API_KEY'
}.body)
page['results'].each do |row|
    html = Faraday.get(row['result_url']).body # the scraped page
    puts "#{row['external_id'] || row['task_id']} #{html.length} chars"
end
# 1) List the successful results; each row has a presigned result_url valid 2h.
curl "https://async.api.zenrows.com/v1/jobs/JOB_ID/results?status=successful" \
  -H "X-API-Key: YOUR_ZENROWS_API_KEY"

# 2) Download one result's scraped HTML using its result_url from the response above.
curl "RESULT_URL"
Want to see the cost before you run anything? You can estimate usage client-side with no API call. See Estimate cost.

Next Steps

Developer Guide: REST API

Every feature against the REST API: open jobs, CSV upload, scheduling, webhooks, and more.

Troubleshooting

Diagnose authentication, submission, download, and webhook issues fast.

FAQ

Quick answers on access, pricing, limits, and scraping options.