# Bulk File & Image Downloader — URLs, Drive, Dropbox to ZIP (`saliga/bulk-file-downloader`) Actor

Download files and images from a list of URLs, a CSV / Google Sheet, a dataset or web pages into storage, with a public link, size and sha256 for each, and optional ZIP archives. Google Drive, Dropbox and OneDrive share links work. $1 per 1,000 files + $0.25 per GB.

- **URL**: https://apify.com/saliga/bulk-file-downloader.md
- **Developed by:** [Salih Küçükkayış](https://apify.com/saliga) (community)
- **Stats:** 2 total users, 0 monthly users, 100.0% 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

Download files and images in bulk. Give a list of URLs, a CSV or Google Sheet, the dataset of another Actor, or web pages, and every file is saved to the run's storage with a **public download link**, its content type, size and **sha256**. **Google Drive, Dropbox, OneDrive and SharePoint share links** work as they are, and you can also get everything as **ZIP archives**. One output row per URL, so it plugs straight into n8n, Make, Zapier or your own code.

**$1 per 1,000 files**, with the first 10 MB of every file included; bigger files add **$0.25 per GB** (in started 10 MB steps). Failed downloads, skipped files, refused links and duplicates of a file already stored in the run are free. No subscription, no platform usage fees. (Plus a tiny start fee of $0.00005 per GB of run memory: $0.00005 per run at the default 1 GB.)

### Quick start

Press **Start** with the example input. It downloads a sample PDF and two public-domain images:

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png",
    "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg"
  ]
}
```

Share links and a ZIP of everything:

```json
{
  "urls": [
    "https://drive.google.com/file/d/1AbC.../view?usp=sharing",
    "https://www.dropbox.com/scl/fi/abc123/report.pdf?rlkey=xyz&dl=0",
    "https://1drv.ms/b/s!Ab..."
  ],
  "zip": true
}
```

All images and PDFs on some web pages:

```json
{
  "pageUrls": ["https://www.example.com/press-kit"],
  "fileTypes": ["jpg", "png", "webp", "pdf"],
  "maxFilesPerPage": 200
}
```

A Google Sheet with a column of links (share it as "Anyone with the link"):

```json
{
  "csvUrl": "https://docs.google.com/spreadsheets/d/1XyZ.../edit#gid=0",
  "urlColumn": "image_url",
  "maxFileSizeMb": 50
}
```

One row per URL (example, values are illustrative):

```json
{
  "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "finalUrl": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "status": "ok",
  "error": null,
  "httpStatus": 200,
  "fileName": "dummy.pdf",
  "key": "dummy-1a2b3c4d5e.pdf",
  "fileUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/dummy-1a2b3c4d5e.pdf",
  "contentType": "application/pdf",
  "bytes": 13264,
  "sha256": "<64 hex characters>",
  "sourcePage": null,
  "duplicateOf": null
}
```

### What it does

- **Many sources**: a list of URLs, a public CSV or Google Sheet (`csvUrl` + `urlColumn`), an Apify dataset (`datasetId` + `urlField`, nested fields like `image.url` and lists of links work), and web pages (`pageUrls`). Duplicate URLs are downloaded once.
- **Share links become downloads**: Google Drive file links, Google Docs / Sheets / Slides (saved as .docx / .xlsx / .pptx), Dropbox (`/s/` and `/scl/fi/` links), OneDrive (`1drv.ms`, `onedrive.live.com`) and SharePoint "Anyone with the link" links. Google Drive **folder** links are not supported yet: you get a clear error row, free.
- **Files on web pages**: the page is read and its file links of the selected types are collected: `<img src>`, `srcset`, lazy-load `data-src`, `<picture><source>`, `<a href>`, `<video>`, `<audio>`, `<embed>`, `<object>`. *Max files per page* caps each page.
- **Streaming downloads**: files go straight to disk and on to storage in chunks, so large files never sit in memory. *Max file size* skips bigger files (before the download when the server sends the size). *Time limit per file* stops slow downloads, including servers that send a few bytes at a time.
- **Storage with a public link**: each file is saved in the run's key-value store under a safe, unique key (cleaned-up file name + short hash + extension) with the right content type. `fileUrl` is a direct link you can open, share or pass to the next step of a workflow.
- **ZIP archives** (`zip: true`): all downloaded files in one archive (`files.zip`), or in parts of *ZIP part size* each (`files-part1.zip`, `files-part2.zip` ...). A part is uploaded as soon as it is full, and its files are deleted from the run's disk. Links in the `OUTPUT` record and the log. No extra charge.
- **sha256** of every file, to check integrity. **Duplicates are free**: when a file with the same content was already stored in this run (the same sha256), the row is `ok` and points to the stored copy (`key`, `fileUrl`) with `duplicateOf` set to the URL it was first downloaded from. It is not stored or charged again, and not added to the ZIP twice.
- **Web pages are not files**: when a link found on a page, or a link that ends in a file type like `.pdf` or `.jpg`, answers with a web page (usually a login page, an error page or a soft "not found" page), it is refused before the body is downloaded, free. Add `html` to `fileTypes` to keep web pages; an `.html` link you list yourself is always kept.
- **Filters**: `fileTypes` limits downloads to the extensions you list (for listed URLs the check happens on the response headers, before the body is downloaded).

### Chain it with other Actors

The `fileUrl` column is a public link, so it is the input of the next step:

- [Document to Markdown](https://apify.com/saliga/document-to-markdown): turn the downloaded PDFs, Word, PowerPoint and Excel files into Markdown for LLMs and RAG.
- [Image to Text OCR](https://apify.com/saliga/image-to-text-ocr): read the text in the downloaded images and scans.
- [Audio to Text](https://apify.com/saliga/audio-to-text): transcribe the downloaded audio and video files.

All three read links from a dataset: give them this run's dataset ID and `fileUrl` as the link field.

### Polite and safe

- **robots.txt** is honoured: always for web pages and the files found on them, and by default for listed URLs too (*Honour robots.txt for listed URLs*). A URL that robots.txt closes is not requested; its row says so and it is free. Turn the option off only for files you own or have permission to download, for example your own share links.
- At most **two downloads at a time per host**, with a short gap between requests; different hosts download in parallel (*Downloads at a time*, default 5).
- Retries on 429 and server errors with backoff (Retry-After honoured), and on dropped connections.
- Private and internal addresses (localhost, 10.x, 192.168.x, cloud metadata) are blocked, including every redirect.
- **Not supported**: YouTube, TikTok, Instagram, Facebook, X/Twitter, LinkedIn, Reddit and Amazon, and their CDNs. Their terms do not allow downloading their media with tools like this, so these links are refused with a clear error, free. (For Amazon this means the store and its image hosts; AWS pages and public S3 files on `amazonaws.com` work.)
- **Only download files you have the right to download.** You are responsible for how you use the files.

### Input

| Field | Default | What it does |
|---|---|---|
| `urls` | | File links and share links |
| `csvUrl`, `urlColumn` | , `url` | Public CSV or Google Sheet with a column of links |
| `datasetId`, `urlField`, `maxDatasetItems` | , `url`, 10,000 | Links from an Apify dataset |
| `pageUrls` | | Web pages to collect file links from |
| `fileTypes` | | Extensions to keep (pages: common types when empty; listed URLs: any type when empty) |
| `maxFilesPerPage` | 100 | File links taken from each page |
| `maxFiles` | 10,000 | Cap on files per run (and on the price) |
| `maxFileSizeMb` | 100 | Bigger files are skipped |
| `maxConcurrency` | 5 | Downloads at a time (never more than 2 per host) |
| `fileTimeoutSecs` | 300 | Time limit per file |
| `respectRobotsTxt` | true | Honour robots.txt for listed URLs |
| `zip`, `zipPartMb`, `zipName` | false, 500, `files` | ZIP archives of all downloaded files |

### Output

One dataset row per URL (per file found, in page mode): `url`, `finalUrl` (after redirects), `status` (`ok`, `error` or `skipped`), `error` (the reason, for errors and skipped files), `httpStatus`, `fileName`, `key` (key-value store key), `fileUrl` (public link to the stored file), `contentType`, `bytes`, `sha256`, `sourcePage` (the page a file was found on), `duplicateOf` (for a free duplicate: the URL of the file it repeats). Every row has every column, so CSV and spreadsheet exports line up.

The `OUTPUT` record has the totals (`files` and `bytes` stored, `duplicates`, `errors`, `skipped`), the ZIP archives (`key`, `url`, `files`, `bytes`) and whether the run stopped at the spending limit or before its timeout.

### What it costs

| Run | Price |
|---|---|
| 1,000 product images under 10 MB each | $1.00 |
| 100 PDFs of 2 MB | $0.10 |
| 10 videos of 100 MB (1 GB) | $0.24 |
| 50 audio files of 25 MB | $0.30 |

| Event | Price | When |
|---|---|---|
| `file` | $1 per 1,000 | A file stored in the key-value store (first 10 MB included) |
| `data-10mb` | $2.50 per 1,000 ($0.25 per GB) | Each started 10 MB of a file beyond its first 10 MB |

Failed downloads, skipped files (robots.txt, type filter, size limit, not enough disk), refused links (including web pages returned instead of a file) and duplicates are free; ZIP archives are free. Set **Maximum cost per run** and the run never goes over it: when the next file would not fit, the run stops cleanly and lists the rest as skipped. The run also stops about 3 minutes before its timeout and keeps everything it stored: downloads in progress are stopped (free), files already being saved are finished and charged.

### Good to know

- Stored files stay as long as the run's storage is kept (see your Apify data retention). Copy them elsewhere if you need them longer.
- If your account restricts storage access, `fileUrl` comes with a signature so the link still opens without a token.
- Files behind a login are not supported: the link must download the file directly (or be a public share link or a pre-signed URL).
- Very large Google Drive files sometimes need "Anyone with the link" sharing to download without the virus-scan page.
- Only the HTML of web pages is read (no browser), so file links that JavaScript adds later are not found.
- Disk space: the run keeps track of its disk and never fills it. A file that would not fit is skipped, free, with the reason "not enough disk space in this run", and *Max file size* is lowered to what fits (half of it with ZIP on, since the archive needs a copy). With ZIP on, files wait on disk until their part is full, so use a smaller *ZIP part size*, more memory (disk grows with it), or turn ZIP off for very large jobs.

### FAQ

**Can I use it from n8n or Make?** Yes: run the Actor with the Apify node, then read the dataset; each row has `fileUrl`, a direct link to the file. Code examples are linked below.

**How do I get one ZIP of everything?** Set `zip` to true. The link is in the `OUTPUT` record (`zip[0].url`) and in the log.

**Can an AI agent use it?** Yes, through the Apify MCP server: "download these files and give me the links".

### More from this developer

- [Document to Markdown](https://apify.com/saliga/document-to-markdown): PDF, Word, PowerPoint, Excel and scans to clean Markdown for LLMs and RAG, $2 per 1,000 pages.
- [Image to Text OCR](https://apify.com/saliga/image-to-text-ocr): text from images and scanned PDFs, 40+ languages, $1.50 per 1,000 images.
- [Audio to Text](https://apify.com/saliga/audio-to-text): audio, video and podcast feeds to timestamped text, SRT/VTT subtitles and RAG chunks with Whisper.
- [Bulk HTTP Requests](https://apify.com/saliga/bulk-http-requests): call any API once for every row of a CSV, JSON list or Apify dataset.
- [Sitemap Extractor](https://apify.com/saliga/sitemap-extractor): every URL a website lists in its sitemaps, with lastmod, images and hreflang.
- [Broken Link Checker](https://apify.com/saliga/broken-link-checker): crawl a site and find broken links and images, with the pages they are on.
- [Website Screenshot + Change Detection](https://apify.com/saliga/website-screenshot-change-detection): screenshots of many URLs with visual change detection between runs.

Code examples (curl, Python, Node.js, n8n) for this and the other Actors: [github.com/msalihk/apify-actor-examples](https://github.com/msalihk/apify-actor-examples).

Questions or a missing feature: open an issue on the Actor page.

# Actor input Schema

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

Direct links to files or images, and Google Drive, Google Docs/Sheets/Slides, Dropbox, OneDrive or SharePoint share links (turned into downloads for you). One row of output per URL. Only download files you have the right to download.

## `csvUrl` (type: `string`):

Link to a public CSV file, or a Google Sheet shared as "Anyone with the link" (its CSV export is read). The list is read once as input.

## `urlColumn` (type: `string`):

Name of the column with the file links (any case). When it is missing, the first column that holds links is used.

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

An Apify dataset with file links, for example the output of a scraper run.

## `urlField` (type: `string`):

Field with the file link in each dataset item. Use dots for nested fields (image.url). A list of links is read too.

## `maxDatasetItems` (type: `integer`):

Read at most this many items from the dataset.

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

Public HTML pages. The page itself is not saved; its file links of the selected types are downloaded.

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

File extensions to keep, e.g. jpg, png, webp, pdf, docx. For web pages, empty means common types (images, PDF, Office files, CSV, TXT, ZIP, MP3, MP4, EPUB). For listed URLs, empty means any type; when set, other files are skipped before they are downloaded. A link that answers with a web page instead of a file (a login or "not found" page) is refused, free, unless you add html here.

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

File links taken from each web page, in page order, duplicates removed.

## `maxFiles` (type: `integer`):

Cap on files downloaded in this run (and on the price). URLs over the cap are listed as skipped.

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

Larger files are skipped: before the download when the server sends the size, otherwise the download stops at the limit. Free. Lowered automatically to what fits on the run's disk (half of it with ZIP on).

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

Files downloaded in parallel. Never more than two at a time from the same host, with a short gap between requests.

## `fileTimeoutSecs` (type: `integer`):

A download that takes longer is stopped and reported as an error (free).

## `respectRobotsTxt` (type: `boolean`):

Skip listed URLs whose site's robots.txt does not allow them (free). Turn off only for files you own or have permission to download, such as your own share links. Web pages and the files found on them always honour robots.txt.

## `zip` (type: `boolean`):

Also store ZIP archive(s) of all downloaded files in the key-value store. Their links are in the OUTPUT record and the log. No extra charge. Files wait on the run's disk until their part is full; for many GB, use smaller parts or more memory (disk grows with it).

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

Archives are split into parts of about this size (a bigger file gets its own part). A part is uploaded as soon as it is full and its files are deleted from the run's disk.

## `zipName` (type: `string`):

Name of the archive: files.zip, or files-part1.zip, files-part2.zip ... when split.

## Actor input object example

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png",
    "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg"
  ],
  "urlColumn": "url",
  "urlField": "url",
  "maxDatasetItems": 10000,
  "maxFilesPerPage": 100,
  "maxFiles": 10000,
  "maxFileSizeMb": 100,
  "maxConcurrency": 5,
  "fileTimeoutSecs": 300,
  "respectRobotsTxt": true,
  "zip": false,
  "zipPartMb": 500,
  "zipName": "files"
}
```

# Actor output Schema

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

One row per URL: status, file name, public link to the stored file, size and content type.

## `fullResults` (type: `string`):

Every field, including final URL after redirects, HTTP status, key-value store key and sha256.

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

Files stored, bytes, duplicates, errors, skipped, and the links of the ZIP archives when ZIP is on.

## `storage` (type: `string`):

The key-value store that holds the downloaded files.

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

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,saliga/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/BAfbCxc3LE3ex6edc/builds/ERtGolCHFn4jWrQh4/openapi.json
