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.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-Keyheader 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.
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
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_renderaccept onlytrue/false(or"true"/"false"). - Unsupported parameters. Batch rejects keys it doesn’t accept, such as the screenshot parameters (
unknown_param). - Invalid
external_idormetadata.external_idtakes only letters, digits,.,_and-, up to 128 characters; put free text, such as a search keyword, inmetadata(string values only). - A POST task with
js_render,js_instructionsorjson_response(method_param_conflict). POST tasks run without a browser; drop those parameters for them.
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
A409 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_openor409 last_batch_received). Once you sendlast_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 to rerun a job whose previous run is stillpendingorrunning, 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 alreadycompleted,failedordeleted. 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 sameIdempotency-Keycame 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 aliasPOST /jobs/{id}/cancel). To suspend it without ending it, usePOST /jobs/{id}/pauseand laterPOST /jobs/{id}/resume. See Control the current run.
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.
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’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.
- 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
failedwith afailure_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.
Webhooks Not Arriving
The API sends arun.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/testwith your receiver URL to confirm the API can reach it. The response reportsdeliveredandstatus_code. - Deduplicate, don’t assume once. A retried delivery can arrive more than once. Deduplicate on the
X-ZenRows-Event-Idheader. - Answer fast. Return a
2xxwithin 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 theX-Signatureheader against the raw request body, not a re-serialized object, with the secret base64-decoded to bytes, and pick the secret by the header’skidduring a key rotation. The SDKs’verify_webhook_signature,verifyWebhookSignatureandVerifyWebhookSignaturehelpers do this for you. 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. - A fire was skipped. A scheduled fire is skipped while the job’s previous run is still
pendingorrunning. - A scheduled run ended
failedwith 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/stateusing{"schedule_state": "paused"}or{"schedule_state": "active"}.
Unexpected Cost
- Target
404and410responses are charged. They’re billed as on Fetch, even though Batch records those tasks asfailed. 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(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.