> ## 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 Scraper API issues: authentication, submission errors, job state conflicts, downloads, webhooks, scheduling, and unexpected cost.

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

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.

| Symptom                          | Likely cause                                     | Go to                                                       |
| -------------------------------- | ------------------------------------------------ | ----------------------------------------------------------- |
| `401` on every request           | Missing or wrong API key, or no beta access      | [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 usage 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-scraper-api/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 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](/batch-scraper-api/developer-guide-restapi#open-jobs-adding-tasks-in-batches) accepts up to **10,000**. For very large lists, use a [CSV upload](/batch-scraper-api/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. 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:

| 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 **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](/batch-scraper-api/developer-guide-restapi#reruns-and-retrying-failures). Successful results carry over, so you only pay to re-scrape what failed.

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

## 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](/batch-scraper-api/developer-guide-restapi#verifying-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 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](/batch-scraper-api/developer-guide-restapi#check-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](/batch-scraper-api/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.
