Skip to main content
The Batch Scraper API is in private beta. If requests fail with 401 unauthenticated even though your key is valid, confirm your account has been granted beta access before working through the steps below.
Most Batch Scraper API problems fall into a handful of categories. Find your symptom in the table below, then jump to the section that explains the cause and the fix.
SymptomLikely causeGo to
401 on every requestMissing or wrong API key, or no beta accessAuthentication and access
400 invalid_argument on submitMalformed body, bad URL, or batch over the limitSubmission errors
429 quota_exceeded on submitToo many active jobs (max 3)Account limits
409 conflict when adding tasksThe job is already closedJob and run state conflicts
Download link returns an errorThe presigned URL expiredResults and downloads
Some URLs never returned dataTask-level failures, not request errorsTasks that fail
Webhook never arrivedReceiver not HTTPS, or signature setupWebhooks not arriving
Schedule didn’t fire as expectedUnsupported time value or timezoneScheduled jobs
Spent more usage than estimatedRetries or mode=auto rangeUnexpected cost
503 responsesTransient server errorTransient errors

How to Diagnose Any Request

1

Read the HTTP status and error code

Every error response is JSON in the application/problem+json format and carries a stable code (for example invalid_argument or conflict). The detail field usually states the cause directly. See the full table in the developer guide.
2

Separate request errors from task errors

A non-2xx response means the API rejected your request. A 2xx response with failed results means individual URLs failed while the job itself succeeded. These are handled differently. See Tasks that fail.
3

Keep the job ID and request ID

Note the job_id (and run_id where relevant). If you need to contact support, include them so the team can trace your job in the logs.

Authentication and Access

401 unauthenticated means the API key is missing or invalid.
  • Send the key in the X-API-Key header on every request. Never put it in the URL.
  • Use the same key as your Universal Scraper API. No separate Batch Scraper API key exists.
  • If the key is correct but you still get 401, your account may not have private-beta access yet. Confirm access before retrying.
402 payment_required means your account has no available usage. Top up your plan, then resubmit.

Submission Errors

400 invalid_argument means the request body failed validation. The response includes an invalid_tasks array listing each problem as {index, reason}, so you can see exactly which URL failed and why. Common causes and fixes:
  • Malformed or unencoded URLs. Make sure each url is a complete, valid address. Fix any entry flagged in invalid_tasks.
  • Too many URLs. A job submit accepts up to 100,000 URLs, and each add-tasks call on an open job accepts up to 10,000. For very large lists, use a CSV upload.
  • Wrong value types in zenrows_params. Values can be strings, booleans, or numbers. Boolean options such as js_render are sent as "true" / "false".
CSV uploads must use the exact content-type the upload slot requires. When you PUT the file to the presigned upload URL, send the headers returned in slot.upload.headers (for example Content-Type: text/csv) unchanged. A mismatched content-type causes the upload to fail. Limits: 50 MB / 100,000 rows.

Account Limits

429 quota_exceeded means you’ve hit an account limit. During the private beta, an account can run 3 active jobs concurrently; submitting a 4th returns this error. Wait for one to finish, or contact ZenRows to raise the limit for your account. Beta limits are soft and can be raised per customer within minutes.

Job and Run State Conflicts

409 conflict means the job is in a state that doesn’t allow your action. Check the detail and code to see which conflict it is.
  • Adding tasks to a closed job. Once you send last_batch: true or call close, the job locks and rejects further adds. Open a new job for more URLs.
  • Ingestion still in progress. A very large submission keeps writing its task rows for a few seconds after responding. POST /jobs/{id}/tasks on an open job can return 409 during this window; wait a moment and retry.
  • run_not_terminal. You tried an action that requires the run to be finished (for example, exporting) while it’s still running. Wait for the run to reach a terminal state, then retry.
  • Abandoning an in-flight run. To stop a run that’s still processing, call POST /jobs/{id}/stop.

Results and Downloads

A result_url returns an error. Each result_url is a presigned URL (a temporary, signed link) valid for 2 hours. If it expired, re-list the results to get a fresh link rather than reusing a stored one. Don’t persist these URLs long-term. Fetching a single task’s content returns a non-200 status. The /tasks/{id}/content endpoint uses status to signal task state:
StatusMeaning
200Content is ready; the body is the scraped result.
409The task is still pending or processing. Poll again shortly.
422The task failed; the body is the stored error.
A bulk export fails or its link stops working. Exports expire 12 hours after creation, and the API presigns the download_url fresh on each poll, so always use the latest one. If a run’s combined results exceed 1 GiB, the export fails with a clear error instead of producing a partial archive. Download results in pages, or split the work across smaller runs.

Tasks That Fail

When the job succeeds but some URLs don’t return data, those are task-level failures, not request errors. They don’t raise an HTTP error or an exception.
  • List results filtered by status="failed" and read each row’s error object (error.code, error.detail) to see what went wrong.
  • A failed task’s results row already carries the error, so you rarely need the /content endpoint just to find out why it failed.
  • To re-run only the failures, use retry failed. Successful results carry over, so you only pay to re-scrape what failed.
If many tasks fail on a protected site, the cause is usually the scrape configuration, not the Batch Scraper API. Enable js_render for dynamic pages, add premium_proxy and proxy_country for blocked or geo-restricted sites, or use wait_for to wait for a selector before capturing, the same options you would use with the Universal Scraper API.

Webhooks Not Arriving

The API sends a run.completed event when a run finishes. If you don’t receive it:
  • Use an HTTPS receiver. Webhook URLs must use HTTPS.
  • Test the endpoint first. Call POST /webhook/test with your receiver URL to confirm the API can reach it. The response reports delivered and status_code.
  • Deduplicate, don’t assume once. Deliveries are at-least-once, so the same event can arrive more than once. Deduplicate on the X-ZenRows-Event-Id header.
  • Signature verification fails. Signed deliveries (signature: true) require an active HMAC key for your organization. Verify the X-Signature header against the raw request body, not a re-serialized object. See Verifying signed webhooks.
Webhooks are a convenience, not the source of truth. If you ever miss a delivery, poll the job. The run state and result URLs are always authoritative.

Scheduled Jobs

  • Times of day must be full hours. Use "09:00", not "09:30". A non-hour value is rejected.
  • Use IANA timezone names. Names like Europe/Berlin keep daylight-saving transitions correct. Pair a timezone with at and calendar schedules.
  • Provide exactly one schedule type. Send one of at, rate, or calendar per scheduled job.
  • Pausing and resuming. Change state with POST /jobs/{id}/schedule/state using {"schedule_state": "paused"} or {"schedule_state": "active"}.

Unexpected Cost

  • Only successful requests cost usage. A run with failures costs less than its up-front estimate, never more for the failed tasks.
  • The estimate assumes one success per URL. Compare it against the run’s actual spend with Checking actual cost.
  • Adaptive Stealth Mode is a range. With mode=auto, each task’s usage is dynamic (1 to 25) depending on what the site requires, so the estimate is a range rather than a single number.

Transient Errors

503 internal is a transient server-side error and is safe to retry. Use an increasing delay (a backoff) between attempts rather than a tight retry loop, as shown in the developer guide. On a 503 from a submit specifically, retry with a fresh idempotency key, since a key claimed by the failed attempt replays 409. A late 503 can also leave a visible job with a stopped run, which is safe to DELETE.

Still Stuck?

If a problem persists after you’ve worked through the relevant section above, reach out to support with:
  • The job_id (and run_id if the issue is run-specific).
  • The exact request body, with your API key redacted.
  • The HTTP status and the full error code and detail.
  • Whether the issue is reproducible or intermittent, and when it started.
This lets the team trace your job in the logs and respond without re-asking for the basics.