# Bulk Image Downloader – Download Images, Resize & WebP (`tidyrun/bulk-image-downloader`) Actor

Download thousands of images from a URL list or any scraper dataset. Resize, convert to WebP/JPEG/PNG, skip duplicates and icons, get one ZIP. $1 per 1,000 images; failed downloads free.

- **URL**: https://apify.com/tidyrun/bulk-image-downloader.md
- **Developed by:** [Tidyrun](https://apify.com/tidyrun) (community)
- **Categories:** Developer tools, Automation, E-commerce
- **Stats:** 4 total users, 3 monthly users, 96.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 images

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 Downloader – Resize & WebP

Download **thousands of images in one run** from a list of URLs **or straight from any scraper's dataset**. Each image is saved as a file with a clickable link, and you can optionally **resize, convert to WebP/JPEG/PNG, skip duplicates and tiny icons, and get everything in one ZIP**.

**$1 per 1,000 images saved. Failed downloads, duplicates and skipped icons are free.**

### Quick start

1. Paste image links into **Image URLs** (or upload a text file with one URL per line), **or** pick a scraper's dataset in **Take image URLs from a dataset**.
2. Optional: choose **Convert to** WebP, set **Max width / Max height**, tick **ZIP all images**.
3. Click **Start**. The **Output** tab shows every image as a preview with its size, and a link to the file.

Input example (URL list):

```json
{
    "imageUrls": [
        { "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg" },
        { "url": "https://upload.wikimedia.org/wikipedia/commons/b/bd/Lorine_Niedecker_reading_by_river.webp" }
    ],
    "convertTo": "webp",
    "maxWidth": 1200,
    "zip": true
}
```

Input example (images from a scraper run, e.g. a product scraper whose items have `images: [{ "url": ... }]`):

```json
{
    "datasetId": "YOUR_SCRAPER_RUN_DATASET_ID",
    "urlField": "images.url",
    "minWidth": 100
}
```

`urlField` accepts a field holding one URL, a list of URLs, or objects with `url`/`src`. Dot paths work: `imageUrl`, `images.0.url` (first image only), `images.url` (every image). Leave it empty and the Actor finds image links on its own.

One output row per image:

```json
{
    "sourceUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg",
    "status": "saved",
    "fileUrl": "https://api.apify.com/v2/key-value-stores/.../records/960px-The_Earth_seen_from_Apollo_17-129fefd01b.webp?signature=...",
    "key": "960px-The_Earth_seen_from_Apollo_17-129fefd01b.webp",
    "contentType": "image/webp",
    "width": 960,
    "height": 961,
    "bytes": 192974,
    "originalBytes": 267290,
    "originalFormat": "jpeg",
    "sha256": "5d0c…",
    "recordIndex": null,
    "error": null
}
```

`status` is `saved`, `duplicate` (same file as an earlier row; `fileUrl` points to that copy), `skipped` (below the minimum size) or `failed` (with an `error` that says what to fix). `recordIndex` is the position of the source item in your dataset, so you can join images back to the scraped records. `fileUrl` opens in any browser without an API token.

### Why this one

| Common problem | What happens here |
| --- | --- |
| Images are inside another scraper's results | Pick the dataset and the field (dot paths like `images.url` work); every row keeps the dataset record index. |
| "Out of memory" on big batches | Images are handled a few at a time with limits that follow the run's memory; a very large image gets a clear error instead of crashing the run. |
| Same picture downloaded many times | URLs are downloaded once, and identical files (same SHA-256) are saved once. Duplicates are free. |
| Logos, icons and tracking pixels mixed in | **Min width / Min height** skips them, free of charge. |
| Files too big for your site or app | **Max width / Max height** shrinks them (aspect ratio kept, never enlarged); **WebP** is usually 25–35% smaller than JPEG. |
| Getting files out of Apify | Signed `fileUrl` links that open anywhere, plus an optional single **images.zip**. |
| Paying for broken links | 404s, web pages instead of images, blocked or oversized files are **never charged**. |

### Pricing

| Event | Price |
| --- | --- |
| Image saved (files over 5 MB count one more per started 5 MB) | $0.001 |
| ZIP, per started 100 MB of ZIP file (runs over 1 GB get several ZIP parts) | $0.02 |

So 1,000 normal images cost $1; a 12 MB photo counts as 3 images. Use **Max width/height** or WebP to keep files small and cheap. Failed downloads, duplicates and skipped images are free, and the run stops cleanly at your maximum charge limit.

### Call it from your code

```bash
curl -X POST "https://api.apify.com/v2/acts/tidyrun~image-download/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"imageUrls":[{"url":"https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg"}],"convertTo":"webp"}'
```

To run it automatically after a scraper, add it as an **integration** on the scraper's page and set **Take image URLs from a dataset** to `{{resource.defaultDatasetId}}`.

### Tips

- **Keep original format** with no resize saves each file byte for byte, which is the fastest and cheapest mode.
- Converting many full-size photos is CPU work: run with **2 GB memory** for those; it finishes faster for about the same cost.
- SVG files stay SVG with "keep"; they are rendered to pixels when you convert or resize. Animated GIFs stay animated when converted to WebP.
- The run summary (`OUTPUT` in the key-value store) has the counts and the ZIP link.

### Responsible use

This Actor downloads public image files with a plain HTTP request that identifies itself as `TidyrunImageDownloader/1.0`, at most 4 at a time per site, and slows down when a site asks it to (HTTP 429). It does not log in, solve CAPTCHAs, or get around hotlink protection or bot walls; such images are reported as errors and not charged. Make sure you have the right to download and store the images you list, and respect each site's terms and copyright.

# Actor input Schema

## `imageUrls` (type: `array`):

Direct links to image files (JPEG, PNG, WebP, GIF, AVIF, TIFF, SVG). You can also upload a text file with one URL per line.

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

Optional dataset, for example from a scraper run. Its image links are downloaded together with the URLs above, and each row keeps the dataset record index. When connected as an integration after a scraper, use {{resource.defaultDatasetId}}.

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

Field that holds the image link: a URL, a list of URLs, or objects with url/src. Dot paths work: images.0.url, or images.url for every image in a list. Leave empty to find image links automatically.

## `convertTo` (type: `string`):

Keep saves the original file byte for byte (unless you resize). WebP is usually 25–35% smaller than JPEG. SVG is saved as SVG with keep, or rasterized when converting.

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

Shrink larger images to fit this width. Aspect ratio is kept and small images are never enlarged.

## `maxHeight` (type: `integer`):

Shrink larger images to fit this height. Works together with Max width.

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

JPEG, WebP and AVIF quality when an image is converted or resized. Lower means smaller files.

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

Also save one images.zip with every saved image (split into 1 GB parts for very large runs). Charged per started 100 MB of ZIP.

## `dedupe` (type: `boolean`):

Download each URL once, and save identical files (same SHA-256) once. Duplicate rows link to the first copy and are not charged.

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

Skip images narrower than this, e.g. 100 to drop icons and tracking pixels. Skipped images are not charged.

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

Skip images shorter than this.

## `maxImages` (type: `integer`):

Stop after saving this many images. Leave empty for no limit.

## `maxFileSizeMb` (type: `number`):

Larger files are skipped with an error (not charged).

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

How long one download may take before it is given up (not charged).

## Actor input object example

```json
{
  "imageUrls": [
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg"
    },
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/960px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg"
    },
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/960px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg"
    }
  ],
  "convertTo": "keep",
  "quality": 82,
  "zip": false,
  "dedupe": true,
  "minWidth": 0,
  "minHeight": 0,
  "maxFileSizeMb": 25,
  "timeoutSecs": 30
}
```

# Actor output Schema

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

One row per image: fileUrl, sourceUrl, status (saved, duplicate, skipped, failed), width, height, bytes, sha256, dataset record index and any error.

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

Counts of saved, duplicate, skipped and failed images, and the ZIP link when "ZIP all images" is on.

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

Every saved image (and images.zip) in the run's key-value store.

# 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 = {
    "imageUrls": [
        {
            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg"
        },
        {
            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/960px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg"
        },
        {
            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/960px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidyrun/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 = { "imageUrls": [
        { "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg" },
        { "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/960px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg" },
        { "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/960px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidyrun/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 '{
  "imageUrls": [
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/960px-The_Earth_seen_from_Apollo_17.jpg"
    },
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/960px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg"
    },
    {
      "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/960px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg"
    }
  ]
}' |
apify call tidyrun/bulk-image-downloader --silent --output-dataset

```

## MCP server setup

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