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.| Symptom | Likely cause | Go to |
|---|---|---|
401 on every request | Missing or wrong API key, or no beta access | Authentication and access |
400 invalid_argument on submit | Malformed body, bad URL, or batch over the limit | Submission errors |
429 quota_exceeded on submit | Too many active jobs (max 3) | Account limits |
409 conflict when adding tasks | The job is already closed | Job and run state conflicts |
| Download link returns an error | The presigned URL expired | Results and downloads |
| Some URLs never returned data | Task-level failures, not request errors | Tasks that fail |
| Webhook never arrived | Receiver not HTTPS, or signature setup | Webhooks not arriving |
| Schedule didn’t fire as expected | Unsupported time value or timezone | Scheduled jobs |
| Spent more usage than estimated | Retries or mode=auto range | Unexpected cost |
503 responses | Transient server error | Transient errors |
How to Diagnose Any Request
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.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.Authentication and Access
401 unauthenticated means the API key is missing or invalid.
- Send the key in the
X-API-Keyheader 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
urlis a complete, valid address. Fix any entry flagged ininvalid_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 asjs_renderare sent as"true"/"false".
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: trueor callclose, 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}/taskson an open job can return409during 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
Aresult_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:
| Status | Meaning |
|---|---|
200 | Content is ready; the body is the scraped result. |
409 | The task is still pending or processing. Poll again shortly. |
422 | The task failed; the body is the stored error. |
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’serrorobject (error.code,error.detail) to see what went wrong. - A failed task’s results row already carries the error, so you rarely need the
/contentendpoint 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.
Webhooks Not Arriving
The API sends arun.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/testwith your receiver URL to confirm the API can reach it. The response reportsdeliveredandstatus_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-Idheader. - Signature verification fails. Signed deliveries (
signature: true) require an active HMAC key for your organization. Verify theX-Signatureheader against the raw request body, not a re-serialized object. See Verifying signed webhooks.
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/Berlinkeep daylight-saving transitions correct. Pair a timezone withatandcalendarschedules. - Provide exactly one schedule type. Send one of
at,rate, orcalendarper scheduled job. - Pausing and resuming. Change state with
POST /jobs/{id}/schedule/stateusing{"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(andrun_idif the issue is run-specific). - The exact request body, with your API key redacted.
- The HTTP status and the full error
codeanddetail. - Whether the issue is reproducible or intermittent, and when it started.