# Bulk File Downloader - Parallel URL Downloads to Storage (`seemuapps/bulk-file-downloader`) Actor

Download thousands of files, images and PDFs from a URL list or another actor's dataset in parallel, with SHA-256 hashes, public links and an optional ZIP.

- **URL**: https://apify.com/seemuapps/bulk-file-downloader.md
- **Developed by:** [Seemu Scraping](https://apify.com/seemuapps) (community)
- **Stats:** 4 total users, 4 monthly users, 95.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 file downloadeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bulk File Downloader - Parallel URL Downloads to Storage

Download thousands of files from a list of URLs, or straight from another actor's dataset, in parallel. Images, PDFs, documents, audio, video and archives are saved to Apify storage with public download links and SHA-256 hashes. You can also get everything in one ZIP.

### What you get

- **Every file saved** in the run's key-value store with the correct content type, ready to download or hotlink
- **One row per URL** in the dataset: `url`, `status` (ok / failed), `fileName`, `kvKey`, `downloadUrl`, `contentType`, `sizeBytes`, `sha256`, `durationMs`, `attempts`, `httpStatus`, `finalUrl`, `error`
- **Optional ZIP archive** (`files.zip`) with every downloaded file, built as the run goes so memory use stays low
- **Smart file names**: taken from the server's `Content-Disposition` header or the URL, with the right extension worked out from the file type (an `image.php` that returns a JPEG is saved as `.jpg`). Duplicate names get `-2`, `-3` suffixes
- **Clear failure reasons**: 404s, timeouts, oversized files and invalid URLs each get a readable error in the dataset, and the rest of the run carries on
- Export the results table to JSON, CSV, Excel or Google Sheets from the Apify console

### Use cases

- **Download product images** from an e-commerce or marketplace scraper's output
- **Archive social media media**: profile pictures, post images and thumbnails from a social media scraper's dataset
- **Bulk PDF download** of reports, invoices, filings or research papers from a URL list
- **Build machine-learning image datasets** with SHA-256 hashes for deduplication
- **Back up website assets** or move files between systems through one ZIP

### How to use

1. Paste your file URLs into **File URLs**, one per line, and/or
2. Enter a **Dataset ID** from a previous run and the **Dataset field** that holds the URLs (for example `imageUrl`, `images` or `images.url`)
3. Optionally switch on **Bundle everything into one ZIP**
4. Run the actor. Files appear in the **Key-value store** tab and one result row per URL appears in the **Dataset** tab

#### Chaining from another actor

Most scrapers output image or file URLs in a column. Copy the dataset ID from that run's **Storage** tab, paste it into **Dataset ID** and enter the column name in **Dataset field**. Dotted paths and arrays are supported:

| Dataset item | Dataset field |
|---|---|
| `{"imageUrl": "https://…"}` | `imageUrl` |
| `{"images": ["https://…", "https://…"]}` | `images` |
| `{"images": [{"url": "https://…"}, …]}` | `images.url` or `images` |
| `{"media": {"photo": {"src": "https://…"}}}` | `media.photo.src` |

You can also run this actor automatically after another one with an Apify **integration** (webhook) and pass `datasetId` in the input.

#### File name template

Use `{index}` (position in the list, zero-padded), `{name}` (original name without extension) and `{ext}` (detected extension):

- `{name}` gives `IMG_2045.jpg` (default)
- `{index}-{name}` gives `0007-IMG_2045.jpg`, which keeps the input order when sorted
- `product-{index}.{ext}` gives `product-0007.jpg`

Only the characters `a-z A-Z 0-9 ! - _ . ' ( )` are allowed in storage keys. Others are replaced with `-`.

### Input options

| Field | Default | Description |
|---|---|---|
| File URLs | none | Direct file links, one per line |
| Dataset ID / field | none / `url` | Read URLs from another run's dataset |
| Max files | 0 (all) | Cap the number of URLs processed |
| Skip duplicate URLs | on | Download each distinct URL once |
| Bundle into ZIP | off | Also store `files.zip` with everything |
| File name template | `{name}` | See above |
| Parallel downloads | 10 | 1-50 files at once |
| Max file size (MB) | 100 | Larger files are skipped with an error; 0 = no limit |
| Timeout per file | 60 s | Per download attempt |
| Retries per file | 2 | For network errors, timeouts, 429 and 5xx |
| Proxy | off | Optional Apify Proxy or your own proxies |

### Output format

Each dataset row:

```json
{
  "index": 1,
  "url": "https://picsum.photos/id/10/800/600",
  "status": "ok",
  "fileName": "10-800x600.jpg",
  "kvKey": "10-800x600.jpg",
  "downloadUrl": "https://api.apify.com/v2/key-value-stores/abc123/records/10-800x600.jpg",
  "contentType": "image/jpeg",
  "sizeBytes": 69637,
  "sha256": "4265dda7ddcd61eb7fefc9f3d399fd383ce7331c4ad0a6b1065821f7e873c0a0",
  "durationMs": 485,
  "attempts": 1,
  "httpStatus": 200,
  "finalUrl": "https://fastly.picsum.photos/id/10/800/600.jpg",
  "error": null,
  "downloadedAt": "2026-10-12T02:50:12.813Z"
}
```

Failed rows have `status: "failed"` and an `error` such as `HTTP 404 Not Found`, `Timed out` or `File is 20.0 MB, larger than the 15 MB limit (maxFileSizeMb).`

The ZIP is stored as `files.zip` in the key-value store, or under your custom **ZIP file name**.

### Pricing

You pay per file actually saved, plus per megabyte downloaded (rounded up per file, so a 300 KB image counts as 1 MB). Failed downloads, 404s and skipped files are free. If you route downloads through **residential** proxy, a per-MB proxy surcharge applies. Datacenter proxy and your own proxies add no surcharge.

### Limits and tips

- **File size**: single files of 100 MB and more are supported (raise **Max file size** above the 100 MB default when you need them). Big files take longer, and the run's disk is limited by its memory setting. For many files over 100 MB, raise the run memory.
- **ZIP**: built on disk as files finish, so memory stays low. The ZIP needs disk space roughly equal to the total download size. For multi-GB batches, give the run more memory or skip the ZIP and download files individually.
- **Speed**: 10 parallel downloads is a good default. Raise it to 30-50 for many small images on a fast CDN. Lower it if a site returns 429 (Too Many Requests).
- **Blocked downloads (403)**: the actor automatically retries once with browser-like headers. If a site still blocks you, turn on a proxy.
- **Run timeout**: if the run gets close to its timeout, the remaining URLs are written to the dataset as `failed` with the message *Not attempted*. Filter those rows and run them again.
- Only direct file URLs are supported. Web pages are saved as `.html` files and are not crawled for images.

### FAQ

**Can I download images from a Google Sheet or CSV?** Yes. Paste the URL column into **File URLs**, or import the sheet into a dataset first and use **Dataset ID**.

**How do I get all files at once?** Turn on **Bundle everything into one ZIP** and download `files.zip` from the Key-value store tab.

**Are file contents changed?** No. Files are stored byte for byte. Use the `sha256` field to verify them or to find duplicates.

# Actor input Schema

## `urls` (type: `array`):

Direct links to the files you want to download (images, PDFs, documents, audio, video, archives). One URL per line. Redirects are followed automatically.

## `datasetId` (type: `string`):

Optional. ID of an Apify dataset to read URLs from, e.g. the output of a product or social media scraper. Combined with the URL list above.

## `datasetField` (type: `string`):

Field in each dataset item that holds the file URL. Dotted paths and arrays are supported: 'imageUrl', 'images', 'images.url', 'media.0.src'. Arrays of URLs or of objects with a url/src/href field are all expanded.

## `maxItems` (type: `integer`):

Maximum number of URLs to process in this run. 0 means no limit.

## `deduplicateUrls` (type: `boolean`):

Download each distinct URL only once. Useful when a dataset repeats the same image across many rows.

## `createZip` (type: `boolean`):

Also create a single ZIP archive of all downloaded files in the key-value store, so you can grab everything with one click.

## `zipFileName` (type: `string`):

Name of the ZIP record in the key-value store. Only used when the ZIP option is on.

## `fileNameTemplate` (type: `string`):

How stored files are named. Placeholders: {index} (position in the list, zero-padded), {name} (original file name without extension, from the server or URL), {ext} (extension inferred from the file type). Example: 'product-{index}.{ext}'. If {ext} is missing it is appended. Duplicate names get -2, -3 suffixes.

## `concurrency` (type: `integer`):

How many files to download at the same time (1-50). Lower it if a site starts rate-limiting you.

## `maxFileSizeMb` (type: `integer`):

Files larger than this are skipped with a clear error (checked before and during the download). 0 means no limit.

## `timeoutSecs` (type: `integer`):

Maximum time for a single download attempt, including redirects. Raise it for large files or slow servers.

## `maxRetries` (type: `integer`):

How many times to retry a file after a network error, timeout, 429 or 5xx response. 404 and other client errors are not retried.

## `proxyConfiguration` (type: `object`):

Optional. Route downloads through a proxy, e.g. when a site blocks datacenter IPs or serves files only to some countries. Most public file hosts work without a proxy. Retries use a fresh proxy session.

## Actor input object example

```json
{
  "urls": [
    "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "datasetField": "url",
  "maxItems": 0,
  "deduplicateUrls": true,
  "createZip": false,
  "zipFileName": "files.zip",
  "fileNameTemplate": "{name}",
  "concurrency": 10,
  "maxFileSizeMb": 100,
  "timeoutSecs": 60,
  "maxRetries": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per URL: index, url, status (ok/failed), finalUrl, httpStatus, fileName, kvKey, downloadUrl, contentType, sizeBytes, sha256, durationMs, attempts, error.

## `files` (type: `string`):

All downloaded files, stored under their kvKey in the default key-value store.

## `zip` (type: `string`):

files.zip with every downloaded file (only when 'Bundle everything into one ZIP' is on and a custom ZIP name was not set).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("seemuapps/bulk-file-downloader").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": [
        "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("seemuapps/bulk-file-downloader").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call seemuapps/bulk-file-downloader --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,seemuapps/bulk-file-downloader"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9ISBZ0acCZNJIlrKI/builds/KPqmmYB3Fi7JYWdac/openapi.json
