Skip to main content
Batch is available to every Zenrows account. If requests fail with 401 unauthenticated, check that your API key is present and valid before working through the steps below.
Most Batch 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.

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. The error’s type links to the code’s entry. See the table in the API reference, and every code in API error codes.
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) and the X-Request-ID response header, which every API response carries. 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, or the account can’t use the API.
  • Send the key in the X-API-Key header on every request. Never put it in the URL.
  • Use the same key as your Fetch. No separate Batch key exists.
401 subscription_inactive means the API key is valid, but the subscription needs a billing action. The detail reads “Subscription is not active.”
  • Past due or unpaid: update your payment method in Billing.
  • Incomplete: finish the pending payment, or subscribe again if it expired.
  • Paused: resume the subscription.
  • Subscribed through Azure Marketplace: restore or renew the subscription in Azure Marketplace.
Contact support if none of these apply. Retry once the subscription is active again. 402 payment_required means your account has no available credits, its subscription has expired, or its plan doesn’t include the API. Top up or change your plan in Billing, then resubmit. A run already in progress that runs out ends failed with failure_reason: insufficient_credits; rerun its failed and pending tasks once you have credits. 402 api_key_cap_reached means the API key reached one of its credit caps. The detail names the cap and when it resets. Raise or remove the cap, or wait for it to reset, then retry. See API key credit caps and Batch.

Submission Errors

400 invalid_argument means the request body failed validation. The response includes an invalid_tasks array listing each problem as {index, reason, value}, so you can see exactly which URL failed and why. Each reason has an entry in API error codes. 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 accept only true / false (or "true" / "false").
  • Unsupported parameters. Batch rejects keys it doesn’t accept, such as the screenshot parameters (unknown_param).
  • Invalid external_id or metadata. external_id takes only letters, digits, ., _ and -, up to 128 characters; put free text, such as a search keyword, in metadata (string values only).
  • A POST task with js_render, js_instructions or json_response (method_param_conflict). POST tasks run without a browser; drop those parameters for them.
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. The upload link works for 30 minutes and the uploaded file for 24 hours.

Account Limits

429 quota_exceeded means you’ve hit an account limit, and the detail says which one. By default an account runs 3 active jobs, shared with Crawl, and keeps 5 scheduled jobs; an open job holds 100,000 tasks at most. An open job counts as active even after you stop its run. A job holds a slot until you delete it, or until it’s closed and its latest run has ended (completed, stopped or failed), so closing a job whose run is still going doesn’t free its slot; deleting frees it at once. An open job that receives no tasks for 48 hours is closed automatically, so close open jobs you’re done with. Wait for a run to end, or delete a job, before submitting another. Retrying the same request doesn’t help until a slot frees up; the same Idempotency-Key works once it does. To run more than 3 at once, use the chat in your dashboard. Email success@zenrows.com if you can’t use the chat.

Job and Run State Conflicts

A 409 means the job is in a state that doesn’t allow your action. Check the code and detail to see which conflict it is.
  • Adding tasks to a closed job (409 job_not_open or 409 last_batch_received). 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 to rerun a job whose previous run is still pending or running, or to add tasks while the run is still writing its tasks. Stop it first, or wait for it to finish, then retry.
  • run_not_stoppable. You tried to stop, pause or resume a run that is already completed, failed or deleted. Start a rerun instead.
  • run_has_descendants. You tried to delete a run whose results a later partial rerun reuses. Delete the later runs first, or delete the whole job.
  • idempotency_key_conflict. The same Idempotency-Key came with a different body, or while the first attempt was still running. Use a new key for a new request.
  • Exporting a run that’s still in progress. An export doesn’t wait for the run to finish: it packs only the results stored so far. Export after the run reaches a terminal state to get every result.
  • Abandoning an in-flight run. To stop a run that’s still processing, call POST /jobs/{id}/stop (or its alias POST /jobs/{id}/cancel). To suspend it without ending it, use POST /jobs/{id}/pause and later POST /jobs/{id}/resume. See Control the current run.

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. A successful row has no result_url, or reads expired. Results are kept for 14 days by default after each task finishes. After that the row reads status: "expired", has no result_url, and a status=successful filter still returns it, so skip such rows. Rerun the job to scrape those pages again. Fetching a single task’s content returns a non-200 status. The /tasks/{id}/content endpoint uses status to signal task state: A bulk export fails or its link stops working. The download_url is valid for 2 hours and is presigned fresh on each poll, so always use the latest one. The export itself expires 12 hours after creation; after that, polling it returns 410 result_expired, so start a new export. An export of a partial-rerun run holds only the tasks that run scraped itself; download the carried-over results from the earlier run. If a run’s combined results exceed 100 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 a partial rerun returns 409 partial_rerun_window_expired, more than 10 days have passed since the run you’re retrying started. Start a full rerun instead.
  • If a partial rerun returns 409 no_matching_tasks, no task has the statuses you asked for. A scheduled run refused at start has no tasks at all; run a full rerun.
  • A run that ends failed with a failure_reason (insufficient_credits, subscription_inactive, api_key_cap_reached) stopped for an account reason, not because of its targets. Fix the account, then rerun with ?status=failed,pending.
If many tasks fail on a protected site, the cause is usually the scrape configuration, not Batch. 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 Fetch.

Webhooks Not Arriving

The API sends a run.completed event when a run completes, or a run.failed event when an account-level error ends it. Stopping a run yourself sends no event. 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. A retried delivery can arrive more than once. Deduplicate on the X-ZenRows-Event-Id header.
  • Answer fast. Return a 2xx within 10 seconds, then process the event. A slower answer counts as a failed delivery and is retried with backoff (30 seconds, growing up to 8 hours) for up to 3 days.
  • 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, with the secret base64-decoded to bytes, and pick the secret by the header’s kid during a key rotation. The SDKs’ verify_webhook_signature, verifyWebhookSignature and VerifyWebhookSignature helpers do this for you. See Verifying signed webhooks.
Webhooks are a convenience, not the source of truth. Delivery is best effort: a delivery that keeps failing is dropped after 3 days of retries, a retried event can arrive twice, and stopping a run yourself sends no event. 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.
  • A fire was skipped. A scheduled fire is skipped while the job’s previous run is still pending or running.
  • A scheduled run ended failed with no tasks. The account was out of credits, its subscription was inactive, or the key was capped when the run started. Fix that, then run a full rerun or wait for the next fire.
  • Pausing and resuming. Change state with POST /jobs/{id}/schedule/state using {"schedule_state": "paused"} or {"schedule_state": "active"}.

Unexpected Cost

  • Target 404 and 410 responses are charged. They’re billed as on Fetch, even though Batch records those tasks as failed. Other failed tasks aren’t billed. A task’s attempt history shows what each attempt was charged. This doesn’t push a run over its estimate, which already prices every task as a success, but it means a run with these failures costs as much as one without them.
  • 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 credit cost is dynamic (1 to 25) depending on what the site requires, so the estimate is a range rather than a single number.

Transient Errors

500 internal or 503 internal is a 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 API reference. If a retried submit with the same idempotency key returns 409 idempotency_key_conflict, the job was created before the error: list your recent jobs to find it. A late 503 can 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.