# Bulk File Downloader: Download Files from URLs to Storage (`ataism/file-downloader`) Actor

Bulk download files from a URL list (PDF, ZIP, CSV, images, audio, video) to storage, each with a public link, real file type and SHA-256. $1 per 1,000 files.

- **URL**: https://apify.com/ataism/file-downloader.md
- **Developed by:** [Kaizoku Data](https://apify.com/ataism) (community)
- **Categories:** Developer tools, Automation, Integrations
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 file downloaded (first 5 mb included)s

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 File Downloader: Download Files from a URL List to Storage

**Bulk download files from a list of URLs** (PDF, ZIP, CSV, images, audio, video, Office documents) into Apify storage, and get one row per file with a **public download link, the real file type, size and SHA-256**. Paste up to 1,000 direct file links, press *Start*, and download the files from your key-value store or use the links in your next tool. **$1 per 1,000 files** up to 5 MB each; failed, refused and skipped files are free.

Give it a list of **direct file links**. It downloads each file (streamed to disk, not held in memory), checks what the file really is from its first bytes, computes its **SHA-256 and size**, saves it in an Apify key-value store and returns a dataset row with a **public link**. PDF, ZIP, CSV, images, audio, video, Office documents and any other file type. Keyless, no browser, no proxy.

**Price: $1 per 1,000 files ($0.001 per file saved).** The first 5 MB of each file are included; a larger file adds **$0.001 per started 10 MB above 5 MB** (a 25 MB file is $0.001 + 2 x $0.001 = $0.003; a 100 MB file is $0.011). No start fee on top of Apify's default. Only files that are saved are charged.

- **Real file type, not the name:** magic bytes first, then Content-Type, then the extension. Web pages (HTML) are refused for free, so a login page or an error page is never saved as "report.pdf".
- **A row per file:** public link, key, final URL after redirects, file name, server type, detected type, size in bytes, SHA-256.
- **Filters and dedupe:** keep only the types you list (`pdf`, `application/pdf`, `image/*`), skip a file whose SHA-256 you already saved in this run, cap the file size (default 100 MB, up to 500 MB).
- **Your own names and your own store:** give each URL a file name, and pick one of your key-value stores to receive the files.
- **Careful with other people's servers:** robots.txt of each host is checked, a `Crawl-delay` is honoured in full, at most two downloads at a time per host (the host of every redirect hop counts) and at least 250 ms between requests to one host, no crawling, no proxy, no cookies, and social-media and platform hosts are refused.
- **Quick start:** press *Start*. The prefilled input downloads three small public files (a public-domain PDF and a public-domain NASA photo from Wikimedia Commons, and a CC BY 4.0 CSV from GitHub), about 1 second, $0.003.

### Use cases

Each input below can be pasted as-is into the JSON tab of the input (the links are public files and work today).

**1. Archive a list of report PDFs under your own file names** (compliance archives, research libraries, invoices you have links to). Only real PDFs are kept: a login page or an error page that answers instead is refused for free.

```json
{
  "fileUrls": ["https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf"],
  "fileNames": ["maine-constitution-2013.pdf"],
  "allowedTypes": ["pdf"]
}
```

**2. Turn image links from a scraper or a spreadsheet into stored files with stable links** (product photos, press images, assets for a catalogue). Keep images only, skip identical content, cap the size:

```json
{
  "fileUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/a/a8/NASA-Apollo8-Dec24-Earthrise.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf"
  ],
  "allowedTypes": ["image/*"],
  "dedupeBySha256": true,
  "maxFileSizeMb": 20
}
```

The PDF in this list gets a free `type-not-allowed` row; only the photo is saved and charged.

**3. Snapshot data files on a schedule and spot changes by hash** (CSV, JSON or Parquet exports for a data pipeline). Run it on an Apify schedule; when a row's `sha256` differs from the previous run's, the file changed. With a fixed file name and a store you picked in *Save files in this key-value store*, each run overwrites the latest copy; leave `fileNames` empty instead to keep every version, because the automatic key includes the first 8 characters of the SHA-256.

```json
{
  "fileUrls": ["https://raw.githubusercontent.com/fivethirtyeight/data/master/airline-safety/airline-safety.csv"],
  "fileNames": ["airline-safety-latest.csv"],
  "allowedTypes": ["csv"]
}
```

More input shapes (your own key-value store, files up to 300 MB) are under *Example inputs* below.

**What a run costs, worked out:** 1,000 files up to 5 MB each = 1,000 x $0.001 = **$1.00**, plus Apify's start event ($0.00005 at the default 512 MB). A mixed batch of 900 small files and 100 files of 30 MB = 900 x $0.001 + 100 x ($0.001 + 3 x $0.001) = $0.90 + $0.40 = **$1.30** (+ $0.00005). Full table under *Pricing*.

### What it does

For each file URL:

1. Refuses social-media, video-platform and Google hosts before any DNS lookup (`platform`).
2. Reads the host's `robots.txt` (our User-Agent is `file-downloader-actor/0.1 (https://apify.com/ataism/file-downloader)`). A disallowed path is **not downloaded** and gets `blocked-by-robots`, free. A `Crawl-delay` is honoured in full, never shortened (see *Limits*).
3. Downloads the file (public addresses only, at most 5 redirects with every hop re-checked, size cap, stall limit and a time limit set by the file's price, see *Limits*) straight to the run's disk, computing the SHA-256 on the way.
4. Looks at the first 4 KB: a web page is refused (`not-a-file`), text served under a binary name is refused (`type-mismatch`), and a type outside your list is skipped (`type-not-allowed`). All free, and the download stops after a few KB.
5. Saves the file in the key-value store (the run's, or the one you picked), pushes one dataset row, and only then charges.

**Detected types** include PDF; ZIP, DOCX, XLSX, PPTX, EPUB, ODT, JAR; GZIP, BZIP2, XZ, 7z, RAR, TAR, ZSTD; JPEG, PNG, GIF, WebP, AVIF, HEIC, TIFF, BMP, ICO, PSD, SVG; MP3, AAC, FLAC, OGG, WAV, AIFF, MIDI, M4A; MP4, MOV, WebM, MKV, AVI, FLV, WMV, MPEG; RTF, PostScript, legacy Office (OLE), SQLite, Parquet, WASM, fonts, executables; and text formats (CSV, TSV, JSON, JSONL, XML, Markdown, plain text) by header or extension. Anything else is `application/octet-stream` and is still saved.

### Input

| Field | What it does |
|---|---|
| **File URLs** (`fileUrls`) | Direct `http(s)` links, one per line, at most 1,000 per run. Through the API each entry can also be `{"url": "...", "fileName": "report.pdf"}`. Duplicate URLs collapse into one. |
| **File names** (`fileNames`) | Optional, one per URL in the same order (leave a line empty for the automatic name). The name becomes the key in the store, made safe (no folders or odd characters) and numbered (`-2`, `-3`) if two files in the run get the same key. |
| **Allowed types** (`allowedTypes`) | Optional. Extensions (`pdf`, `.zip`) and/or MIME types (`application/pdf`, `image/*`). Others are skipped for free. The real type from the bytes decides; for plain text and unknown binaries the header and the extension are used too. Empty = every type except web pages. **Add `text/html` to download HTML pages on purpose** (`text/*` is not enough). |
| **Max file size (MB)** (`maxFileSizeMb`) | Default 100, maximum 500. Bigger files are refused from their `Content-Length`, or cut off while streaming, and are not charged. Lowered automatically if the run's disk (twice its memory) is too small. |
| **Skip duplicate content** (`dedupeBySha256`) | Off by default. On: a file whose SHA-256 matches one already saved in this run is not stored again and not charged (`status: "duplicate"`, `duplicateOf` = key of the first copy). |
| **Save files in this key-value store** (`outputStore`) | Optional. Pick one of your stores with the store picker; the Actor then gets write access to that store only (it works with Limited permissions and never asks for an API token). Empty = the run's own default store. A file with the same key in a store you pick is overwritten. |
| **Allow slow hosts** (`allowSlowHosts`) | Off by default. Off: files on a host whose robots.txt asks for a `Crawl-delay` above 1 second are skipped (free `slow-host` rows). On: delays up to 2 seconds (1.62 s at 1,024 MB of memory) are honoured in full. Above that a host is always skipped. |
| **Download timeout** (`downloadTimeoutSecs`) | Per file including redirects, default 300 s (10 to 600). Time spent waiting for a `Crawl-delay` or for a free per-host slot is not counted. Each file also has a time limit set by its price (3.6 s for a file up to 5 MB at 512 MB, longer for bigger files), counted from the moment the request is sent: connect, TLS, the wait for response headers, the robots.txt check and the download all count. See *Minimum speed* below. |

#### Example inputs

**1. A list of reports, original names, PDF only:**

```json
{
  "fileUrls": ["https://example.org/reports/q1.pdf", "https://example.org/reports/q2.pdf"],
  "fileNames": ["2026-Q1-report.pdf", "2026-Q2-report.pdf"],
  "allowedTypes": ["pdf"]
}
```

**2. Mixed assets into your own store, duplicates skipped, images and archives only:**

```json
{
  "fileUrls": ["https://example.org/a.zip", "https://example.org/photo.jpg", "https://example.org/copy-of-photo.jpg"],
  "allowedTypes": ["image/*", "zip"],
  "dedupeBySha256": true,
  "outputStore": "<your key-value store ID, from the store picker>"
}
```

**3. Large files (up to 300 MB each):** the default 512 MB of memory is enough (files are streamed to disk); only the limits change. The source must deliver about 5.2 MB/s or better for a 300 MB file (see *Minimum speed*):

```json
{
  "fileUrls": ["https://example.org/data/export.parquet"],
  "maxFileSizeMb": 300,
  "downloadTimeoutSecs": 600
}
```

### Output

One dataset row per input URL (`inputIndex` is its position in your input; rows are saved in completion order):

```json
{
  "inputIndex": 0,
  "url": "https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf",
  "finalUrl": "https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf",
  "fileName": "Constitution_of_Maine_2013.pdf",
  "status": "ok",
  "key": "Constitution-of-Maine-2013-30ee0dfa.pdf",
  "publicUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/Constitution-of-Maine-2013-30ee0dfa.pdf?signature=<signature>",
  "contentType": "application/pdf",
  "detectedType": "application/pdf",
  "detectedFrom": "magic",
  "sizeBytes": 167843,
  "sha256": "30ee0dfacc507a93ef86bde42b5d0e3bb13ae9b5e4355c26e993949f3a88c39f",
  "charged": { "file-downloaded": 1, "large-file": 0 },
  "downloadedAt": "2026-10-20T14:40:07.913Z"
}
```

- `key`: without a name from you, **file name + first 8 characters of the SHA-256 + extension**, so the same content always gets the same key and two different files with the same name never collide. With a name from you, the key is that name (made safe).
- `detectedFrom`: `magic` (from the bytes), `text` (the bytes are text of a recognisable kind), `header` or `extension` (plain text or unknown binary: the server's Content-Type or the name decided), `unknown`.
- **Stored content type:** only types a browser cannot run or render as a page keep their own Content-Type in the store (images except SVG, audio, video, fonts, PDF, archives, Office/OpenDocument files, CSV, plain text, JSON). **Everything else is stored as `application/octet-stream`, a download:** HTML, XHTML, SVG, every XML type (`xml`, `xsl`, `+xml`), JavaScript and any type not on that short list. `detectedType` still reports the real type.
- `publicUrl` is a key-value-store record link; it carries a signature when the store requires one. A named store you picked adds `storeId`.
- The run's default key-value store also holds `RUN_SUMMARY` (counts, events charged, bytes downloaded, failures by code, peak memory, why the run stopped) and `PROGRESS` (lets a restarted run skip finished files, so nothing is downloaded or charged twice). On a restart the Actor cross-checks Apify's own charge counts against what it can see; if rows that were already charged are not visible in the dataset yet it waits up to 60 s, and if they still do not show up it stops with an error instead of downloading and charging them again (progress is kept, resurrect the run later). It never charges retroactively.
- Rows with `status: "error"` have `errorCode` and `message`; `status: "duplicate"` has `duplicateOf`. Neither is charged.

#### Status and error codes (only `ok` is charged)

| `errorCode` | Meaning |
|---|---|
| `invalid-url`, `blocked`, `dns` | Not a valid URL; not http(s), or a private, loopback, link-local or internal address (also after a redirect); host does not resolve. |
| `platform` | Social-media, video-platform or Google host (YouTube, TikTok, Instagram, Facebook, X, Vimeo, SoundCloud, Spotify, Twitch, Reddit, LinkedIn, Google, Discord, Pinterest and their CDNs). Refused before any lookup, also after a redirect. |
| `blocked-by-robots` | The host's robots.txt disallows the path for our User-Agent. Not downloaded. |
| `robots-unreachable` | robots.txt could not be read (server error, HTTP 429 or timeout, tried twice), so the file was not downloaded. A missing robots.txt (HTTP 404) means no restrictions. |
| `slow-host` | The host's robots.txt asks for a `Crawl-delay` above 1 second and `allowSlowHosts` is off, or above 2 seconds (1.62 s at 1,024 MB). Free; the delay is never shortened. |
| `refused` | The server answered 401, 403, 407, 429, 451 or 999 (login, ban, rate limit), or answered 200 with a bot-challenge or access-denied page instead of the file. Not retried, with no other method tried. |
| `host-skipped` | The host refused or failed three requests earlier in this run (a refusal above, or HTTP 503), so the rest of its files are skipped. |
| `http`, `network`, `timeout`, `redirects` | Other HTTP error; connection failure or cut connection; no data for 30 s or over the download timeout; more than 5 redirects. |
| `too-slow` | Dropped, free, because the download would cost more run time than the file is worth: the file (including the wait for the server's first answer) took longer than its price pays for in compute (a file up to 5 MB: 3.6 s at 512 MB; see *Minimum speed*), or the run as a whole had passed its compute allowance and this was its slowest download. See *Limits*. |
| `too-large` | Above Max file size. |
| `not-a-file` | The link returned a web page (HTML). Use a direct file link, or allow `text/html`. |
| `type-mismatch` | The name or header promises a binary file (PDF, ZIP, image, ...) but the server sent plain text, usually an error message. |
| `type-not-allowed` | The file's real type is not in Allowed types. |
| `empty-file` | The server sent 0 bytes. |
| `disk`, `store-failed` | The file did not fit on the run's disk; the file was downloaded but could not be saved in the key-value store. |
| `budget` | The run's Max total charge cannot pay for this file (big files cost more). |
| `unexpected` | Internal error; try again, and tell us if it persists. |

### Pricing

Pay per event, plus Apify's standard Actor-start event. There is **no additional start fee** from this Actor.

| Event | Price | When |
|---|---|---|
| `file-downloaded` | **$0.001** | One per file saved, after it is stored and its row is pushed. |
| `large-file` | **$0.001** per unit | One per started 10 MB of file size above the first 5 MB (MB = 1,048,576 bytes): up to 5 MB = 0, up to 15 MB = 1, up to 25 MB = 2, up to 100 MB = 10, up to 500 MB = 50. Charged together with `file-downloaded`. |

| Each file is | Cost per file | 100 files | 1,000 files |
|---|---|---|---|
| up to 5 MB | $0.001 | $0.10 | $1.00 |
| 10 MB | $0.002 | $0.20 | $2.00 |
| 30 MB | $0.004 | $0.40 | $4.00 |
| 100 MB | $0.011 | $1.10 | $11.00 |
| 500 MB (the maximum) | $0.051 | $5.10 | $51.00 |

Failed, blocked, refused, filtered and duplicate files cost nothing. Set **Max total charge** in the run options to cap spending: the run stops cleanly when the limit is reached (`stoppedBy: "max-charge"`), never starts a download it cannot pay for, and never charges for a file it did not save. Apify subscription discounts apply on top. You pay only these events: with pay-per-event pricing, Apify bills the run's compute to us, the developer, not to you. The defaults (512 MB) keep it small next to the price.

### For AI agents (MCP)

**Purpose:** one call downloads the direct file links you give it into Apify storage and returns one row per link: `status`, a `publicUrl` to the stored file, `detectedType`, `sizeBytes` and `sha256`. It does not find files on pages and does not open or summarise file contents.

**Minimal input that works:**

```json
{ "fileUrls": ["https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf"] }
```

Everything else has a default (every type except web pages, 100 MB per file, no dedupe, the run's own store). An empty `fileUrls` list fails the run with "Add at least one file URL to download."

**One output row per URL** (trimmed): `{"inputIndex": 0, "url": "...", "status": "ok", "key": "Constitution-of-Maine-2013-30ee0dfa.pdf", "publicUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/...", "detectedType": "application/pdf", "sizeBytes": 167843, "sha256": "30ee0dfa..."}`. Rows with `status: "error"` carry `errorCode` and `message` and are free; `status: "duplicate"` is free too.

**Cost:** $0.001 per saved file up to 5 MB (1 file = $0.001, 100 = $0.10, 1,000 = $1.00), plus $0.001 per started 10 MB above 5 MB, plus Apify's standard small per-run start fee. Cap spend with Apify's *Max total charge*, `maxFileSizeMb` or `allowedTypes`.

**Limits:** direct file links only, at most 1,000 per run; social-media, video-platform and Google hosts are refused; robots.txt is honoured.

**How to call it via the Apify MCP server** (`https://mcp.apify.com`; tool names from the [apify-mcp-server README](https://github.com/apify/apify-mcp-server)):

1. `search-actors` with a query such as `bulk file downloader urls` (free) may list `ataism/file-downloader`; search results are ranked, so if it isn't listed, go straight to step 2 with the Actor name.
2. `fetch-actor-details` with actor `ataism/file-downloader` returns the input schema, pricing and output schema (free).
3. `call-actor` with `actor` = `ataism/file-downloader` and `input` = `{"fileUrls": ["https://example.org/a.pdf"]}` runs it. The response holds run metadata and a dataset ID, not the rows.
4. `get-dataset-items` with that dataset ID returns the rows (follow the `nextStep` in the `call-actor` response).

To expose only this Actor as a tool, connect to `https://mcp.apify.com?tools=ataism/file-downloader`.

### Use it via API, n8n or Make

Get an API token in Apify Console → *Settings* → *API & Integrations*, and keep it in an environment variable (`APIFY_TOKEN` below). Never paste it into shared code.

**cURL** (starts a run, waits for it and returns one row per file; the synchronous endpoint waits up to 5 minutes, fine for a few dozen small files; for bigger batches use the clients below):

```bash
curl -X POST "https://api.apify.com/v2/acts/ataism~file-downloader/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fileUrls": ["https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf"]}'
```

**JavaScript** (`npm install apify-client`):

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('ataism/file-downloader').call({
    fileUrls: ['https://example.org/a.pdf', 'https://example.org/b.zip'],
    allowedTypes: ['pdf', 'zip'],
    dedupeBySha256: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const f of items) console.log(f.url, f.status, f.detectedType, f.sizeBytes, f.sha256, f.publicUrl);
```

**Python** (`pip install "apify-client>=3"`; with versions before 3.0, `call()` returns a dict: use `run["defaultDatasetId"]`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("ataism/file-downloader").call(run_input={
    "fileUrls": ["https://example.org/a.pdf", "https://example.org/b.zip"],
    "allowedTypes": ["pdf", "zip"],
})
for f in client.dataset(run.default_dataset_id).iterate_items():
    if f["status"] != "ok":
        print("not saved (not charged):", f["url"], f.get("errorCode"), f.get("message"))
    else:
        print(f["publicUrl"], f["sizeBytes"], f["sha256"])
```

**n8n:**

1. Start with any trigger that gives you file links (a form, a sheet row, a webhook).
2. Add the **Apify** node, operation **Run an Actor and get dataset**, Actor `ataism/file-downloader`, input `{"fileUrls": ["{{ $json.fileUrl }}"]}`.
3. Use `publicUrl`, `sha256` or `sizeBytes` in the next node (a sheet, a database, a storage upload).

**Make:**

1. Add the **Apify** module **Run an Actor**, choose `ataism/file-downloader`, put your links in the input JSON and set *Run synchronously* to *Yes*.
2. Add the **Apify** module **Get Dataset Items** and map the run's default dataset ID (one bundle per file).
3. Send `publicUrl`, hashes or sizes to your next module (Google Sheets, Airtable, a webhook, a storage service).

### Privacy and terms

- **You are responsible for having the right to download and copy every file you give this Actor** (copyright, licences, contracts, privacy and data-protection law). It does not check this and does not find files for you. Files can contain personal data; the Actor does not open, scan or analyse them beyond reading their first bytes to find the type, and sends them nowhere except to the key-value store you choose.
- Files are written to the run's disk while they arrive and deleted as soon as they are saved or refused. The only copies that remain are in **your key-value store**, plus `RUN_SUMMARY` and `PROGRESS` in the run's default store. Storages belong to your Apify account and follow your retention settings.
- The `url` and `finalUrl` fields echo what you supplied and what the redirects led to (including a signed query string if you used one), so treat the dataset as sensitive if your URLs are. Logs show only a URL's host and path, never its query string, and never file content. `publicUrl` carries the store's signature when the store requires one: share it only as widely as you share the file.
- The Actor sends no cookies, no login and no API token to the sites it downloads from. No data goes to any AI or third-party service.
- **Respect the sites you download from.** robots.txt is checked and honoured, but a site's terms of use may still restrict bulk downloads. Bulk downloading is your responsibility. For example, GitHub's Acceptable Use Policies bar "excessive automated bulk activity (for example, spamming)", and Wikimedia asks scripts to identify themselves; this Actor identifies itself, fetches only the files you list and limits itself to two downloads at a time per host, but the volume you request is yours to judge.
- **Credit for the sample data:** the default CSV is `airline-safety.csv` from FiveThirtyEight's data repository (data compiled from the Aviation Safety Network), used under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/): *Aviation Safety Network via FiveThirtyEight, CC BY 4.0*. The default PDF (Constitution of Maine) and photo (NASA, Apollo 8 Earthrise) are public domain according to Wikimedia Commons.

### Safety

Only `http` and `https` URLs to **public** addresses are fetched. Private, loopback, link-local and internal addresses, and non-http schemes (`file:`, `ftp:` and so on), are blocked, including after redirects (at most 5, each re-checked, also against robots.txt and the platform list) and at connection time. Social-media and platform hosts are refused before any DNS lookup. Files are checked by content, not by name. A 401, 403, 429 or challenge page is an error row: there is no retry with other headers or methods, no CAPTCHA handling and no proxy; after three refusals the rest of that host's files in the run are skipped.

### Limits and honest caveats

- **Direct file URLs only.** No page crawling, no link discovery, nothing behind a login, no streaming-site extraction.

- Up to **1,000 files per run**, **100 MB per file by default (500 MB maximum)**, 4 downloads in parallel, at most 2 per host (the host of each redirect hop too, with at least 250 ms between requests to one host), and at most 100 GB downloaded per run (`stoppedBy: "total-size-cap"`).

- **Memory:** the Actor runs with 512 MB by default and at most **1,024 MB** (more is never needed, because files are streamed; more memory only makes waiting more expensive, and halves every per-file time limit).

- **Memory and disk:** files are streamed to the run's disk, so memory use stays near 100 to 150 MB whatever the file size (measured with a 500 MB file). A run's disk is twice its memory; at the default 512 MB about 800 MB of files can be in flight, and the file-size limit is lowered automatically if the disk is smaller. Files up to 24 MB are handed to the key-value store as one buffer (the Apify client compresses buffer uploads with gzip, which costs CPU), bigger files are streamed from disk.

- **Large records:** Apify key-value store records of 17.5 MB were verified to work on the platform (2026-10-20). Records above that, up to 500 MB, work locally (a 70 MB file is tested end to end) but have **not been verified on the platform**; if a platform upload fails the row says `store-failed` and is not charged.

- **Crawl-delay:** a `Crawl-delay` in robots.txt is honoured in full for every download from that host (also after a redirect to it), never shortened, and waiting is not part of the download timeout. Waiting costs run time (billed to us, not to you), so a host that asks for more than 1 second is skipped (free `slow-host` rows) unless you set `allowSlowHosts`, and one that asks for more than 2 seconds (1.62 s at 1,024 MB of memory: 0.9 x 1.8 s x 1,024 / memory) is always skipped. `10`, `10s` and `10 seconds` all read as 10; if several lines or groups apply, the longest counts.

- **Compute guards (why some slow downloads are dropped):** compute is billed to us as run time, so a slow source can cost more than a file earns. Everything is costed at $0.20 per compute unit and kept within 10% of the price: (1) **a time limit per file, set by the price that file pays** (base $0.001 plus $0.001 per started 10 MB above 5 MB), counted from the moment the request is sent; the size used is the larger of the announced length and the bytes received so far, so unknown sizes are covered too, and before the headers arrive only the base price counts. Connect, TLS, the wait for the first answer, the robots.txt check of the host and the download all count; waits for a Crawl-delay or a free host slot do not. Over the limit: a free `too-slow` row. (2) The **run-level guard**: if run seconds x memory x $0.20 per compute unit goes above 10% of what the run has charged, plus the price of the bytes every running download has already received, plus $0.002, no new download starts and the slowest running download is dropped as a free `too-slow` row; if nothing is running and the run is still over, it stops (`stoppedBy: "compute-guard"`). The compute of earlier processes of a restarted run counts too. (3) The server must keep sending (30 s without data). Dropped downloads are never charged. If your sources are slow, run fewer files per run or accept that slow ones are skipped.

- **Minimum speed (what the time limit means in practice):** a file must arrive, start to finish, within the time its price pays for. MB means MiB (1,048,576 bytes); 1 MiB/s is about 8.4 Mbps.

  | File size | Price | 512 MB (default): time limit / minimum average speed | 1,024 MB: time limit / minimum average speed |
  | --- | --- | --- | --- |
  | 1 MB | $0.001 | 3.6 s / 0.28 MB/s | 1.8 s / 0.56 MB/s |
  | 5 MB | $0.001 | 3.6 s / 1.39 MB/s | 1.8 s / 2.78 MB/s |
  | 10 MB | $0.002 | 7.2 s / 1.39 MB/s | 3.6 s / 2.78 MB/s |
  | 30 MB | $0.004 | 14.4 s / 2.08 MB/s | 7.2 s / 4.17 MB/s |
  | 100 MB | $0.011 | 39.6 s / 2.53 MB/s | 19.8 s / 5.05 MB/s |
  | 300 MB | $0.031 | 111.6 s / 2.69 MB/s | 55.8 s / 5.38 MB/s |
  | 500 MB | $0.051 | 183.6 s / 2.72 MB/s | 91.8 s / 5.45 MB/s |

  The speeds are the average needed if nothing else took time. Every second spent before the first byte (robots.txt of a new host, connect, TLS, a slow first answer) comes off the limit: with 1 s of such overhead a 5 MB file needs 1.92 MB/s at 512 MB (a 1 MB file: 0.38 MB/s). Small files are limited mostly by the overhead, big files by their throughput (about 2.7 MB/s, roughly 23 Mbps, at 512 MB). The default memory is the cheapest choice for slow sources: doubling the memory halves every time limit.

- A run stops early (`stoppedBy: "unpaid-work-cap"`) after 200 failed files, 1,000 MB downloaded for files that were not saved, or 10 minutes spent on failed files (downloads still waiting for a server that has not answered count too), so a list of bad links cannot burn compute unpaid. Files you filtered out with Allowed types are not counted as failed.

- The type check reads the first 4 KB. It cannot tell a ZIP-based format it does not know from a plain ZIP, and an unknown binary is reported as `application/octet-stream` but still saved. Plain text is typed from the header or the extension.

- If the same content is downloaded from two URLs without dedupe, both are saved, each with its own key (a number is added when the keys are equal).

- The public link works for as long as the key-value store exists. Unnamed stores expire after Apify's data-retention period (see Apify's documentation); pick a named store to keep files longer.

### FAQ

**Does it find files on a web page?** No. Give it the file URLs. That keeps it simple and robots.txt-checkable.

**Why was my file not charged?** Only `ok` is charged. Check `status`, `errorCode` and `message`.

**Can it download from YouTube, Instagram or Google Drive?** No. Those hosts are refused on purpose.

**Can it save HTML pages?** Only when you add `text/html` to Allowed types; it still does not crawl or render anything.

**Does the default run work without setup?** Yes. It downloads a public-domain PDF and photo from Wikimedia Commons and a CC BY 4.0 CSV (Aviation Safety Network via FiveThirtyEight, [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)) from GitHub, about half a megabyte in total.

**How much does it cost to download 1,000 files?** $1.00 if each is 5 MB or smaller, plus Apify's start event ($0.00005 at the default memory). Bigger files add $0.001 per started 10 MB above 5 MB: 1,000 files of 30 MB are $4.00. Only saved files are charged.

**Does it make a ZIP of all files?** No. Each file is stored as its own record with its own public link, so you can fetch, share or delete them one by one.

**Can I feed it links from another Actor or a spreadsheet?** Yes, as a list of URLs in `fileUrls` (via the API, n8n or Make, map your link column or dataset field into that list). It does not read another run's dataset by itself.

**Can it download from Google Drive, Dropbox share pages or behind a login?** Google hosts (including Drive) are refused. Other share pages usually return a web page, which is refused for free as `not-a-file`; use the direct download link. Nothing behind a login is downloaded.

**Where are my files and how long do they stay?** In the run's default key-value store, or in the store you picked under *Save files in this key-value store*. Unnamed stores follow Apify's data-retention period; pick a named store to keep files longer.

**Can an AI agent use it?** Yes, through the Apify MCP server; see *For AI agents (MCP)* above.

# Actor input Schema

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

Direct http(s) links to files, one per line. Web pages are refused (free) unless you allow text/html below; nothing is crawled. Private, local and internal addresses are blocked, and so are social-media, video-platform and Google hosts. robots.txt of each host is checked. Through the API you can also pass objects like {"url": "https://...", "fileName": "report.pdf"}. At most 1,000 files per run.

## `fileNames` (type: `array`):

One name per URL, in the same order as the URLs; leave a line empty to use the name the server or the URL gives. A name you give is used as the key in the store (made safe: no folders or odd characters; a number is added if two files get the same key). Without a name, the key is the file name plus the first 8 characters of its SHA-256, so the same content always gets the same key.

## `allowedTypes` (type: `array`):

Only keep files of these types; others are skipped for free. Use extensions (pdf, zip, csv) or MIME types (application/pdf, image/\*). The check uses the file's real type from its first bytes, not just its name. Empty = every type except web pages. To download HTML pages on purpose, add text/html here.

## `maxFileSizeMb` (type: `integer`):

Larger files are skipped with an error and not charged. Default 100, maximum 500. Files are streamed to the run's disk (about twice its memory), so with the default 512 MB of memory about 800 MB fit at once; the limit is lowered automatically if the disk is too small. Files above 5 MB add the large-file event (one per started 10 MB above the first 5 MB, see Pricing).

## `dedupeBySha256` (type: `boolean`):

On: a file whose SHA-256 matches a file already saved in this run is not stored again and not charged (its row says status duplicate and points to the first copy). Off (default): every file is saved.

## `outputStore` (type: `string`):

Pick one of your own key-value stores to receive the files (the Actor then gets write access to that store only; it works with Limited permissions). Empty = the run's own default store. Files with the same key in a store you pick are overwritten.

## `allowSlowHosts` (type: `boolean`):

Off: files on a host whose robots.txt asks for a Crawl-delay above 1 second are skipped (free "slow-host" rows), because waiting between downloads is billed as run time and can exceed the file price. On: delays up to 2 seconds (1.62 seconds at 1,024 MB of memory) are honoured in full between every request to that host, so the run takes longer. Hosts that ask for more than that are always skipped. The delay is never shortened. Default off.

## `downloadTimeoutSecs` (type: `integer`):

Per file, including redirects. Time spent waiting for a Crawl-delay is not counted. Every file also has a time limit set by its price, counted from the moment the request is sent (3.6 seconds for a file up to 5 MB at 512 MB; the wait for the server's first answer counts). A file that takes longer is dropped for free (see the README, Minimum speed).

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

Internal: "selfTest" runs the maintainer health check and saves no files.

## Actor input object example

```json
{
  "fileUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf",
    "https://upload.wikimedia.org/wikipedia/commons/a/a8/NASA-Apollo8-Dec24-Earthrise.jpg",
    "https://raw.githubusercontent.com/fivethirtyeight/data/master/airline-safety/airline-safety.csv"
  ],
  "maxFileSizeMb": 100,
  "dedupeBySha256": false,
  "allowSlowHosts": false,
  "downloadTimeoutSecs": 300,
  "mode": "run"
}
```

# Actor output Schema

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

One row per file: public link, key, real type, size, SHA-256, or the error (JSON, CSV, Excel or XML).

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

List of the files in the run's default key-value store (each row also has a direct public link). Files saved in a store you picked are not listed here.

## `runSummary` (type: `string`):

Counts, events charged, bytes downloaded, failures by code and why the run stopped.

# 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 = {
    "fileUrls": [
        "https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf",
        "https://upload.wikimedia.org/wikipedia/commons/a/a8/NASA-Apollo8-Dec24-Earthrise.jpg",
        "https://raw.githubusercontent.com/fivethirtyeight/data/master/airline-safety/airline-safety.csv"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ataism/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 = { "fileUrls": [
        "https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf",
        "https://upload.wikimedia.org/wikipedia/commons/a/a8/NASA-Apollo8-Dec24-Earthrise.jpg",
        "https://raw.githubusercontent.com/fivethirtyeight/data/master/airline-safety/airline-safety.csv",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("ataism/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 '{
  "fileUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/f/f4/Constitution_of_Maine_2013.pdf",
    "https://upload.wikimedia.org/wikipedia/commons/a/a8/NASA-Apollo8-Dec24-Earthrise.jpg",
    "https://raw.githubusercontent.com/fivethirtyeight/data/master/airline-safety/airline-safety.csv"
  ]
}' |
apify call ataism/file-downloader --silent --output-dataset

```

## MCP server setup

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