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

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Download all results of a job at once

> Download every result of a Zenrows Batch job at once, into a folder or as a single ZIP file.

A finished Batch run holds one result per task. Instead of fetching them one by one, save them all in one step: as one file per task in a folder, named by your `external_id`, or as a single ZIP archive that Zenrows builds for you.

**Uses:** [Batch results](/batch/developer-guide-restapi#download-content) (`GET /jobs/{id}/results`) and [bulk export](/batch/developer-guide-restapi#bulk-export-zip) (`POST /jobs/{id}/runs/{run_id}/exports`)

## Ask your AI agent

Paste this prompt into a coding agent such as Claude Code or Cursor, with the [Zenrows docs connected](/academy/connect-your-ai-agent#give-your-agent-the-zenrows-docs). The agent looks up the details and writes the calls.

```text theme={"dark"}
Using the Zenrows docs, write a script that takes a Zenrows Batch job ID, waits for its
latest run to finish, and saves every successful result into a folder named after the job,
one file per page named by my external IDs. It should handle runs with 100,000 pages.
```

## Pick a method

| Method | You get | Best for |
| - | - | - |
| Folder download | One file per task, named by `external_id` or `task_id` | Processing pages one by one, keeping your IDs in the file names |
| ZIP export | One archive of the whole run, built by Zenrows | Archiving a run, moving it to storage, handing it to another team |

## Save every result to a folder

Both SDKs page through the results and download each body for you. The CLI does the same and also writes a `_manifest.jsonl` index with each task's `external_id`, `metadata`, `url` and file name.

<CodeGroup>
  ```python Python (SDK) theme={"dark"}
  # pip install zenrows
  from zenrows import ZenRowsBatchClient

  client = ZenRowsBatchClient(api_key="YOUR_ZENROWS_API_KEY")
  job = client.job("YOUR_JOB_ID")
  job.run.wait(timeout=900)  # returns as soon as the run has finished

  # One file per result, named by external_id
  saved = job.run.download_to_dir("./pages", use_external_id=True)
  print(f"Saved {saved} files to ./pages")
  ```

  ```javascript Node.js (SDK) theme={"dark"}
  // npm install zenrows
  import { ZenRowsBatchClient } from "zenrows";

  const client = new ZenRowsBatchClient("YOUR_ZENROWS_API_KEY");
  const job = client.job("YOUR_JOB_ID");
  await job.run.wait(); // returns as soon as the run has finished

  // One file per result, named by external_id
  const saved = await job.run.downloadToDir("./pages", { useExternalId: true });
  console.log(`Saved ${saved} files to ./pages`);
  ```

  ```bash CLI theme={"dark"}
  zenrows batch results YOUR_JOB_ID --download ./pages
  ```
</CodeGroup>

`job.run` is the job's latest run. To save another run, use `client.run(job_id, run_id)`. Over the raw REST API, the loop in [Scrape a list of URLs](/academy/scrape-from-a-list-of-urls#submit-the-list-as-one-batch-job) does the same: page through `GET /jobs/{job_id}/results` with `cursor` and download each `result_url`.

The CLI run on a job with three successful tasks:

```text theme={"dark"}
✓ Downloaded 3/3 result(s) → ./pages
  index: pages/_manifest.jsonl
```

```text Folder theme={"dark"}
_manifest.jsonl
capri-fields.json
hoodie-markdown.md
ip-de.html
```

In the CLI folder, each file's extension follows the result `type`: `.md` for markdown, `.json` for CSS-extracted fields, `.html` for pages. The SDKs name files differently: the Python SDK adds the result type as the extension (`hoodie-markdown.markdown`, `capri-fields.json`), and the Node.js SDK adds none (`hoodie-markdown`).

## Export the whole run as a ZIP

1. Read the run's `run_id` from `latest_run` on `GET /jobs/{job_id}`.
2. `POST /jobs/{job_id}/runs/{run_id}/exports` starts the export and returns an `export_id`.
3. Poll `GET /jobs/{job_id}/runs/{run_id}/exports/{export_id}` until `status` is `completed`, then download `download_url`.

<CodeGroup>
  ```python Python (Requests) theme={"dark"}
  import time
  import zipfile
  import requests

  BASE = "https://async.api.zenrows.com/v1"
  headers = {"X-API-Key": "YOUR_ZENROWS_API_KEY"}
  job_id = "YOUR_JOB_ID"

  # 1. Find the run to export (the job's latest run)
  run_id = requests.get(f"{BASE}/jobs/{job_id}", headers=headers, timeout=60).json()["latest_run"]["run_id"]

  # 2. Start the export and poll until it's ready
  exports = f"{BASE}/jobs/{job_id}/runs/{run_id}/exports"
  export = requests.post(exports, headers=headers, timeout=60).json()
  while export["status"] not in {"completed", "failed"}:
      time.sleep(3)
      export = requests.get(f"{exports}/{export['export_id']}", headers=headers, timeout=60).json()
  if export["status"] == "failed":
      raise RuntimeError(export.get("error"))

  # 3. Download the ZIP from its presigned link
  with open("results.zip", "wb") as f:
      f.write(requests.get(export["download_url"], timeout=300).content)

  with zipfile.ZipFile("results.zip") as z:
      for name in z.namelist():
          print(name, z.getinfo(name).file_size, "bytes")
  ```

  ```python Python (SDK) theme={"dark"}
  # pip install zenrows
  from zenrows import ZenRowsBatchClient

  client = ZenRowsBatchClient(api_key="YOUR_ZENROWS_API_KEY")
  job = client.job("YOUR_JOB_ID")

  # Starts the export, waits for it, downloads the ZIP
  path = job.run.download_all_results("./results.zip")
  print("ZIP saved to", path)
  ```

  ```javascript Node.js (SDK) theme={"dark"}
  // npm install zenrows
  import { ZenRowsBatchClient } from "zenrows";

  const client = new ZenRowsBatchClient("YOUR_ZENROWS_API_KEY");
  const job = client.job("YOUR_JOB_ID");

  // Starts the export, waits for it, downloads the ZIP
  const path = await job.run.downloadAllResults("./results.zip");
  console.log("ZIP saved to", path);
  ```

  ```bash cURL theme={"dark"}
  base="https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID/runs/YOUR_RUN_ID/exports"

  # Start the export
  id=$(curl -s -X POST "$base" -H "X-API-Key: YOUR_ZENROWS_API_KEY" | jq -r '.export_id')

  # Poll until it's completed
  while :; do
    export=$(curl -s "$base/$id" -H "X-API-Key: YOUR_ZENROWS_API_KEY")
    status=$(echo "$export" | jq -r '.status')
    case "$status" in completed|failed) break ;; esac
    sleep 3
  done

  # Download the ZIP
  curl -s "$(echo "$export" | jq -r '.download_url')" -o results.zip
  ```
</CodeGroup>

The SDK methods start the export, poll it and stream the archive to disk when the archive is ready.

## Example output

The Python (Requests) run. Inside the ZIP, files are named by `task_id` with an extension for the result type:

```text theme={"dark"}
01M4ZCGP27BNBFSVGJ8TVX4WPK.json 38 bytes
01M4ZCGP27EP0Y26M2G1GVFAHB.html 39 bytes
01M4ZCGP27RVKE4DJYZGMQFN5Z.md 4362 bytes
```

The SDK runs:

```text theme={"dark"}
Saved 3 files to ./pages
ZIP saved to results.zip
```

To map ZIP files back to your own IDs, list the results once (`GET /jobs/{job_id}/results?status=successful`) and join on `task_id`, or use the CLI's `_manifest.jsonl`.

## Customize it

| Change | How |
| - | - |
| Faster folder downloads | `concurrency=8` on `download_to_dir`, `concurrency: 8` on `downloadToDir` |
| Your own file names | `name_fn=lambda row: ...` in Python, `nameFn: (row) => ...` in Node.js |
| Results in memory instead of files | `download_to_memory()` / `downloadToMemory()` |
| A list of results without bodies | `zenrows batch results YOUR_JOB_ID --out results.jsonl` |
| Download as soon as a run ends | Start the download from a `run.completed` webhook, see [Get notified when a Batch job finishes](/academy/batch/webhooks) |
| Each run of a scheduled job | Loop over `job.runs()` and save each run, see [Run a Batch job on a schedule](/academy/batch/scheduled-jobs) |

## Troubleshooting

* **Fewer files than tasks**: the folder download saves successful results. Read the failures with `status=failed`, see [Retry the tasks that failed](/academy/batch/retry-failed-tasks).
* **A `result_url` or `download_url` returns `403`**: the presigned link is stale. List the results or poll the export again for a fresh link.
* **The export stays `pending` or `running`**: Zenrows is still building the archive. Keep polling every few seconds.
* **The files are named by `task_id`**: pass `use_external_id=True` (`useExternalId: true`), and give each task an `external_id` when you submit it.

## Related recipes

<CardGroup cols={2}>
  <Card title="Scrape a list of URLs" icon="list" href="/academy/scrape-from-a-list-of-urls">
    Submit thousands of URLs as one Batch job.
  </Card>

  <Card title="Start a Batch job from a CSV file" icon="file-spreadsheet" href="/academy/batch/csv-input">
    Upload a whole CSV of URLs at once.
  </Card>

  <Card title="Get notified when a Batch job finishes" icon="bell" href="/academy/batch/webhooks">
    Receive a webhook instead of polling.
  </Card>

  <Card title="Retry the tasks that failed" icon="refresh" href="/academy/batch/retry-failed-tasks">
    Rerun only the failures and keep the successes.
  </Card>
</CardGroup>


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