# Bulk Image & File Downloader from CSV, Google Sheet or Dataset (`nerolabs/bulk-file-downloader`) Actor

Downloads every image or file linked in a dataset, CSV, Excel or Google Sheet and stores each with a public link, keeping your columns. Inputs: datasetId or fileUrl, urlField, fileNameField, createZip. Charged per file stored plus per 10 MB; broken links and repeats free. Agent-ready: x402, MCP.

- **URL**: https://apify.com/nerolabs/bulk-file-downloader.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** Developer tools, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.80 / 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 Image & File Downloader: every file linked in your spreadsheet, stored and zipped

Got a scraper's output, a CSV or a Google Sheet full of image, PDF or video links, and you need the actual files, not the links?

Point this Actor at an **Apify dataset, a CSV or Excel file, or a Google Sheet**. For every link it downloads the file, stores it with a **public link**, names it the way you want (by SKU, by listing ID, or in sheet order), and hands you back **every original column with the file's link, type, size and dimensions added**. Turn on one switch and you also get **the whole lot as a single ZIP**.

Every other downloader on the Store takes a typed-in list of URLs. If your links live in a spreadsheet, you have to pull the column out, paste it in, and then work out which downloaded file belonged to which product. This one takes the whole sheet, reads links in any shape (one per cell, several per cell, or an `images` array of objects straight out of a scraper) and keeps photo 37 next to product 37.

### What you get per file

| Field | What it is |
|---|---|
| `fileUrl` | A public link to the stored file, ready to paste anywhere |
| `fileName` | The stored name, for example `NL-1001-2.jpg` or `0007-1-IMG_2041.jpg` |
| `fileCategory`, `extension`, `contentType` | What the file really is (image, video, audio, pdf, document, archive, text), read from its own bytes |
| `sizeBytes`, `sizeMb` | How big it is |
| `width`, `height` | Pixel dimensions, for images |
| `sha256` | A fingerprint of the file, for spotting identical files under different links |
| `downloadStatus`, `statusDetail` | What happened, in plain English, when it did not work |
| `rowNumber`, `fileIndex`, `sourceField`, `sourceUrl`, `resolvedUrl` | Which row, which link and which column the file came from, and where any redirect led |
| `zipFile`, `duplicateOf`, `httpStatus`, `downloadMs`, `downloadedAt` | The facts behind the row |

Plus every column that was already in your row. One output row per link, so a product with five photos gives five rows.

### Naming

- **By column**: set `fileNameField` to `sku` and the files come out as `NL-1001.jpg`, then `NL-1001-2.jpg` for a row's second photo.
- **In sheet order** (the default): `0007-1-IMG_2041.jpg`, the row number first so files sort the way your sheet does, then the file number and the original name.
- The extension always matches what the file really is. A `.jpg` link that serves a PNG is saved as `.png`.

### What it costs

- **$0.004 per file stored** (Bronze, Silver and Gold Store discounts apply from day one).
- **$0.003 per full 10 MB of a file.** Files under 10 MB, which is nearly every image and most PDFs, pay only the per-file price.
- **ZIP: $0.003 per 10 MB of archive**, rounded up, only when you switch it on.
- $0.01 per export file, $0.02 per webhook delivery.

Never charged: broken links and 404s, links that return a web page instead of a file, bot-protection pages, files filtered out by type, files over your size limit, private network addresses, empty cells, and **repeats of a link already downloaded in the same run**.

- 5,000 product photos of about 300 KB each: **$20.00**
- The same 5,000 photos as one ZIP as well (about 1.5 GB): **+$0.46**
- 200 PDF brochures of 2 MB: **$0.80**
- 50 product videos of 45 MB: **$0.80**

`maxFiles` caps the number of files in a run, and `maxFileMb` (default 200 MB) caps any one file, so a run cannot surprise you.

### Inputs

Give it any one of these:

- **Dataset** picked from your account, for example a product, property, Google Maps or social media scraper's output
- **File or Google Sheet URL**: CSV, TSV, Excel, JSON or JSON Lines. A normal Google Sheets link works, shared as "Anyone with the link can view"
- **File URLs**: a plain list, for a quick one-off
- **Rows**: inline JSON

The link column is detected automatically, preferring a column whose links end in `.jpg`, `.png`, `.pdf` or `.mp4` over one merely named `url`. Set `urlField` to be explicit, or give several columns separated by commas (`mainImage, gallery`). Google Drive and Dropbox share links are converted to direct downloads.

Useful options:

- `fileTypes` to keep only images, only PDFs, only video and so on. Anything else is skipped free.
- `storeName` to keep the files in a named store, which Apify keeps indefinitely. Without it they live in the run's own storage, which your plan keeps for its retention period (7 days on Free).
- `headers` for hosts that only serve files with a `Referer` header.
- `keep: "problems"` to list just the broken links, web pages and blocked hosts in a large sheet.
- `dedupeUrls` (on by default) downloads a repeated link once and points every later row at the same file.

### The honest limits

- Files are fetched straight from their own servers, with no proxy. Most image CDNs, file hosts and public buckets serve them happily. A host with strong bot protection may refuse; that link is reported as `blocked` and not charged.
- A link to a **web page** (a product page, a Google Drive preview, an Instagram post) is not a file. It is reported as `not_a_file` and not charged. Point `urlField` at the column that holds the direct file links.
- Signed links that expire (common on social media CDNs) must be downloaded before they expire, so run this straight after the scraper, for example chained with the [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner) or an Apify integration.

### Speed

Files stream straight to disk rather than into memory, so the default 1 GB of memory handles large videos as easily as thumbnails. Eight files download at once by default, and up to 32 with `concurrency`.

Measured on Apify: 120 product photos from a dataset, named by listing ID and zipped, in about 5 seconds; a 100 MB file plus four smaller ones, stored and split into two ZIP parts, in about 5 seconds with memory peaking at 340 MB. The default example (six photos from a Google-Sheet-style CSV) finishes in about 7 seconds including start-up. Speed on your own links depends mostly on how fast their servers are.

### Exports and pipelines

- `exportFormats` writes the results table as a real downloadable **CSV or Excel** file.
- `outputDatasetName` appends every run into one named dataset. Not charged again.
- `webhookUrl` POSTs the run summary (counts, sizes, ZIP links) to Slack, Zapier, Make, n8n or your own API the moment the run finishes. Charged only on a confirmed 2xx.
- Agent-ready: pay per event via x402, and callable from MCP clients through Apify's MCP server.

### FAQ

**Can it download a scraper's image arrays?** Yes. A cell holding `["https://...1.jpg", "https://...2.jpg"]` or `[{ "url": "..." }, { "url": "..." }]` gives one file per link, all tied to that row. `maxFilesPerRow` (default 100) caps it.

**Can I get everything in one download?** Switch on `createZip`. Large sets are split into parts (1 GB each by default, set with `zipPartMb`), and each row's `zipFile` says which part holds its file.

**Will re-running download everything again?** Yes, each run downloads the links it is given. To refresh a set in place, use the same `storeName` and `fileNameField`: files with the same name are replaced.

**Does it convert or resize images?** No. Files are stored exactly as the server sent them, byte for byte, with their real type and dimensions reported.

**Can I read the text in the images or PDFs I downloaded?** Pair it with [Bulk OCR](https://apify.com/nerolabs/bulk-ocr-image-pdf-to-text) for scans and photos, or [Dataset PDF Extract](https://apify.com/nerolabs/dataset-pdf-extract) for born-digital PDFs. Both read the same kind of sheet.

**Is it legal to download these files?** The Actor downloads the links you give it; you need the right to use the files. Product photos from your own or a supplier's catalogue, documents you are entitled to, and openly licensed media are the typical uses.

**Is my data safe?** The Actor reads your sheet, fetches the files you point it at, and stores them in your own Apify account. Stored files are reachable only through their signed `fileUrl` links. If you set a webhook, the run summary goes to the URL you chose and nowhere else.

### The rest of the toolkit

Sibling Actors that fit together as a pipeline:

- [Bulk OCR](https://apify.com/nerolabs/bulk-ocr-image-pdf-to-text) text out of images and scanned PDFs
- [Dataset PDF Extract](https://apify.com/nerolabs/dataset-pdf-extract) text and tables out of born-digital PDFs
- [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter) dedupe, flatten, clean, export
- [Dataset Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform) filter by rule, rename, compute, cast
- [Dataset Join & Merge](https://apify.com/nerolabs/dataset-join-merge) VLOOKUP for datasets
- [Dataset Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector) what changed since last time
- [Dataset AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich) one instruction, typed output columns
- [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner) chain any of these in one call

### Notes

ZIP files are written with [archiver](https://github.com/archiverjs/node-archiver) (MIT) and image dimensions read by [image-size](https://github.com/image-size/image-size) (MIT). The sample product sheet and photos in the default input are synthetic, made for this Actor; no real products or businesses.

If this saved you a pile of right-click-save-as, a review on the Apify Store helps a lot. Found a link it would not download? Open an issue with the link and I will look at it.

# Actor input Schema

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

An Apify dataset with file or image links in it, for example the output of a product, property, Google Maps or social media scraper. Links can be one per cell, several per cell, or an array such as 'images'. Every original column is kept next to each downloaded file. Use the picker rather than typing an ID.

## `fileUrl` (type: `string`):

A public link to a CSV, TSV, Excel, JSON or JSON Lines file with file or image links in it. A normal Google Sheets link works: share it as 'Anyone with the link can view'. Used when no dataset is given.

## `fileFormat` (type: `string`):

Leave on 'Detect automatically' unless the link has no file extension and the server reports the wrong content type.

## `sheetName` (type: `string`):

Which sheet to read from an Excel workbook. Defaults to the first sheet.

## `fileUrls` (type: `array`):

A plain list of links to download, for a quick one-off run with no spreadsheet. Use the dataset, file or Google Sheet inputs above to keep your own columns next to each file.

## `data` (type: `array`):

Rows as inline JSON, an alternative to a dataset or file. Each object needs a field holding one or more file links.

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

The column holding the file links. Several columns can be given separated by commas, for example 'mainImage, gallery'. Left empty, the column is detected automatically, preferring one whose links end in .jpg, .png, .pdf or .mp4 over one merely named 'url'.

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

Only keep files of these types, judged from the file's own bytes. Anything else is skipped and not charged. Leave empty to keep every type.

## `fileNameField` (type: `string`):

A column whose value names each file, for example 'sku' gives SKU-123.jpg, then SKU-123-2.jpg for a row's second file. Left empty, files are named row number, file number and the original file name, for example 0007-1-IMG\_2041.jpg, so they sort in sheet order.

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

Pack every downloaded file into one ZIP to download in one go, split into parts when the total is large. Charged by size like the files themselves.

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

Largest size of one ZIP part. Downloads above this are split into downloaded-files-part-1-of-N.zip and so on.

## `storeName` (type: `string`):

Save the files into a named key-value store in your account, which Apify keeps indefinitely, instead of the run's own storage, which expires with your plan's retention (7 days on Free). A file with the same name as one already there is replaced, so a re-run refreshes the set. Letters, digits and dashes.

## `dedupeUrls` (type: `boolean`):

When the same link appears more than once, download it once and point every later row at the same file, free of charge.

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

Extra HTTP headers sent with every download, for hosts that only serve files with a Referer or to a signed-in session, for example {"Referer": "https://example.com/"}.

## `keep` (type: `string`):

One output row per link. 'Problems only' is the quick way to find broken links, web pages and blocked hosts in a large list.

## `keepOriginalFields` (type: `boolean`):

Keep every column from the input row next to each file, so photo 37 stays next to product 37. Turn off for file details only.

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

How many files to download in parallel. Files stream to disk, so memory is not the limit; be gentle with small hosts.

## `requestTimeoutSecs` (type: `integer`):

How long one file may take to download in full before it is abandoned. Raise it for large videos on slow hosts. A timed-out file is never charged.

## `maxFileMb` (type: `integer`):

Files bigger than this are skipped and not charged.

## `maxFilesPerRow` (type: `integer`):

The most links taken from one row, counted across every link column.

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

A ceiling on files downloaded in the whole run, as a cost cap. Links beyond it are listed as skipped and not charged. 0 means no limit.

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

A ceiling on how many rows are read from the input.

## `exportFormats` (type: `array`):

Also write the results table (every row with its file link) as a downloadable CSV or Excel file.

## `outputDatasetName` (type: `string`):

Also append every kept row to a named dataset that persists across runs. Not charged again.

## `webhookUrl` (type: `string`):

POST the run summary (counts, sizes, ZIP links) to this URL when the run finishes, for Slack, Zapier, Make, n8n or your own API. Charged only on a confirmed 2xx response.

## Actor input object example

```json
{
  "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-product-images.csv",
  "fileFormat": "auto",
  "urlField": "imageUrl, gallery",
  "fileTypes": [],
  "fileNameField": "sku",
  "createZip": false,
  "zipPartMb": 1000,
  "dedupeUrls": true,
  "keep": "all",
  "keepOriginalFields": true,
  "concurrency": 8,
  "requestTimeoutSecs": 120,
  "maxFileMb": 200,
  "maxFilesPerRow": 100,
  "maxFiles": 0,
  "exportFormats": []
}
```

# Actor output Schema

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

One row per link with the stored file's public link, name, type, size and status, next to your original columns.

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

Every stored file, the ZIP parts and the summary, in the run's key-value store.

## `downloadSummary` (type: `string`):

Statuses, counts by type, total size, ZIP links and warnings.

# 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 = {
    "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-product-images.csv",
    "urlField": "imageUrl, gallery",
    "fileNameField": "sku"
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/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 = {
    "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-product-images.csv",
    "urlField": "imageUrl, gallery",
    "fileNameField": "sku",
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/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 '{
  "fileUrl": "https://nerolabs-samples.nerolabs.workers.dev/sample-product-images.csv",
  "urlField": "imageUrl, gallery",
  "fileNameField": "sku"
}' |
apify call nerolabs/bulk-file-downloader --silent --output-dataset

```

## MCP server setup

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