> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Diagnose and fix the most common Batch issues: authentication, submission errors, job state conflicts, downloads, webhooks, scheduling, and unexpected cost.

<Note>
  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.
</Note>

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.

| Symptom | Likely cause | Go to |
| - | - | - |
| `401` on every request | Missing or wrong API key | [Authentication and access](#authentication-and-access) |
| `400 invalid_argument` on submit | Malformed body, bad URL, or batch over the limit | [Submission errors](#submission-errors) |
| `429 quota_exceeded` on submit | Too many active jobs (max 3) | [Account limits](#account-limits) |
| `409 conflict` when adding tasks | The job is already closed | [Job and run state conflicts](#job-and-run-state-conflicts) |
| Download link returns an error | The presigned URL expired | [Results and downloads](#results-and-downloads) |
| Some URLs never returned data | Task-level failures, not request errors | [Tasks that fail](#tasks-that-fail) |
| Webhook never arrived | Receiver not HTTPS, or signature setup | [Webhooks not arriving](#webhooks-not-arriving) |
| Schedule didn't fire as expected | Unsupported time value or timezone | [Scheduled jobs](#scheduled-jobs) |
| Spent more credits than estimated | Retries or `mode=auto` range | [Unexpected cost](#unexpected-cost) |
| `503` responses | Transient server error | [Transient errors](#transient-errors) |

## How to Diagnose Any Request

<Steps>
  <Step title="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](/batch/developer-guide-restapi#error-handling-reference).
  </Step>

  <Step title="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](#tasks-that-fail).
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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 Fetch. No separate Batch key exists.

**`402 payment_required`** means your account has no available credits. Top up your plan, then resubmit.

**`402 api_key_cap_reached`** means the API key reached one of its [credit caps](/first-steps/api-key-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](/batch/developer-guide-restapi#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}`, 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](/batch/developer-guide-restapi#open-jobs-adding-tasks-in-batches) accepts up to **10,000**. For very large lists, use a [CSV upload](/batch/developer-guide-restapi#large-inputs-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"`.

<Warning>
  **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.
</Warning>

## Account Limits

**`429 quota_exceeded`** means you've hit an account limit. 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. These limits can be raised per account on request.

## 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` (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](/batch/developer-guide-restapi#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.

**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. |

**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 **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](/batch/developer-guide-restapi#5-reruns-and-retrying-failures). Successful results carry over, so you only pay to re-scrape what failed.
* If a partial rerun returns `409 partial_rerun_window_expired`, the 10-day window for retrying individual statuses has closed. Start a full rerun instead.

<Tip>
  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.
</Tip>

## Webhooks Not Arriving

The API sends a `run.completed` event when a run completes, or a `run.failed` event when it fails. 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](/batch/developer-guide-restapi#verify-signed-webhooks).

<Tip>
  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.
</Tip>

## 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 credits.** 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](/batch/developer-guide-restapi#check-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

**`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](/batch/developer-guide-restapi#check-job-status). 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.