# Bulk Image Downloader - URLs, Pages & Datasets to ZIP (`martin10291/bulk-image-downloader`) Actor

Download images in bulk from image URLs, web pages (img, srcset, og:image) or any Apify dataset (images found automatically, file names from SKU). Public link per file + images.zip. Duplicates by SHA-256 are skipped and not charged, icons filtered, optional resize and WebP/JPG/PNG conversion. 300 im

- **URL**: https://apify.com/martin10291/bulk-image-downloader.md
- **Developed by:** [Martin Petrov](https://apify.com/martin10291) (community)
- **Categories:** Integrations, E-commerce, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 results

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 Downloader - URLs, Pages & Datasets to ZIP

**Problem:** your scraper gave you thousands of image links (products, listings, posts), but you need the actual files: stored, named by SKU, without duplicates, maybe resized to WebP, and as one ZIP. Doing it by hand or with a script means retries, rate limits, broken links and disk space.

**This actor downloads every image for you** from three kinds of input, in any mix:

- **Image URLs**: direct links, one per line.
- **Web pages**: every image on the page (`<img>`, lazy `data-src`, the largest `srcset` size, `og:image`, links to image files).
- **Any Apify dataset** (`datasetId` or the Integrations tab): image fields are found automatically, also nested lists like `images[].url` or `media.photos[]`. File names can come from a field such as `sku`, `asin` or `title`.

Each file is stored in the run's key-value store with a **public link** (one result row per file), and all files are packed into **images.zip**.

Measured: **300 images from 15 pages in 8.7 s** (about 2,000 images per minute, 100 MB RAM) with 12 parallel downloads.

### What you get that other downloaders don't

- **Duplicates are free**: the same picture under different URLs (CDN sizes, mirrors) is stored once (SHA-256 of the content) and **is not charged**.
- **No per-MB fee**: one flat price per stored file. Failed links, skipped icons and duplicates cost nothing.
- **Icons and tracking pixels skipped** (min width / height, default 50 px).
- **Resize + convert**: keep the original, or convert to JPG / WebP / PNG with a max width (for example 2400 px JPG -> 800 px WebP: 505 KB -> 79 KB).
- **Only-new mode** (`monitorId`): scheduled runs skip every URL stored before, so you pay only for new images.
- **Polite and honest**: max 6 parallel downloads per host, retries with back-off on 429/5xx, honest User-Agent, no proxies, no logins.

### Input example

```json
{
  "pageUrls": ["https://books.toscrape.com/"],
  "urls": ["https://picsum.photos/id/237/800/600"],
  "outputFormat": "webp",
  "maxWidth": 1200,
  "createZip": true,
  "monitorId": "my-shop-images"
}
```

Download the images of another actor's run (for example a product scraper), named by SKU:

```json
{
  "datasetId": "YOUR_DATASET_ID",
  "imageFields": ["images"],
  "nameField": "sku",
  "fileNaming": "name"
}
```

### Output example (one row per stored file)

```json
{
  "sourceUrl": "https://books.toscrape.com/media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg",
  "fileUrl": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/2cdad67c44b002e7ead0cc35693c0e8b.jpg",
  "key": "2cdad67c44b002e7ead0cc35693c0e8b.jpg",
  "fileName": "2cdad67c44b002e7ead0cc35693c0e8b.jpg",
  "contentType": "image/jpeg",
  "bytes": 9876,
  "originalBytes": 9876,
  "width": 125,
  "height": 155,
  "format": "jpg",
  "sha256": "4752d0ef411fb1c4aad31d69425d075c219345c386fd94f43ddfbf4495ea0437",
  "name": null,
  "position": null,
  "foundOn": "https://books.toscrape.com/",
  "downloadedAt": "2026-11-06T00:58:14Z"
}
```

The ZIP link is in the run's **OUTPUT** record and in the status message: `https://api.apify.com/v2/key-value-stores/STORE_ID/records/images.zip` (big runs: `images-2.zip`, `images-3.zip` ..., part size set by `zipPartMb`). `RUN_SUMMARY` lists counts (stored, duplicates, small, failed), failed links with the reason, and `lastError`.

### Use it from the API

```bash
curl -X POST "https://api.apify.com/v2/acts/martin10291~bulk-image-downloader/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://picsum.photos/id/237/800/600"], "pageUrls": ["https://books.toscrape.com/"]}'
```

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")
run = client.actor("martin10291/bulk-image-downloader").call(run_input={
    "pageUrls": ["https://books.toscrape.com/"], "outputFormat": "webp", "maxWidth": 1200})
for f in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(f["fileUrl"], f["width"], f["height"])
print(client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")["value"]["zip"])
```

### Use it as an integration (chain it after another actor)

1. Open your scraper (products, real estate, Google Maps, social posts) in Apify Console -> **Integrations** tab.
2. **Add integration** -> **Actor** -> pick this actor, set **Dataset** to `{{resource.defaultDatasetId}}`, optionally `nameField` (e.g. `sku`) and `monitorId`.
3. Every time the scraper finishes, its images are downloaded, deduplicated and zipped automatically.

### Schedule it (only new images, daily)

Create a **Schedule** (daily or hourly) with the same input and a fixed `monitorId`. Each run downloads only images that were not stored before: a daily snapshot of new product photos, listing photos or page images, without paying twice.

### Use it from AI agents (MCP)

The actor works through the Apify MCP server (`https://mcp.apify.com`), so Claude, ChatGPT, Cursor or any MCP client can call it as a tool: "download all images from this page as WebP and give me the ZIP link".

### Pricing

**$1.50 per 1,000 stored files** (pay per result). Duplicates, skipped icons, non-images and failed links are not charged. No per-MB fee (files up to `maxFileMb`, default 25 MB).

### FAQ

- **Which sites work?** Any public image link or page reachable without a login. Pages that build images only with JavaScript after load may give fewer images; then use their dataset (from a browser scraper) as input.
- **Social media, YouTube, Instagram?** Not supported as pages (they need logins/proxies). Give direct image links from your own data instead.
- **How long are files kept?** As long as the run's storage is kept by your Apify plan. Download the ZIP or move files to your storage.
- **Why was a link skipped?** See `RUN_SUMMARY`: `failedExamples` (HTTP errors, too big), `skippedSmall`, `skippedDuplicate`, `skippedType`.
- **Is it legal to download?** You are responsible for having the right to use the images. The actor only downloads public files you point it to, with an honest User-Agent.

# Actor input Schema

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

Direct image (or file) links, one per line. Each is downloaded and stored with a public link. Leave empty if you use pages or a dataset.

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

Web pages. Every image on the page is collected: <img> src, lazy data-src, the largest srcset size, og:image / twitter:image and links to image files. Small icons are filtered by Min width / Min height.

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

Optional. A dataset ID or dataset URL, for example the result of a product scraper. In the Integrations tab of another actor set it to {{resource.defaultDatasetId}}. Every image link in every item is downloaded.

## `imageFields` (type: `array`):

Optional. Field names that hold the images, for example images, thumbnailUrl or media.url (dot paths and lists work). Empty = every field that looks like an image link is used.

## `nameField` (type: `string`):

Optional. Dataset field used for file names, for example sku, asin, id or title. Files become <value>\_1.jpg, <value>\_2.jpg ... Set File naming to 'name' to use it.

## `datasetMaxItems` (type: `integer`):

Upper limit of dataset items read, to keep the cost under control.

## `monitorId` (type: `string`):

Optional. Any name, for example my-shop-images. Runs with the same monitor ID skip every URL stored in earlier runs, so a daily schedule downloads only new images. Empty = download everything every time.

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

Images only skips links that turn out not to be images. All files stores any file type.

## `outputFormat` (type: `string`):

Keep the original bytes or convert every image (transparent images get a white background in JPG). SVG and HEIC stay as they are.

## `maxWidth` (type: `integer`):

Wider images are scaled down to this width, keeping the aspect ratio. 0 = no resize.

## `quality` (type: `integer`):

Quality for converted JPG and WebP images (30-100).

## `minWidth` (type: `integer`):

Images narrower than this are skipped (icons, tracking pixels). 0 = keep all.

## `minHeight` (type: `integer`):

Images lower than this are skipped. 0 = keep all.

## `dedupeByContent` (type: `boolean`):

The same picture under different URLs (CDN sizes, mirrors) is stored once, detected by SHA-256 of the file content. Duplicates are not charged.

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

How stored files are named. Names are made unique automatically.

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

Also pack all stored files into images.zip (more parts images-2.zip ... for big runs). Links are in the OUTPUT and RUN_SUMMARY records.

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

Maximum size of one ZIP part.

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

Stop after this many stored files (each stored file = one result).

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

Bigger files are skipped.

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

How many files download at the same time (max 6 per host, to stay polite).

## Actor input object example

```json
{
  "urls": [
    "https://picsum.photos/id/237/800/600",
    "https://picsum.photos/id/1025/640/480"
  ],
  "pageUrls": [
    "https://books.toscrape.com/"
  ],
  "datasetMaxItems": 1000,
  "fileTypes": "images",
  "outputFormat": "original",
  "maxWidth": 0,
  "quality": 85,
  "minWidth": 50,
  "minHeight": 50,
  "dedupeByContent": true,
  "fileNaming": "auto",
  "createZip": true,
  "zipPartMb": 50,
  "maxFiles": 200,
  "maxFileMb": 25,
  "concurrency": 12
}
```

# Actor output Schema

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

One row per stored file: sourceUrl, fileUrl (public link), key, fileName, contentType, bytes, width, height, format, sha256, name, position, foundOn.

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

images.zip with every stored file (more parts are listed in the OUTPUT 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": [
        "https://picsum.photos/id/237/800/600",
        "https://picsum.photos/id/1025/640/480"
    ],
    "pageUrls": [
        "https://books.toscrape.com/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("martin10291/bulk-image-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/237/800/600",
        "https://picsum.photos/id/1025/640/480",
    ],
    "pageUrls": ["https://books.toscrape.com/"],
}

# Run the Actor and wait for it to finish
run = client.actor("martin10291/bulk-image-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/237/800/600",
    "https://picsum.photos/id/1025/640/480"
  ],
  "pageUrls": [
    "https://books.toscrape.com/"
  ]
}' |
apify call martin10291/bulk-image-downloader --silent --output-dataset

```

## MCP server setup

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