# Bulk Image & File Downloader: SKU-named ZIP + Image Audit (`open-data-tools/bulk-image-file-downloader`) Actor

Download images and files in bulk from a list of links, a CSV/Google Sheet or another Actor's dataset. Names files by SKU, splits ZIPs at 500 MB, and audits every link: broken, duplicate, too small, non-square, wrong type, CMYK. $1 per 1,000 files, no start fee.

- **URL**: https://apify.com/open-data-tools/bulk-image-file-downloader.md
- **Developed by:** [Open Data Tools](https://apify.com/open-data-tools) (community)
- **Categories:** E-commerce, 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

$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 Image & File Downloader: SKU-named ZIP + Image Audit

Download images and other files in bulk from a list of links, a CSV or Google Sheet, or the results of another Actor. Files are named by SKU (`MUG-B_1.jpg`, `MUG-B_2.jpg`) or by their own file name, saved with a download link each, and can be packed into ZIP files that split at 500 MB. Every link also gets an audit row, so you can see which product images are broken, duplicated, too small, not square, the wrong file type or CMYK.

It costs **$1 per 1,000 downloaded files**, with **no start fee**. Broken links, web pages, refused hosts and skipped files are free.

### What you get for each link

- **status**: `ok`, `failed` or `skipped`, with the HTTP status and the reason
- **fileUrl**: a download link for the saved file, and **fileName**
- **Real file type** from the file's first bytes, not from the link: a `.jpg` link that serves WebP is saved as `.webp` and flagged `type-mismatch`
- **bytes**, and for images **width x height**, format and colour space
- **sha1** content hash, with **duplicateOf** pointing to the first link with the same file
- **sku** and **sourceRow**, so every file maps back to the row it came from
- **flags**: `broken-link`, `duplicate`, `small`, `non-square`, `type-mismatch`, `cmyk`, `animated`, `tiny-file`, `redirected`, `html-page`, `too-big`, `download-error`

At the end of the run you also get `manifest.csv` (every link mapped to its file name, SKU, ZIP part and flags), the ZIP parts if you asked for them (each carries its own `manifest.csv`), and a `SUMMARY` record with counts per flag and the ZIP links.

### What people use it for

- **Product image exports.** Feed a Shopify, WooCommerce or marketplace product export (CSV or Google Sheet). The image column and the SKU column are found automatically, files come out as `{sku}_1.jpg`, `{sku}_2.jpg` in the order given, and the ZIP is ready to upload to another store or send to a designer.
- **After a scraper.** Pick the dataset of a product, real-estate or catalogue scraper. Fields such as `images`, `imageUrl`, `photos`, `thumbnail` or `media[].url` are detected, and lists of links in one field are all downloaded.
- **Image audits.** Turn off *Save each file separately* to check thousands of image links without storing them: find dead links, duplicates and images under the marketplace minimum (set *Flag images smaller than*, for example 1000 px).
- **Document and media backups.** PDFs, spreadsheets, audio, video and archives download the same way, up to 1 GB per file.

### Input options

| Field | What it does |
|---|---|
| File links | One link per line. Put a SKU on the line to name files: `SKU-123, https://.../123.jpg`. A line may hold several links. |
| CSV or Google Sheet link | A CSV/TSV link, or a Google Sheet shared as "Anyone with the link". Cells may hold several links separated by spaces, `|`, `;` or commas. |
| Dataset from another Actor | Pick a dataset, or use `{{resource.defaultDatasetId}}` in an integration. |
| Link field / SKU field | Leave empty to detect. Dots reach nested fields (`media.0.url`); separate several link fields with commas. |
| File names | SKU-based (default when a SKU is known), original file name, or content hash. |
| Pack all files into ZIP / ZIP part size | ZIP parts `files-001.zip`, `files-002.zip`... split at the size you set (500 MB default). Files are stored, not recompressed. |
| Store identical files once | On by default: an identical file under a second link is flagged `duplicate` and points to the first copy. |
| File types | Any, or images only. |
| Flag images smaller than | Shorter side in px for the `small` flag (500 default, 0 = off). |
| Maximum file size, Maximum number of links, Parallel downloads, Parallel downloads per host, Timeout | Limits. Per-host limit is 2 by default so no site is hammered. |

### Input example

```json
{
  "csvUrl": "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit#gid=0",
  "zip": true,
  "zipPartMb": 500,
  "minImageSide": 1000
}
```

### Output example

```json
{
  "index": 0,
  "url": "https://cdn.example.com/products/mug-blue-front.jpg",
  "sku": "MUG-B",
  "status": "ok",
  "httpStatus": 200,
  "contentType": "image/jpeg",
  "fileType": "jpg",
  "bytes": 184233,
  "width": 1000,
  "height": 1000,
  "imageFormat": "jpeg",
  "colorSpace": "srgb",
  "sha1": "6f1c0b0e2c8f4a1d9a3e5b7c2d4f6a8b0c1d2e3f",
  "duplicateOf": null,
  "flags": [],
  "fileName": "MUG-B_1.jpg",
  "fileUrl": "https://api.apify.com/v2/key-value-stores/.../records/MUG-B_1.jpg?signature=...",
  "zipPart": 1,
  "error": null,
  "source": "csv",
  "sourceRow": 2
}
```

### How it works

1. **Collect.** Links from the list, the CSV and the dataset are merged; a link given twice is downloaded once.
2. **Download.** Up to 8 files at once and at most 2 per host, served round-robin across hosts. Timeouts, rate limits (HTTP 429, honouring `Retry-After`) and server errors are retried up to 3 times.
3. **Check.** The file type is read from the file's first bytes. Images are measured without being decoded in full. A sha1 hash catches identical files.
4. **Save.** Each file goes to the run's key-value store with a signed download link, and is streamed into the current ZIP part.

### Limits and fair use

- Only the links you give are downloaded. Nothing is crawled, and links that return a web page are reported as `html-page` and not charged. To collect the images on web pages, use [Image Resizer & Converter](https://apify.com/open-data-tools/image-resizer-webp-converter) with *Web pages*, which also converts them to WebP.
- Social media (Instagram, Facebook, TikTok, YouTube, X, LinkedIn, Pinterest, Reddit), Google and Amazon hosts are refused, because their terms forbid automated downloads. Use their official export tools.
- Use it only for files you have the right to download and store.
- Files over 100 MB need more run memory; 1 GB is the default and enough for most image jobs.

### Pricing

Pay per event: **$0.001 per downloaded file** ($1 per 1,000). Failed, skipped and refused links are not charged. Platform usage is included. Set a maximum charge per run in the run options and the Actor stops cleanly when it is reached.

### Related Actors

- [Image Resizer & Converter: Bulk WebP/AVIF Compressor](https://apify.com/open-data-tools/image-resizer-webp-converter): resize and convert the downloaded images.
- [Sitemap URL Extractor](https://apify.com/open-data-tools/sitemap-extractor): list every page of a site.

# Actor input Schema

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

One direct file link per line (images, PDFs, documents, audio, video, archives). To name files by product, put the SKU on the same line: SKU-123, https://shop.example.com/img/123.jpg. A line can hold several links. Links listed twice are downloaded once.

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

Optional. A CSV/TSV file link or a Google Sheet shared as 'Anyone with the link' (for example a Shopify or WooCommerce product export). The column with file links and the SKU column are found automatically; a cell can hold several links.

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

Optional. Download the files from another Actor's results, for example a product or real-estate scraper. Pick the dataset or paste its ID. In an Actor-to-Actor integration set it to {{resource.defaultDatasetId}}.

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

Column or field that holds the file links, for CSV and dataset input. Leave empty to detect it (image, images, imageUrl, photos, thumbnail, file, pdf, downloadUrl and similar). Dots reach nested fields (media.0.url). Separate several with commas: images, thumbnail.

## `skuField` (type: `string`):

Column or field used to name files {sku}\_1.jpg, {sku}\_2.jpg. Leave empty to detect it (sku, Variant SKU, handle, productId, asin, mpn, gtin, ean, id).

## `fileNaming` (type: `string`):

The real file type decides the extension, so a .jpg link that serves WebP is saved as .webp (and flagged type-mismatch). Name clashes get -2, -3.

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

Adds files-001.zip (and -002, -003 when the size limit is reached) to the key-value store, each with a manifest.csv mapping every file back to its link and SKU. Links are in the run SUMMARY.

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

A new ZIP part starts when this size would be passed. 500 MB opens easily on any computer and in email/drive uploads.

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

Each file gets its own download link (fileUrl). Turn off to keep files only in the ZIP, or for an audit-only run (check links, sizes and dimensions without storing anything).

## `skipDuplicateFiles` (type: `boolean`):

Files with the same content (sha1) under different links are flagged duplicate and point to the first copy instead of being stored twice.

## `fileTypes` (type: `string`):

Web pages (HTML) are never saved: the row is marked failed with flag html-page and is not charged.

## `minImageSide` (type: `integer`):

Images whose shorter side is below this get the flag 'small'. Marketplaces often want 1000 px or more for zoom. 0 turns the check off.

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

Larger files fail with flag too-big and are not charged. For files over 100 MB raise the run memory too.

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

Optional cap on how many links are downloaded in this run.

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

Files downloaded at the same time across all hosts.

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

Keeps the load on each website polite. Rate limits (HTTP 429) and server errors are retried up to 3 times, honouring Retry-After.

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

A file that takes longer is retried, then marked failed.

## Actor input object example

```json
{
  "urls": [
    "https://picsum.photos/id/1015/1200/800.jpg",
    "https://picsum.photos/id/1025/800/800.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"
  ],
  "fileNaming": "auto",
  "zip": false,
  "zipPartMb": 500,
  "saveFiles": true,
  "skipDuplicateFiles": true,
  "fileTypes": "any",
  "minImageSide": 500,
  "maxFileMb": 50,
  "maxConcurrency": 8,
  "maxPerHost": 2,
  "timeoutSecs": 120
}
```

# Actor output Schema

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

One row per link: status, file link, type, size, dimensions, duplicate and audit flags.

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

Every downloaded file and ZIP part in the key-value store.

## `manifest` (type: `string`):

Every link mapped to its file name, SKU, ZIP part and flags.

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

Counts, flags, ZIP links and the fields that were detected.

# 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://picsum.photos/id/1015/1200/800.jpg",
        "https://picsum.photos/id/1025/800/800.jpg",
        "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("open-data-tools/bulk-image-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://picsum.photos/id/1015/1200/800.jpg",
        "https://picsum.photos/id/1025/800/800.jpg",
        "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("open-data-tools/bulk-image-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://picsum.photos/id/1015/1200/800.jpg",
    "https://picsum.photos/id/1025/800/800.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"
  ]
}' |
apify call open-data-tools/bulk-image-file-downloader --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,open-data-tools/bulk-image-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/uPa78yTQDmSW5rD0S/builds/KGNV1kxB9pz3DrxSc/openapi.json
