# Website Image Downloader: Bulk Images from Pages or URL Lists (`jtpalms/website-image-downloader`) Actor

Download every image from web pages or direct image URLs as files or ZIP archives, with source page, alt text, size, format and hash per image. robots.txt honored, no proxy. Or list image URLs only, for audits. USD 2.50 per 1,000 images saved.

- **URL**: https://apify.com/jtpalms/website-image-downloader.md
- **Developed by:** [JT Palms](https://apify.com/jtpalms) (community)
- **Categories:** Developer tools, Automation, SEO tools
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 image saveds

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

## Website Image Downloader: Bulk Images from Pages or URL Lists

Give it web pages or direct image URLs. Get every image back as a file or in a ZIP archive, with one row per image: the page it was found on, the image URL, alt text, width, height, size, format, SHA-256 and a link to the saved file. Or switch to list mode and get only the image URLs and alt text for an audit, without downloading anything.

It uses plain HTTP only: no browser, no proxy, no login. It reads robots.txt (pages and image hosts, Crawl-delay included), keeps to at most 2 requests at a time per host and 500 ms between requests, and names itself honestly in the User-Agent.

**USD 2.50 per 1,000 images saved. USD 0.80 per 1,000 images listed in list mode.** Plus a start fee of USD 0.001 per run. Images that are skipped, duplicated, too large, blocked by robots.txt or that fail are free.

### What people use it for

- **Site migrations and backups.** Moving a shop, a blog or a media library to a new platform or CDN, and want every image of your own site in one place, named safely, with a record of which page used it.
- **Image SEO and accessibility audits.** List mode returns one row per image on each page with its alt text (missing, empty or filled), title attribute, size and where it was found (img, srcset, picture, lazy-loaded, CSS background, og:image, twitter:image). Filter the `hasAlt` column and you have your fix list. Schedule it monthly.
- **Datasets you have the right to use.** Collect images from open-licensed or public-domain pages, or from a site you have permission to copy, with the hash and size of every file, for research and machine learning.
- **Asset collection from your own sites.** Agencies and designers pulling logos, product shots and banners from sites they own or work on.

### Sample output

A real run on `https://commons.wikimedia.org/wiki/Commons:Featured_pictures` (the default input: 30 pictures, JPEG only, files, 16 seconds, 3.9 MB). Long URLs shortened:

```json
{
  "imageUrl": "https://thumb.wikimedia.org/wikipedia/commons/thumb/0/08/Bessen_van_een_Mahonia_aquifolium...01.jpg/960px-Bessen_van_een_Mahonia_aquifolium...01.jpg?utm_source=commons.wikimedia.org&utm_campaign=parser&utm_content=thumbnail",
  "sourcePageUrl": "https://commons.wikimedia.org/wiki/Commons:Featured_pictures",
  "pageTitle": "Commons:Featured pictures - Wikimedia Commons",
  "alt": null,
  "titleAttr": null,
  "hasAlt": false,
  "discoveredVia": "srcset",
  "format": "jpg",
  "mimeType": "image/jpeg",
  "width": 960,
  "height": 960,
  "fileSizeBytes": 126691,
  "sha256": "ac3dccaeeb1ecbddc44143aae31e5946dc637e51486b700cd372924f47f5ee25",
  "filename": "960px-Bessen_van_een_Mahonia_aquifolium._17-08-2025._(actm.)_01.jpg",
  "fileKey": "960px-Bessen_van_een_Mahonia_aquifolium._17-08-2025._(actm.)_01.jpg",
  "fileUrl": "https://api.apify.com/v2/key-value-stores/<store id>/records/960px-Bessen_van_een_Mahonia_aquifolium._17-08-2025._(actm.)_01.jpg",
  "archiveEntry": null,
  "duplicateOf": null,
  "status": "saved",
  "skipReason": null,
  "note": null,
  "error": null,
  "httpStatus": 200,
  "finalImageUrl": null,
  "fetchedAt": "2026-10-06T21:55:37.596Z"
}
```

`alt` is `null` when the page has no alt attribute at all and an empty string when the page marks the image as decorative with `alt=""`. `hasAlt` is true only when there is real alt text. Skipped, duplicate and failed images also get a row (free), with `status`, `skipReason`, `note` or `error` saying why. A page that cannot be read gets one row with `imageUrl: null`.

The run summary (key-value store record `SUMMARY`) has the counts: images saved or listed, duplicates, skipped by reason, errors, pages blocked by robots.txt, pages that need JavaScript, bytes downloaded, the ZIP archives written, requests and seconds:

```json
{
  "mode": "download",
  "outputFormat": "files",
  "imagesSaved": 30,
  "billedImages": 30,
  "skipped": {},
  "errors": 0,
  "pagesFetched": 1,
  "imagesFound": 30,
  "imageBytesDownloaded": 3883345,
  "bytesReceivedTotal": 4461630,
  "archives": [],
  "requests": 35,
  "secs": 16.3
}
```

### Modes and options

| Setting | What it does |
|---|---|
| `startUrls` | Web pages (every image on the page is found) or direct image URLs. One per line. An address without an image extension that answers with an image is downloaded as an image. |
| `mode` | `download` saves every image. `list` returns image URLs and details only, nothing is downloaded (cheaper). |
| `outputFormat` | `files`: each image is its own file in the key-value store, with a link in its row (default). `zip`: archives for the whole run. `zipPerUrl`: one archive per input page. |
| `filenameMode` | `original` (the name in the URL, made safe), `hash` (first 16 characters of the SHA-256) or `numbered`. The extension always matches the real format. |
| `maxImagesPerPage`, `maxImages` | Images taken per page (default 100) and in the whole run (default 1,000, up to 100,000). |
| `maxFileMb`, `maxTotalMb` | One image at most 20 MB by default (up to 50). The run stops starting downloads after 500 MB by default (up to 5,000). Oversized images are skipped, not charged. |
| `maxZipMb` | ZIP archives are split at this size: default 50 MB, never above 100 MB. |
| `formats` | Formats to keep: JPG, PNG, WebP, GIF, SVG, AVIF, BMP by default; ICO, TIFF and HEIC on request. The format is read from the file's bytes, not its name. |
| `minWidth`, `minHeight`, `minFileKb` | Skip small images and tracking pixels. Download mode reads the real size from the file. In list mode the size filters need `fetchDimensions`. |
| `sameDomainOnly` | Only images hosted on the page's own domain or its subdomains. Off by default because many sites use a CDN. |
| `includeImageUrlPatterns`, `excludeImageUrlPatterns` | Keep or drop image URLs by glob (`**/uploads/**`), plain text (`/thumbnails/`) or regular expression (`re:\.png$`). |
| `srcsetPolicy` | `largest`: one image per srcset or picture, the widest (default). `all`: every size. |
| `includeBackgroundImages`, `includeMetaImages`, `includeIcons` | CSS background images in style attributes and the page's own style blocks (on), og:image and twitter:image (on), favicons and touch icons (off). |
| `crawlDepth`, `maxPages` | 0 reads only the pages you gave. 1 to 3 also reads same-site pages linked from them, up to `maxPages` per start URL. Pages must pass robots.txt. |
| `includeUrlPatterns`, `excludeUrlPatterns`, `stayInStartFolder` | Which linked pages to visit. |
| `fetchDimensions` | List mode only: read the first 64 KB of each image for width, height, real format and size. Adds one request per image. |
| `uniqueImageUrls` | List mode: list each image URL once for the whole run instead of once per page. |
| `preferModernFormats` | Ask image CDNs for WebP and AVIF. Off by default so you get the original format. |
| `requestsPerSecondPerHost` | 0.1 to 2 (2 means one request every 500 ms, the fastest this actor goes). |

Duplicates are recognised twice: the same image URL seen again is a free `duplicate` row without a second download, and a different URL that turns out to hold the same bytes is a free `duplicate` row pointing to the first (`duplicateOf`). In list mode the same image on two pages gives two rows, because an alt text audit needs every page.

### ZIP archives and size limits

- Files mode keeps each image as its own record, so the only limit is `maxFileMb` (20 MB by default, 50 MB at most).
- ZIP archives are built in memory and written when they reach `maxZipMb` or at the end of the run. They never exceed **100 MB**, far below what the key-value store accepts (a 150 MB record was tested). A single image can be at most 90% of the archive limit.
- A row is written and charged only after the archive that holds its image has been stored, so you never pay for an image you cannot get.
- Memory: the run lowers its number of parallel downloads when `maxFileMb` times the parallel downloads would not fit in the run's memory (1 GB by default).
- Originals are saved byte for byte. If a photo has EXIF data such as a GPS position, it stays inside the saved file. This actor never reads or outputs EXIF, there are no face or person fields, and nothing is re-encoded.

### What it does not do

- **JavaScript-only galleries.** There is no browser. A page that builds its images with JavaScript has no images in its HTML, and you get a free row saying `needs JavaScript` with the number of noscript blocks and image-looking data attributes it saw.
- **Login walls and social networks.** No login, no cookies, no CAPTCHA handling. Social network pages are not supported and mostly return a login page.
- **Hotlink protection.** A server that answers 403 is reported and skipped. There are no tricks to get around it.
- **Video, audio and PDFs.** Images only. Stylesheet files linked from a page (`.css`) are not read, only the page's own style blocks.
- **A browser's view of the page.** Images are found in the HTML as the server sends it, with img, srcset, picture, lazy-load attributes, CSS backgrounds and meta tags.

### Rights and robots.txt

Images belong to their publishers. You are responsible for having the right to download and use every image you collect. This actor is for sites you own or have permission to archive, and for content with an open licence or in the public domain. It is not meant for property listing photos. Downloading an image does not give you the right to reuse it.

robots.txt is always respected, for the pages and for every image host, and there is no switch to turn that off. A page or image path that robots.txt disallows is skipped and reported (`blocked by robots.txt`), and a host whose robots.txt cannot be read is not fetched. A Crawl-delay in robots.txt replaces the 500 ms spacing.

The default input reads the Featured pictures page of Wikimedia Commons, whose pictures are freely licensed. Each picture has its own licence and author on its Commons file page, and some licences require attribution.

### Pricing

| | Price |
|---|---|
| Image saved (download mode) | USD 0.0025 each, USD 2.50 per 1,000 (Gold plan USD 2.00) |
| Image listed (list mode) | USD 0.0008 each, USD 0.80 per 1,000 |
| Run start | USD 0.001 per run |

Examples:

- The default run, 30 pictures: USD 0.076.
- 1,000 images: USD 2.50.
- Backing up 200 pages with 30 images each (6,000 images): USD 15.00.
- A list-only alt text audit of 500 pages with 40 images each (20,000 rows): USD 16.00.

Skipped, duplicate, oversized, blocked and failed images cost nothing. Set a maximum cost on the run and it stops cleanly when it is reached, keeping everything saved so far. Apify also bills platform usage (compute and data transfer) to the person running the actor, so the size limits above double as cost guards.

### Tips

- For a quick look at a site, start with list mode and `fetchDimensions` off, then download only what you need with `includeImageUrlPatterns`.
- Set `minFileKb` to 2 to drop tracking pixels and spacers.
- To move a site, use `crawlDepth` 1 or 2 with `maxPages`, `outputFormat` `zipPerUrl`, and keep the dataset: it is the map from each page to its images.

### Related actors

- [Website Screenshot API: Full Page, Mobile & URL to PDF](https://apify.com/JTPalms/website-screenshot): Screenshot a list of URLs as PNG, JPEG, WebP or PDF: full page, first screen, one element, or an exact clip rectangle, on desktop,...
- [Sitemap URL Extractor: All Pages of Any Website](https://apify.com/JTPalms/sitemap-url-extractor): Get every URL of a website from its XML sitemaps.
- [Link Preview & URL Metadata: Open Graph, Meta Tags, Favicon](https://apify.com/JTPalms/link-preview-metadata): Get the URL metadata a link preview shows: title, description, image, site name, Open Graph and Twitter card meta tags, best favicon,...
- [Website to Markdown Crawler for RAG: Fast, No Browser](https://apify.com/JTPalms/website-to-markdown-crawler): Crawl a site into clean Markdown for RAG and LLMs without a browser.

# Actor input Schema

## `startUrls` (type: `array`):

Web pages or direct image URLs, one per line. A page is fetched with plain HTTP, no browser: images that only appear after JavaScript runs are not found. The default is the Featured pictures page of Wikimedia Commons, whose pictures are freely licensed.

## `mode` (type: `string`):

download: save every image (charged per image saved). list: return image URLs, alt text and page details without downloading anything, for audits (charged per image listed, much cheaper).

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

files: each image is its own file in the key-value store with a public link in its row. zip: archives of the whole run (split at the size below). zipPerUrl: one archive per input page. Archives are written when full or at the end of the run, and a row is charged only after its archive exists.

## `filenameMode` (type: `string`):

original: the name in the image URL, made safe, with the real format as extension. hash: the first 16 characters of the SHA-256. numbered: image-0001, image-0002 and so on.

## `maxZipMb` (type: `integer`):

A ZIP archive is closed and a new one started when it would grow past this size (1 to 100, default 50). Archives are built in memory, so they never exceed 100 MB.

## `maxImagesPerPage` (type: `integer`):

Images taken from each page after the filters below (1 to 1,000).

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

Hard cap on images saved (or listed) in the whole run (1 to 100,000). The run stops when it is reached.

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

Larger images are skipped and not charged (1 to 50, default 20). Images are read into memory, so the run lowers its parallel downloads when this is high and the run has little memory.

## `maxTotalMb` (type: `integer`):

Stops starting new downloads once this many megabytes have been received (10 to 5,000, default 500). Apify bills data transfer, so this is also a cost guard.

## `formats` (type: `array`):

Only these formats are kept. The format is read from the file's bytes, not from its name. ICO (favicons) is off by default.

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

Skip images narrower than this. Download mode reads the real size from the file; list mode needs 'Read dimensions'. 0 means no limit.

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

Skip images shorter than this (same rules as the width). 0 means no limit.

## `minFileKb` (type: `integer`):

Skip tiny files such as tracking pixels and spacers. 0 means no limit; 2 is a good start.

## `sameDomainOnly` (type: `boolean`):

Keep only images hosted on the page's own domain or its subdomains. Many sites serve images from a CDN, so this is off by default.

## `includeImageUrlPatterns` (type: `array`):

Optional. Keep only image URLs that match at least one pattern: a glob (\* inside one path segment, \*\* across segments, for example **/uploads/**), a plain text the URL must contain, or a regular expression written as re:.(jpg|png)$.

## `excludeImageUrlPatterns` (type: `array`):

Image URLs that match any pattern are skipped (same pattern types as above), for example \**/sprite*.png or /thumbnails/.

## `srcsetPolicy` (type: `string`):

largest: one image per srcset or picture, the one with the largest width. all: every size listed.

## `includeBackgroundImages` (type: `boolean`):

Also take images from inline style attributes and the page's own <style> blocks. Linked stylesheet files are not read.

## `includeMetaImages` (type: `boolean`):

Also take og:image, twitter:image and link rel=image_src.

## `includeIcons` (type: `boolean`):

Also take the page's icon links. Needs the ICO format ticked for .ico files.

## `crawlDepth` (type: `integer`):

0: only the pages you gave. 1 to 3: also read same-site pages linked from them, that many clicks deep. robots.txt is obeyed and requests stay polite.

## `maxPages` (type: `integer`):

When following links: the most pages read for each start URL, the start page included (1 to 1,000).

## `stayInStartFolder` (type: `boolean`):

When following links, only visit pages under the folder of the start URL (https://example.com/blog/ covers /blog/).

## `includeUrlPatterns` (type: `array`):

Optional. When following links, visit only pages that match a pattern (glob, plain text or re: regular expression).

## `excludeUrlPatterns` (type: `array`):

When following links, skip pages that match any pattern, for example **/login** or /cart.

## `fetchDimensions` (type: `boolean`):

List mode only: read the first 64 KB of each image to get width, height, real format and size. Adds one request per image. Rows for images that fail are free.

## `uniqueImageUrls` (type: `boolean`):

List mode normally returns one row per page and image (so a logo on 500 pages gives 500 rows, which is what an alt text audit needs). Turn this on to list each image URL once for the whole run.

## `preferModernFormats` (type: `boolean`):

Send an Accept header that lets image CDNs answer with WebP or AVIF. Off by default so you get the original format.

## `requestsPerSecondPerHost` (type: `number`):

Pause between requests to one host (0.1 to 2; 2 means one every 500 ms, which is the fastest this actor goes). At most 2 requests run at once per host. A Crawl-delay in robots.txt always wins.

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

Downloads in progress at once across all hosts (1 to 16). Per host there are never more than 2.

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

5 to 120.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://commons.wikimedia.org/wiki/Commons:Featured_pictures"
    }
  ],
  "mode": "download",
  "outputFormat": "files",
  "filenameMode": "original",
  "maxZipMb": 50,
  "maxImagesPerPage": 30,
  "maxImages": 1000,
  "maxFileMb": 20,
  "maxTotalMb": 500,
  "formats": [
    "jpg",
    "jpeg"
  ],
  "minWidth": 0,
  "minHeight": 0,
  "minFileKb": 2,
  "sameDomainOnly": false,
  "srcsetPolicy": "largest",
  "includeBackgroundImages": true,
  "includeMetaImages": true,
  "includeIcons": false,
  "crawlDepth": 0,
  "maxPages": 10,
  "stayInStartFolder": false,
  "fetchDimensions": false,
  "uniqueImageUrls": false,
  "preferModernFormats": false,
  "requestsPerSecondPerHost": 2,
  "maxConcurrency": 8,
  "timeoutSecs": 30
}
```

# Actor output Schema

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

One row per image in the default dataset (JSON, CSV, Excel via the format parameter): page, image URL, alt text, size, format, hash and the link to the saved file.

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

The saved images, or the ZIP archives, in the default key-value store.

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

Counts (saved, listed, duplicates, skipped by reason, errors), bytes, archives, pages blocked by robots.txt and request statistics.

# 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 = {
    "startUrls": [
        {
            "url": "https://commons.wikimedia.org/wiki/Commons:Featured_pictures"
        }
    ],
    "maxImagesPerPage": 30,
    "formats": [
        "jpg",
        "jpeg"
    ],
    "minFileKb": 2
};

// Run the Actor and wait for it to finish
const run = await client.actor("jtpalms/website-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 = {
    "startUrls": [{ "url": "https://commons.wikimedia.org/wiki/Commons:Featured_pictures" }],
    "maxImagesPerPage": 30,
    "formats": [
        "jpg",
        "jpeg",
    ],
    "minFileKb": 2,
}

# Run the Actor and wait for it to finish
run = client.actor("jtpalms/website-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 '{
  "startUrls": [
    {
      "url": "https://commons.wikimedia.org/wiki/Commons:Featured_pictures"
    }
  ],
  "maxImagesPerPage": 30,
  "formats": [
    "jpg",
    "jpeg"
  ],
  "minFileKb": 2
}' |
apify call jtpalms/website-image-downloader --silent --output-dataset

```

## MCP server setup

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