# Bulk File Downloader - URLs to Files and ZIP (`pshivam/bulk-file-downloader-scraper`) Actor

Download files from a list of URLs, web pages or another Actor's dataset: images, PDFs, video, audio. Fast and resumable, with SHA-256, image size, duplicates removed and optional ZIP. You pay only for files stored.

- **URL**: https://apify.com/pshivam/bulk-file-downloader-scraper.md
- **Developed by:** [Shivam Prajapati](https://apify.com/pshivam) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 files

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 - URLs to Files and ZIP

**Download files in bulk from a list of URLs, from web pages, or from the dataset of another Actor.** Images, PDFs, videos, audio and archives are streamed to disk, stored in your key-value store (and packed into ZIP parts on request), and reported in one table with **size, content type, SHA-256, image width and height, redirects and tries**. Same content is stored once, broken downloads continue where they stopped, and **you pay only for files that were stored**: failed and skipped addresses are free. No proxy, no login, no browser.

**Actively maintained.** Last update: 2026-11-07. Every release passes 100+ automated tests, and Apify test-runs it every day.

### What can you use it for?

- **Turn scraper output into files.** Another scraper gave you 5,000 image or PDF links. Give this Actor the dataset id and get the files.
- **Back up media.** Download all images, videos and documents from a list of pages in one run, with a ZIP at the end.
- **Product image libraries.** Name every image by a template such as `{host}_{filename}` and keep one ZIP per run.
- **Archive and prove.** Every file comes with its SHA-256, size and server headers (`etag`, `lastModified`), ready for audits and deduplication.
- **Feed AI and data pipelines.** Hand the stored file links and image sizes to the next step.

### What data do you get?

One row per stored file. Failed addresses are not rows: they are in the `FAILED_URLS` record, with the reason.

| Field | What it is |
|---|---|
| `url`, `finalUrl` | The address you gave, and where it led after redirects |
| `status` | `downloaded`, or `duplicate` when an earlier file in the run has the same content |
| `fileName`, `key`, `storageUrl` | The file's name, its record key and the link to the stored file |
| `contentType`, `fileType`, `extension` | Server content type, `image`, `document`, `video`, `audio`, `archive` or `other`, and the extension |
| `bytes`, `sha256` | Size and SHA-256 of the content |
| `width`, `height` | Image size in pixels (PNG, JPEG, GIF, WebP, BMP) |
| `duplicateOf` | For a duplicate: the key of the file with the same content |
| `httpStatus`, `lastModified`, `etag` | Server answer and headers |
| `sourcePage` | The page the file was found on (page scans) |
| `zipPart` | The ZIP part the file is in, e.g. `files-part-001.zip` |
| `redirects`, `attempts`, `downloadMs` | Redirects followed, tries needed and time taken |
| `fetchedAt` | When the file was downloaded (UTC) |

#### Example result

```json
{
  "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
  "finalUrl": null,
  "status": "downloaded",
  "fileName": "Example.jpg",
  "key": "6a1f0c3e-Example.jpg",
  "storageUrl": "https://api.apify.com/v2/key-value-stores/xxxx/records/6a1f0c3e-Example.jpg",
  "contentType": "image/jpeg",
  "fileType": "image",
  "extension": "jpg",
  "bytes": 12345,
  "sha256": "3b1c...",
  "width": 275,
  "height": 297,
  "duplicateOf": null,
  "httpStatus": 200,
  "sourcePage": null,
  "zipPart": null,
  "redirects": 0,
  "attempts": 1,
  "downloadMs": 180,
  "fetchedAt": "2026-11-07T12:00:00Z"
}
```

### How to use it

1. Paste file links into **File URLs**, or list pages in **Or web pages to scan for files**, or give a **dataset id**.
2. Pick the kinds of files you want, a ZIP option and a file name template if you like.
3. Set **Maximum files** and press Start. Files appear in the key-value store as they arrive.

#### Input options

| Input | What it does |
|---|---|
| `urls` | File links to download: one by one, pasted (one per line) or from a text file. Objects with `url` and `fileName` work through the API |
| `pageUrls` | Web pages to scan for images, video, audio, documents and archives (each page is read once) |
| `datasetId`, `urlFields` | Download every file link in the rows of another Actor's dataset; name the fields or let the Actor find them |
| `fileTypes` | Keep only some kinds of files; others are skipped for free |
| `sameHostOnly`, `maxFilesPerPage` | Page scans: stay on the page's site, and cap the files taken from each page |
| `saveFiles`, `zip`, `zipPartMb` | Store each file, pack ZIP parts (`none`, `single`, `byHost`), and the part size |
| `fileNameTemplate`, `uniqueFileNames` | The record key: `{filename} {name} {ext} {host} {urlhash} {sha} {type} {index}`, with a short address code in front |
| `maxFileSizeMb`, `maxRetries`, `timeoutSeconds` | Size limit, retries and the wait for the server's next bytes |
| `maxPerHost`, `maxConcurrency` | Downloads at once from one site and in total |
| `headers`, `userAgent`, `referer` | Extra request headers, for files you are allowed to download |
| `acceptHtml` | Save an address that returns a web page as a file instead of a failure |
| `keyValueStoreName` | Save into a named key-value store that does not expire |
| `onlyNew` | Skip addresses that earlier runs of this same input already returned |
| `maxItems` | Stop after this many stored files |

### How much does it cost?

You pay per stored file, nothing else. Failed downloads, skipped files, wrong kinds and too-large files are free and do not count toward the maximum. The first Apify plan includes free monthly credit to try it.

| Plan | Price per 1,000 files |
|---|---|
| Free | $1.00 |
| Bronze | $0.90 |
| Silver | $0.80 |
| Gold | $0.70 |

### Use it via API, on a schedule, or with your tools

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("pshivam/bulk-file-downloader-scraper").call(run_input={
    "datasetId": "<DATASET_ID_OF_ANOTHER_RUN>",
    "fileTypes": ["image"],
    "zip": "single",
    "maxItems": 1000,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["fileName"], row["bytes"], row["width"], row["height"], row["storageUrl"])
```

Schedule it in the Apify Console (Schedules) and turn on **Only new files** to download only what earlier runs did not. Connect the result to Google Sheets, Make, Zapier, n8n or your own app with Apify's integrations.

### Good to know

- **Files are streamed to disk and uploaded in parallel.** Memory stays flat, so a 100 MB video and a 5,000-image list run on the same 256 MB.
- **Interrupted downloads continue.** When a connection drops and the server supports byte ranges, the download resumes from the last byte.
- **Same content is stored once.** A second file with the same SHA-256 gets a `duplicate` row that points to the first.
- **Private networks are refused.** Addresses that lead to internal or private IP addresses fail with `blocked_address`.
- **Web pages are read once.** Page scans do not crawl further and do not run JavaScript. Images that a page loads with JavaScript need a browser scraper first.
- **Some links expire.** Links from social media and CDNs often stop working after hours. Run the download soon after the scrape.
- **Streaming sites are not file links.** An address that returns a web page (a YouTube watch page, for example) is a failure `html_instead_of_file`, not a saved page.
- **Files are kept as your run's records.** Run records expire after your plan's retention time. Use `keyValueStoreName` for a store that stays.

### FAQ

#### How do I download many files from a list of URLs?

Paste the links into **File URLs** (one per line) or load a text file, set **Maximum files**, and press Start. Each stored file is a record in the key-value store and a row in the results table.

#### Can it download all images from a web page?

Yes. Put the page in **Or web pages to scan for files** and pick the image kind. It finds `img` tags (the sharpest `srcset` size), `picture` sources, video posters, icons and `og:image` links.

#### Can I download the files of another Actor's results?

Yes. Give the dataset id of the other run. Fields such as `imageUrl`, `images`, `pdf` and `video` are found on their own, or name them in **Dataset fields with the links**.

#### Can I get everything as a ZIP?

Yes. Choose **One ZIP (in parts)**. Parts close at the size you set and appear as `files-part-001.zip` and so on. Turn off **Store each file** to get only the ZIP.

#### What happens to failed downloads?

They are not charged and not counted toward the maximum. The `FAILED_URLS` record lists each address with the reason (`not_found`, `forbidden`, `timeout`, `html_instead_of_file`, `too_large` and more).

#### Does it work with login-protected files?

It sends the headers you give in **Extra request headers**, for example an Authorization header or a Cookie, for files you are allowed to download. It does not log in and does not bypass blocks.

#### Is it legal to download files?

The Actor fetches the addresses you give, like a download manager. You are responsible for having the right to download and keep the files and for following the terms of the sites you download from.

#### How do I download only new files every day?

Turn on **Only new files** and schedule the Actor. It remembers what it returned in your key-value store.

# Actor input Schema

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

Direct links to the files to download (images, PDFs, video, audio, archives). Add them one by one, paste a list (one per line) or load a text file. Repeated addresses are downloaded once.

## `pageUrls` (type: `array`):

Web pages whose images, videos, audio, documents and archives you want. Each page is read once (no further crawling); pages that load their images with JavaScript need a browser scraper first.

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

The id of an Apify dataset (from another Actor's run). Every file link in its rows is downloaded: image, video, pdf and similar fields are found on their own.

## `urlFields` (type: `array`):

Only for a dataset: the fields that hold file links (imageUrl, images, media.main). Empty: find them on their own.

## `fileTypes` (type: `array`):

Keep only these kinds. For web pages, only matching links are taken. A file of another kind is skipped and not charged. Empty: every kind.

## `sameHostOnly` (type: `boolean`):

For web pages: skip files hosted elsewhere (CDNs, other sites).

## `maxFilesPerPage` (type: `integer`):

For web pages: take at most this many files from each page. 0 means all.

## `saveFiles` (type: `boolean`):

Save every file as a record in the key-value store. Turn off to get only the ZIP (fewer API calls, faster).

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

Also pack the files into ZIP parts in the key-value store (files-part-001.zip, ...).

## `zipPartMb` (type: `integer`):

A ZIP part is closed and uploaded when it reaches this size.

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

The record key of each file. Placeholders: {filename} {name} {ext} {host} {urlhash} {sha} {type} {index}. Example: {type}*{host}*{filename}

## `uniqueFileNames` (type: `boolean`):

Put a short code of the address in front of each key, so two files with the same name never overwrite each other.

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

Larger files are skipped and not charged. The size is checked before the download when the server says it.

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

Retries after a timeout, a dropped connection, HTTP 429 or a server error. A broken download continues where it stopped when the server allows.

## `timeoutSeconds` (type: `integer`):

How long to wait for the server's next bytes.

## `maxPerHost` (type: `integer`):

Downloads at the same time from one site. Lower it for sites that block fast downloads.

## `headers` (type: `object`):

Headers sent with every request, e.g. {"Authorization": "Bearer ...", "Cookie": "..."} for files you are allowed to download.

## `userAgent` (type: `string`):

Overrides the User-Agent header.

## `referer` (type: `string`):

Sent as the Referer header. Files found on a page get that page as the Referer on their own.

## `acceptHtml` (type: `boolean`):

By default an address that returns a web page is a failure (an expired link often shows a page). Turn on to save pages as files.

## `keyValueStoreName` (type: `string`):

Save into this named key-value store instead of the run's own one. Named stores do not expire.

## `onlyNew` (type: `boolean`):

Skip addresses that earlier runs of this same input already returned, so you never pay twice. The first run works as usual and remembers what it returned, in your key-value store 'bulk-file-downloader-scraper-only-new'.

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

Stop after this many stored files. You are charged per stored file; failed downloads are free and do not count.

## `maxConcurrency` (type: `integer`):

Files downloaded at the same time.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"
    },
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg"
    },
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ],
  "sameHostOnly": false,
  "maxFilesPerPage": 0,
  "saveFiles": true,
  "zip": "none",
  "zipPartMb": 200,
  "fileNameTemplate": "{filename}",
  "uniqueFileNames": true,
  "maxFileSizeMb": 100,
  "maxRetries": 3,
  "timeoutSeconds": 60,
  "maxPerHost": 8,
  "acceptHtml": false,
  "onlyNew": false,
  "maxItems": 20,
  "maxConcurrency": 20
}
```

# Actor output Schema

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

One row per stored file, shown with the overview view.

## `details` (type: `string`):

Content type, size, image width and height, SHA-256, tries.

## `stored` (type: `string`):

The key-value store with every file and the ZIP parts.

## `failed` (type: `string`):

Addresses that could not be downloaded, with the reason. Not charged.

## `summary` (type: `string`):

Counts, bytes, failure reasons and ZIP parts (RUN_SUMMARY record).

## `runStats` (type: `string`):

Requests, time, status codes and memory for this run (RUN_STATS record).

# 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": [
        {
            "url": "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"
        },
        {
            "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg"
        },
        {
            "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
        }
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("pshivam/bulk-file-downloader-scraper").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": [
        { "url": "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png" },
        { "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg" },
        { "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" },
    ],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("pshivam/bulk-file-downloader-scraper").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": [
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"
    },
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg"
    },
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ],
  "maxItems": 20
}' |
apify call pshivam/bulk-file-downloader-scraper --silent --output-dataset

```

## MCP server setup

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

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/O3nRQaZm9iWRraw7O/builds/eCbBEeGrspIHYdCUO/openapi.json
