Bulk URL Downloader: Download Images, Video, Audio, ZIP · Apify
Bulk File Downloader - URLs, Images, Pages & Datasets avatar

Bulk File Downloader - URLs, Images, Pages & Datasets

Pricing

from $2.00 / 1,000 file downloadeds

Go to Apify Store
Bulk File Downloader - URLs, Images, Pages & Datasets

Bulk File Downloader - URLs, Images, Pages & Datasets

Bulk file downloader, bulk image downloader, URL downloader: download images, PDFs, video and audio in parallel from URLs, pages or Instagram, Facebook, Reddit and Ad Library Datasets (every slide, Reddit video with sound) as files or a ZIP. One manifest row per file. Only new media on repeat runs.

Pricing

from $2.00 / 1,000 file downloadeds

Rating

0.0

(0)

Developer

Vadim Bezrukov

Vadim Bezrukov

Maintained by Community

Actor stats

0

Bookmarked

111

Total users

53

Monthly active users

2 days ago

Last modified

Features

Limited permissions

Share

Paste URLs of files or web pages, pick the file types to take from the pages, and get the files one by one or in a ZIP. A URL downloader for public images, PDFs, documents, video and audio files: up to 500 URLs per run, or up to 10,000 rows from another Actor's Dataset.

  1. Paste URLs into URLs of files or web pages: direct file links, web pages, or both in one list.
  2. Pick file types for the pages: images (the default), PDF, documents, video, audio or archives.
  3. Get the files: one Dataset row per file with status, size, SHA-256 checksum and download URL. Turn on Also pack the files into a ZIP to get files/<name> ZIP parts with JSON and CSV manifests as well. The ZIP adds no charge.
{
"urlsText": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf\nhttps://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
"treatHtmlAs": "auto",
"pageFileTypes": ["image", "pdf"],
"zip": true
}

The PDF link is downloaded as a file. The second URL returns a web page, so the Actor reads it and downloads its images and PDF links.

Already scraped Instagram, Facebook, Reddit or an Ad Library? Point the Actor at that Dataset and it downloads every carousel slide, Reddit gallery image, video (Reddit videos with their sound) and ad creative, names them by account, subreddit and post or ad, packs Ad Library creatives into a swipe-file ZIP and, on repeat runs, only new media. See Instagram and Facebook datasets, Reddit datasets and Ad Library datasets (Meta, TikTok).

Price: $0.002 per stored file plus $0.002 per started 10 MiB, so $0.004 for a file up to 10 MiB, platform usage included, plus $0.00005 per run. 100 such files cost $0.40005; failed URLs, web pages and the ZIP are free. Details and large files: Pricing.

Files are streamed in parallel into Apify storage, and you get one manifest row per file with status, size, SHA-256 checksum and download URL.

Files are saved in the run's Key-Value Store. Open the Dataset to see which downloads succeeded and follow each storage_url to retrieve the file.

Up to five downloads run at once by default, with a separate per-host limit. Total time depends on the file sizes and the servers hosting them.

The per-host limit applies to each redirect destination, including a shared CDN. Repeated HTTP 429/503 responses reduce that host to one active download after existing transfers finish.

Set fileName to choose an output name. Otherwise the Actor uses the response's Content-Disposition filename (filename* takes priority), then the final URL. Names are sanitized for storage; an invalid header falls back to the URL.

See Changelog for release history.

One list for files and web pages

URLs of files or web pages (urlsText, or urls as JSON) takes both. The option When a URL is a web page (treatHtmlAs) decides what an HTML response means:

treatHtmlAsWhat happens
autoA URL that returns an HTML page is read as a web page, and its files of the types in pageFileTypes are downloaded. Every other URL is downloaded as the file itself. The Console form uses this.
fileEvery URL is downloaded as a file; an HTML page is saved as an .html file. An API call without treatHtmlAs uses this, exactly as before the option existed.
pageEvery URL is read as a web page, the same as the pages field below.

In auto, a URL whose path ends in a known file extension (.pdf, .jpg, .mp4, .zip and so on) is downloaded without asking the server first. Any other URL is requested once to learn its type: a file response is closed after its headers and downloaded as a file, and robots.txt is checked before a page is read. Listed files are never filtered by pageFileTypes; the filter applies only to files found on pages. Each listed file keeps its own row, and each page gets a page row followed by the rows of its files.

Download images and files from web pages

Paste product, gallery or article pages into Web pages to download files from, or send them as pageUrlsText or pageUrls:

{
"pageUrlsText": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html\nhttps://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html",
"maxImagesPerPage": 50,
"minImageBytes": 5000
}

Each page is read once over plain HTTP, without a browser. The Actor collects the images in its HTML, downloads each one once and stores it like any other file:

  • img with the largest srcset candidate, and the lazy-load attributes data-src, data-srcset and data-lazy-src (they win over a placeholder src)
  • one image per picture element: its img, or its first source when the img is only a placeholder
  • og:image, twitter:image and link rel="image_src"
  • relative links resolved against <base href> and the page URL; inline data: images are skipped and counted

With pageFileTypes the Actor also takes other files from each page:

TypeFound inConfirmed by
image (default)the image tags abovean image media type or signature
pdflinks and declared typesapplication/pdf or a PDF signature
documentlinks to PDF, Word, Excel, PowerPoint, OpenDocument, RTF, CSV and TXTthe document media type
videovideo and its source tags, video linksvideo/*
audioaudio and its source tags, audio linksaudio/*
archivelinks to ZIP, RAR, 7z, TAR, GZ, BZ2 and XZthe archive media type

A link counts by its file extension or a declared type attribute. Every body is checked by its Content-Type before it is stored: a link that returns something else, such as a "page not found" HTML page, gets NOT_SELECTED_TYPE and is not stored or charged. When the server sends only a generic type such as application/octet-stream, the file name extension decides. maxFilesPerPage (default 50) limits the non-image files per page; images keep their own maxImagesPerPage.

{
"pageUrlsText": "https://www.irs.gov/forms-pubs/about-form-w-9",
"pageFileTypes": ["pdf"],
"maxFilesPerPage": 20
}

The Dataset gets one row per page, followed by one row per file found on it. File rows keep the usual manifest fields and add source_page_url. Open the Pages view for the page rows and Downloaded for the stored files.

Page statusMeaning
PAGE_SCANNEDThe page was read and images were found. page_images counts found, stored, failed and skipped images.
NO_IMAGES_FOUNDThe page was read, but its HTML lists no images. Pages that load images with JavaScript need a browser scraper first; pass its image URLs in urlsText or as a Dataset.
PAGE_FETCH_FAILEDThe page could not be fetched (error_code such as HTTP_404, TIMEOUT, or ROBOTS_UNAVAILABLE when robots.txt could not be read).
ROBOTS_DISALLOWEDThe site's robots.txt does not allow this page. It was not fetched.
NOT_HTMLThe URL returned a file, not an HTML page. Put direct file and image URLs in urlsText.
PAGE_NOT_SCANNEDNot reached in this run: maxPages, the spending limit or the run timeout came first.

page_images explains every image that was not stored: on_earlier_page (the same image was already taken from an earlier page, so it is stored and charged once), over_limit (beyond maxImagesPerPage, default 50), other_host (skipped by sameHostOnly), too_small (below minImageBytes) and data_uris.

With the default pageFileTypes only images are stored. A link that returns an HTML page, for example a login page, gets NOT_AN_IMAGE and is not stored or charged. minImageBytes skips icons and tracking pixels; such an image is fetched to measure it but not stored or charged. sameHostOnly keeps images on the page's own host, which also skips images on a separate CDN host, so leave it off for most shops.

Web page fields take priority over the file URL fields, so the demo URLs are not downloaded alongside your pages. One list holds up to 500 pages, and maxPages (default 100) limits how many are scanned per run. Pages themselves are not charged; each stored image or file is one file-downloaded event.

After the run, RUN_SUMMARY shows the page and image counts. If something did not finish, RETRY_PAGES_INPUT lists the pages to scan again (not scanned, failed, or no image started) and RETRY_INPUT lists unfinished images of pages that already have stored images, with their sourcePageUrl. Start each in a new run. A stored image is never in either list, so it is not charged twice.

Already have image URLs from a product scraper? Use the product image recipe with SKU file names instead: it reads the URLs straight from that scraper's Dataset.

Quick start

For a plain list, paste links into URLs of files or web pages in Console, or send:

{
"urlsText": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf\nhttps://www.rfc-editor.org/rfc/rfc791.txt"
}

Blank lines and surrounding spaces are ignored. A non-empty urlsText takes priority over urls. Leave urlsText blank to use the JSON format with custom names:

{
"urls": [
{
"url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
"fileName": "dummy.pdf"
},
{
"url": "https://www.rfc-editor.org/rfc/rfc791.txt",
"fileName": "rfc791.txt"
}
],
"concurrency": 5,
"maxFileSizeMb": 100,
"timeoutSeconds": 120,
"maxRetries": 2,
"overwrite": false
}

The Console form prefills the same two public demo URLs in the first field, with treatHtmlAs set to auto. Both are direct files, so the demo run is a plain URL list.

Download files from another Actor

Select the upstream Actor's Dataset in Source Dataset, ideally after that Actor finishes. Set urlField to the field containing one file URL per row:

{
"datasetId": "YOUR_DATASET_ID",
"urlField": "image.url",
"metadataFields": ["productId", "sku", "SKU"],
"maxItems": 1000
}

Use the actual 17-character Dataset ID. datasetId overrides the demo URLs, urlsText and urls. Dot paths such as image.url or images.0.url select one value, without expanding arrays. Optional fileNameField selects a custom file name. The Dataset must be accessible to your account; the input requests read-only access.

Each output row preserves source_dataset_id, the absolute source_item_index, and selected identifiers inside metadata, for example {"productId": 123, "SKU": "BLUE-M"}. The defaults preserve productId, sku and SKU. Missing identifiers are omitted. Select up to 20 scalar fields, at most 1024 JSON bytes per value and 4096 bytes together. Invalid values or missing URLs produce explicit failed rows. Unselected source fields are not copied.

The Actor reads pages of 50 rows and freezes the selected range before downloading. maxItems defaults to 1000 and supports up to 10,000 rows per batch. datasetOffset selects the starting row. NEXT_INPUT provides the next range when more rows exist. Finish the current range before starting the next one. Each range is independent, so URL deduplication does not span separate ranges. Apify Datasets only grow, so rows added later never change a range already read. If the upstream Actor is still adding rows, the run downloads the rows it read, sets RUN_SUMMARY.datasetChangedDuringRead and leaves the new rows to NEXT_INPUT. RUN_SUMMARY.datasetTotalIsExact is false when more rows exist; in that case datasetTotalItems is a lower bound, because Apify's total counter can lag writes.

Download the files every time a scraper finishes

Open the scraper or its saved Task in Apify Console, go to Integrations, connect this Actor so it starts when a scraper run succeeds, and use this input:

{
"datasetId": "{{resource.defaultDatasetId}}",
"urlField": "imageUrl",
"metadataFields": ["sku"],
"maxItems": 1000
}

Write the placeholder exactly as shown, in quotes and with no spaces inside the braces. When the scraper run finishes, Apify replaces it with that run's Dataset ID, so every run downloads the files of the new result. Set urlField, metadataFields, fileNameField and maxItems as for any Dataset above.

The integration also adds a payload object to the input. This Actor accepts it and does not read it. If the integration input has no datasetId, the run stops with INVALID_INPUT and says what to set, before any file is downloaded or charged, because only datasetId gives the run read access to that Dataset. Spending limits, NEXT_INPUT and CONTINUE_INPUT work as in any Dataset batch.

Product images with SKU file names

A product scraper often stores one image URL per product row, for example {"sku": "SKU-101", "imageUrl": "https://...", "active": true}. This input downloads one file per row and keeps the SKU in each manifest row, so every file can be matched to its product even when image names repeat:

{
"datasetId": "YOUR_DATASET_ID",
"urlField": "imageUrl",
"metadataFields": ["sku"],
"maxItems": 1000
}

Repeated URLs in one batch already share one file and one file event, and rows without a URL are not charged. To drop products you do not need, name files after the SKU or remove duplicates across several batches, run Dataset Cleaner first with its "Prepare product images for a bulk download" recipe. Then pass the Cleaner run's Dataset ID here and add "fileNameField": "imageName". The cleaning step has its own price; skip it when every row already holds one URL you want.

To receive the images as ZIP archives with one folder per SKU, add export to the same input, see Product images in a ZIP.

A row with a list such as images holds several URLs, and images.0.url downloads only the first one. To download all of them, split the list into one row per image before this Actor, for example in an n8n Code node, and keep the SKU and image position on each row. If you need this built in, say so in an Issue.

Continue after an interruption or spending limit

After the run stops, open its Key-Value Store and copy CONTINUE_INPUT into a new run's JSON input. It contains resumeFromStoreId and resumeStoreIds. Copy both fields: the latter grants read-only access to earlier files in a continuation chain. The new run inherits the frozen source, field mapping and download settings, rebuilds the complete manifest for that range, and downloads only unfinished files. Confirmed files are reused without another file event; reused: true and file_store_id identify those rows. Repeated URLs with different product IDs retain separate output rows but share one file and one file event within the batch.

Keep all previous run storages until you no longer need their files. Reused storage_url values still point to the original storage. Do not start concurrent continuations or branch repeatedly from an older checkpoint: use the most recent stopped run. Up to 20 continuation generations are supported. Each new run still has the normal start event. A file already stored and charged is not charged again. A transfer that is still running when the spending limit is reached is not stored and is not charged.

If a charge acknowledgement is uncertain, continuation stops for reconciliation; it never guesses that charging again is safe. A missing previously stored file also stops continuation. Use a fresh batch to change field mappings or source values. If interruption happened before CONTINUE_INPUT was saved, no file downloads or file charges have started, so start the original input again. Use a new run for continuation.

If Apify restarts or migrates a running run, the run continues on its own in both URL-list and Dataset mode. Files already stored and charged are reused without another file event, rows already in the Dataset are not added twice, and RUN_SUMMARY.restartsRecovered records the restart. A charge that was in progress at the restart is checked against Apify's charge record; if Apify does not confirm it within a minute, the run stops with BILLING_UNCERTAIN for review instead of charging again.

Large batches and the run timeout

Shortly before the run timeout, the Actor stops starting downloads and cuts transfers that are still running. Files already stored finish their charge, and every remaining row is published with RUN_TIME_LIMIT, so the run ends SUCCEEDED with RUN_SUMMARY.status PARTIAL (or RUN_TIME_LIMIT when no file finished) and runTimeLimitRows. It keeps 45 seconds plus half a second per 50 rows for this, at most 40% of the remaining time. Start CONTINUE_INPUT (Dataset batches) or RETRY_INPUT (URL lists) in a new run, or raise the run timeout. A run without a timeout is not stopped early.

Checkpoints contain selected source identifiers and URLs, including signed query parameters. Treat them like the input and keep them private. This integration reduces workflow setup; it does not guarantee higher throughput or sales.

Instagram and Facebook datasets

Social scrapers return one row per post, and a carousel post holds several photos and videos. Their links expire within about 31 hours, so download them right after the scrape. Select the scraper's Dataset and set Social, Reddit or Ad Library Dataset (sourcePreset):

{
"datasetId": "YOUR_DATASET_ID",
"sourcePreset": "auto",
"mediaTypes": ["image", "video"],
"zip": true
}
  • Every slide of a carousel and every video is one file and one Dataset row, with source_item_index (the post row) and media_index (its position).
  • Files are named {shortCode}_{n}.jpg or .mp4; the ZIP has one folder per account and manifest.csv with the post URL, caption, date and owner.
  • auto recognizes the output of Instagram post, Reel and profile scrapers and of Facebook post and photo scrapers; instagram or facebook limits a run to one. A row it does not recognize gets UNKNOWN_SCHEMA with its keys. Video covers, previews and profile pictures are off by default; add cover, preview or avatar to mediaTypes to get them.
  • A link that already expired is not fetched or charged (LINK_EXPIRED), and RESCRAPE_INPUT holds the scraper input that refreshes those posts.

Run it after every scrape: in the scraper's Integrations tab add this Actor, set "datasetId": "{{resource.defaultDatasetId}}" and the same skipDownloadedStoreName, for example "my-brand-media". Media an earlier run stored get SKIPPED with ALREADY_DOWNLOADED and are not charged, so a daily schedule downloads only new posts.

Cost: 100 carousels of 4 photos are 400 files, $1.60 ($0.004 per file up to 10 MiB, see Pricing). Skipped and expired media cost nothing. Stored files stay in your Apify storage as long as your plan keeps run storage; keep the ZIP or the files elsewhere if you need them longer.

Reddit datasets: videos with sound, galleries and images

Reddit scrapers return one row per post. A gallery post holds several images, and a Reddit video is stored as separate picture and sound tracks, so the video link in a scraper's output is usually silent, a v.redd.it page or a signed link that expires within hours. The reddit preset reads the output of the common Reddit scrapers (trudax, harshmaur, fatihtahta and coding-doctor-omar, and any Dataset with the same fields) and downloads the original files:

{
"datasetId": "YOUR_DATASET_ID",
"sourcePreset": "reddit",
"zip": true
}
  • A video is saved as one MP4 with its sound: the Actor reads the video's playlist on v.redd.it, takes the best picture track ("videoQuality": "sd" takes at most 480p) and the best sound track, and joins them without re-encoding. audio_status says MUXED; a video that has no sound says NO_AUDIO_TRACK, and one whose sound could not be fetched or joined is saved without it as AUDIO_FAILED. Neither fails the row.
  • Every image of a gallery is one file, in gallery order (media_index). Preview links become their i.redd.it originals, so an image the scraper lists twice is downloaded once.
  • Files are named {postId}_{n}.jpg or .mp4, and the ZIP has one folder per subreddit: aww/1abc23_1.mp4. manifest.csv holds the post URL, title and date. Links to other sites (articles, YouTube) are not downloaded; video posters and thumbnails are preview media, off by default.
  • auto also recognizes Reddit rows. Text posts get SKIPPED with NO_MEDIA. Signed Reddit links do not limit when you run: videos are read from v.redd.it by their ID.

Cost: a Reddit video counts as one file, like any other: $0.004 up to 10 MiB, $0.006 up to 20 MiB. 100 posts with 60 images and 40 short videos cost $0.40 plus the size units of videos over 10 MiB.

Ad Library datasets (Meta, TikTok)

Ad Library scrapers return one row per ad, and one ad can hold several images, videos and cards. The meta-ads preset reads both the camelCase and the snake_case output of Meta Ad Library scrapers; tiktok-ads reads TikTok Creative Center ads.

{
"datasetId": "YOUR_DATASET_ID",
"sourcePreset": "meta-ads",
"zip": true
}

The ZIP is a swipe file: one folder per advertiser, files named {adId}_{n}.jpg or .mp4, manifest.csv with ad text, title, call to action, landing URL, start date and Ad Library link, and index.html, a page of every creative in the part with its text. It opens offline and loads nothing from the web. HD video is the default; "videoQuality": "sd" takes SD links. Ad media links expire after 6 to 48 hours, so chain this Actor to the scraper the same way as above.

Cost: 100 Meta ads with 60 videos and 40 images are 160 files, $0.64 at $0.004 for files up to 10 MiB.

For other Datasets, urlFields reads several paths per row, and any path takes [*] for every element of an array, for example images[*] and videos[*].url. maxFilesPerRow (default 20) caps the files per row; one batch holds at most 10,000 files.

Example workflows

Choose a batch you already have permission to download:

  1. Public PDF documents: replace the first demo URL with a document URL and set fileName to a recognizable name. Inspect its SUCCESS row before expanding the batch.
  2. Mixed files: use the PDF and text URLs in the quick start above. The Dataset manifest keeps each input's status, checksum, size and storage URL, including explicit failures.
  3. Recurring document batches: pass the next list of document URLs from your workflow and use sha256 downstream to compare file contents. Each run stores its own files; the Actor does not maintain a change-monitor baseline.

Download the files from your result

Open the run's Dataset and select a row with status == "SUCCESS". Its storage_url points to that file in the run's Key-Value Store; open it using your authorized Apify access, or pass it to the next step of your automation. Keep the manifest if you need to match downloaded files back to their input URLs. Failed rows do not represent downloadable files. Files remain subject to your Apify storage retention settings. To get them as ZIP archives with one folder per SKU, see Product images in a ZIP.

Retry unsuccessful files

After a completed batch, read RUN_SUMMARY. If retryStatus is AVAILABLE, open the Retry input output link or the RETRY_INPUT record in the run's Key-Value Store. Copy its complete JSON into a new run. It contains only failed URLs, deduplicated, with original explicit file names and the same download settings. Its blank urlsText prevents an old pasted list from overriding it. Successful files are excluded. No second run starts automatically.

Review the Failed Dataset view first: expired URLs, missing files, security rejections and size limits may need correction. Set the new run's spending limit, memory and timeout separately; these platform run options are not part of Actor input. A later successful download is billed normally. Files from the first run remain in that run's storage; the new run contains only retry results.

retryStatus: NOT_NEEDED means there is no retry input. If billing has an unknown outcome, retryStatus: BLOCKED_BILLING_UNCERTAIN suppresses the entire retry input until charges are reconciled. RETRY_INPUT is absent in both cases and is not promised for runs interrupted before the batch result is saved. Like INPUT and Dataset provenance, it retains exact URLs, including signed query parameters: keep it private.

Every file in a ZIP

Set "zip": true (Also pack the files into a ZIP) with any source: pasted URLs, web pages or a Dataset. The run stores the files as usual and adds ZIP parts to its Key-Value Store:

  • files under files/<name>, named like the stored file. Two different files with the same name become report.pdf and report-2.pdf; a URL listed twice is stored and archived once.
  • manifest.json and manifest.csv in every part, one row per file.
  • parts of at most 256 MiB, named archive-...zip. RUN_SUMMARY.exportArtifacts lists them, and EXPORT_REPORT.html explains what was not archived and why.

Each Dataset row gets export_status and archive_path. Page rows stay out of the ZIP. RETRY_INPUT and RETRY_PAGES_INPUT keep the ZIP option. The ZIP adds no charge. To change the part size and limits, use the export object with "mode": "zip" instead, for example {"mode": "zip", "maxArchiveSizeMb": 100}. With a Dataset, the export object packs product images by SKU, as described below; "zip": true alone uses the flat files/<name> layout for a Dataset too.

Product images in a ZIP, one folder per SKU

Turn a product Dataset into ZIP archives with one folder per SKU, images in order and JSON/CSV manifests, ready for a catalog migration, a marketplace upload or a hand-off to a supplier. The ZIP adds no charge: you pay for the stored files exactly as in any Dataset batch.

SKU folders need a Dataset (datasetId) or its continuation (resumeFromStoreId), because only Dataset rows carry product fields. With pasted URLs or web pages, the export object produces the flat files/<name> ZIP described above. Runs without export or zip work exactly as before.

For a Dataset with sku, imageUrl and an optional zero-based imageIndex:

{
"datasetId": "YOUR_DATASET_ID",
"urlField": "imageUrl",
"maxItems": 1000,
"export": {
"mode": "zip",
"skuField": "sku",
"imageIndexField": "imageIndex"
}
}

Images appear as SHOE-001/001.jpg, SHOE-001/002.jpg and so on. Each ZIP part contains manifest.json and manifest.csv for its source rows, including failures; combine the manifests of all parts for the complete batch. RUN_SUMMARY.exportArtifacts lists the parts with their storage keys, sizes and checksums. The archive is built on disk, so a large export does not need a large amount of memory.

SKU folders and image names

skuField, productIdField, imageIndexField and imageRoleField use the same dot-path rules as urlField. Defaults are sku, productId, imageIndex and imageType. A SKU must be a nonblank string of at most 512 UTF-8 bytes. Product ID is optional, a string or integer of at most 256 characters. Image index is optional, an integer from 0 to 999999999. Role, when supplied, is main, gallery or detail.

Missing image indices use the order of the rows in the batch. Repeated indices get a suffix with the absolute source row number. Unsafe, reserved or case-colliding SKUs get a folder derived from their SHA-256, and the original SKU stays in the manifest. Names stay the same through retries and continuations of the same batch.

A URL used by several SKUs is downloaded and charged once and appears in every SKU folder. Image bytes are not changed. The file extension comes from the image signature, so a PNG served as image/jpeg is still saved as .png. Supported formats: JPEG, PNG, GIF, WebP, TIFF, BMP and AVIF. Other formats, including SVG and HEIC, get UNSUPPORTED_IMAGE_FORMAT in the manifest; the downloaded file stays in storage and is charged like any stored file. This is format identification, not image decoding or a quality check.

Export limits, report and recovery

InputDefaultRange and behavior
maxArchiveSizeMb2561-4096 MiB per ZIP part, including manifests and ZIP overhead.
maxTempDiskMb5122-8192 MiB of temporary disk: at most half for the archive, half for the source files.
maxExportSizeMb102401-102400 MiB of image bytes per batch, counting each SKU copy.
maxExportItems100001-10000 SKU images per batch.

A ZIP part holds at most one 50-row page of the batch and splits further by size. An image too large for one part gets ARCHIVE_ITEM_TOO_LARGE; it is not truncated. Rows over an export limit stay in the manifest with BUDGET_EXCEEDED. These limits apply after downloading, which keeps its own maxItems, maxFileSizeMb and spending limit.

Download status and error_code keep their meaning. Export adds export_status and export_error_code: SUCCESS, INVALID_INPUT, SOURCE_FAILED, STORAGE_FAILED, EXPORT_FAILED or BUDGET_EXCEEDED. An invalid SKU never turns a stored file into a failed download, and the file charge stays as it was.

RUN_SUMMARY adds the source rows, unique files, repeated SKU images, failed downloads, exported and failed images, the ZIP parts and exportBytesWritten / exportObjectsWritten / exportObjectsReused for this run. Each finished export run also writes EXPORT_REPORT.html to its Key-Value Store: the outcome, counts, ZIP parts, the first 200 rows that need attention with a suggested fix, and links to the Dataset and CONTINUE_INPUT. The report has no scripts, external resources or public links. RUN_SUMMARY.exportReportStatus is WRITTEN, FAILED, or SKIPPED when too little time was left before the run timeout. A run stopped by the platform, for example at its timeout, has no report, so read RUN_SUMMARY first.

Shortly before the run timeout the Actor stops on its own, so a long export still ends SUCCEEDED with RUN_SUMMARY.status RUN_TIME_LIMIT (or PARTIAL when some images were exported). To finish it, copy CONTINUE_INPUT from the latest stopped run into a new run and keep the earlier run storages. Finished ZIP parts are reused, a missing part is rebuilt from the stored files, and no stored file is downloaded or charged again. A missing paid file stops the continuation instead of being downloaded and charged a second time. To fix source rows, such as a blank SKU, export the corrected rows as a new batch: CONTINUE_INPUT replays the original batch. Run one continuation at a time.

Product images to your Amazon S3 bucket

The same Dataset export can upload to your own bucket instead of a ZIP. Each image becomes a private object at <prefix><SKU>/<order>.<ext> (media and URL fields: <prefix>files/<name> or one folder per account), and every 50-row page adds manifest.json and manifest.csv under <prefix>_manifests/.

{
"datasetId": "YOUR_DATASET_ID",
"urlField": "imageUrl",
"export": {
"mode": "s3",
"bucket": "my-shop-images",
"region": "eu-central-1",
"prefix": "catalog/2026-11/"
},
"s3Credentials": {
"accessKeyId": "YOUR_ACCESS_KEY_ID",
"secretAccessKey": "YOUR_SECRET_ACCESS_KEY"
}
}
  • s3Credentials is an encrypted secret input. It is never written to the checkpoint, RUN_SUMMARY, the report or CONTINUE_INPUT, so add it again when you start CONTINUE_INPUT. Use a key limited to the bucket: s3:ListBucket on the bucket and s3:PutObject, s3:GetObject and s3:AbortMultipartUpload on the prefix.
  • Every upload is checked against its SHA-256 checksum before the row gets export_status SUCCESS and s3_upload_status UPLOADED. An identical object already in the bucket is reused (REUSED) and not uploaded or charged again. With overwritePolicy skip (default) or fail, a different object under the same key is left untouched and the row gets S3_KEY_CONFLICT; replace overwrites it. Objects get no public ACL.
  • Files over 8 MiB use multipart upload. An interrupted upload is aborted, at the latest by the next CONTINUE_INPUT run, so no incomplete object stays in the bucket.
  • uploadConcurrency (1-5, default 2) sets parallel uploads, also bounded by maxTempDiskMb. The export limits above apply as well.

S3 export charges s3-uploaded-10mib: each started 10 MiB that the run uploads, counted over the whole run, not per file. 1,000 product images of 500 KiB are about 488 MiB, which is 49 events or $0.196 on top of the download events. Reused objects, failed uploads, manifests and continuation runs that find the objects already uploaded are not charged. When the spending limit cannot cover the next upload, the row gets BUDGET_EXCEEDED before anything is uploaded; raise the limit and start CONTINUE_INPUT.

  • Clean the Dataset before downloading: Dataset Cleaner filters rows, builds file names from other fields and removes duplicates across a whole Dataset. Pass its output Dataset ID as datasetId.
  • Get attachment URLs from a paginated API: Paginated REST API Fetcher exports API items across a configured pagination pattern. Select permitted public file URLs from the item data and map them into this Actor's urls input.
  • Monitor a PDF across revisions: PDF Change Monitor keeps a last-good baseline for a direct PDF or a PDF selected from a stable webpage. Choose it when you need content and link change events, rather than a fresh download manifest.
  • Convert the video and audio you downloaded: Bulk FFmpeg Video Converter makes MP4, MP3, GIFs, trimmed clips and thumbnails from the same public URLs or Dataset, with a status per file.
  • Read the text of your PDFs: PDF to Text Extractor extracts per-page text from the same public PDF URLs or Dataset, with a SHA-256 checksum and a status per file.
  • Read HTML as Markdown: Website Content Crawler extracts authorized website text and optional chunks for AI and RAG. Give it the original HTML page URL and rights declaration; it does not parse the downloaded binary files.

Each linked tool has separate input and pricing. Select the URLs and start the appropriate run yourself or through your own automation.

Output example

Successful row:

{
"source": "parallel-bulk-file-downloader",
"source_id": "…",
"status": "SUCCESS",
"source_url": "https://example.com/files/report.pdf",
"final_url": "https://cdn.example.com/report.pdf",
"source_index": 0,
"file_name": "report.pdf",
"content_type": "application/pdf",
"bytes": 4123456,
"sha256": "…",
"kvs_key": "files-000000-report.pdf",
"storage_url": "https://api.apify.com/v2/key-value-stores/…/records/files-000000-report.pdf",
"http_status": 200,
"attempts": 1,
"duration_ms": 842,
"error_code": null,
"error_message": null,
"downloaded_at": "2026-09-17T21:00:00Z",
"scraped_at": "2026-09-17T21:00:00Z",
"fingerprint": "…",
"schema_version": 2
}

Dataset views: All, Downloaded, Failed.

Automation / API use

curl -X POST "https://api.apify.com/v2/acts/automa-flow~parallel-bulk-file-downloader/runs" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urls":[{"url":"https://www.rfc-editor.org/rfc/rfc791.txt","fileName":"rfc791.txt"}],"concurrency":5}'

Schedule a recurring URL batch or call the Actor from n8n / Make / your backend. A second run is useful whenever you have a new URL list, a refreshed CDN export, or need checksums again for integrity checks.

AI agents / MCP

Direct tool URL (authenticated; each caller uses their own Apify OAuth or token):

https://mcp.apify.com?tools=automa-flow/parallel-bulk-file-downloader

Anonymous MCP discovery (search-actors) indexes this public Store Actor. The direct tool URL above is the authenticated path for initialize, tools/list and a paid run. Direct agentic payment needs pay-per-event prices with platform usage included, limited permissions and no Standby mode. This Actor meets those product rules once the included-usage price below is the active Store price. Developer identity verification is a separate Apify requirement and is not claimed here.

When an agent should call it: the user already has a list of public HTTP/HTTPS file URLs, or a list of web pages whose images they need, and needs the files in Apify storage plus a per-file manifest (status, storage_url, SHA-256) for a downstream step (CRM attachment sync, product image import, integrity re-check, report pack).

When not to call it: crawling a whole site (only the listed pages are read), galleries that need JavaScript to show their images, HTML text extraction, authenticated Drive/Dropbox, video-site downloaders, or any private/localhost URL (SSRF policy rejects those per item).

The event price is the whole bill. Platform usage is included. It is not added on top.

ExampleEvent charges at 1 GiB
Two stored files up to 10 MiB$0.00805 ($0.00005 start + 2 × ($0.002 + $0.002))
100 stored files or images up to 10 MiB$0.40005
One 256 MiB file, the maximum size$0.05405

Always set maxTotalChargeUsd (Store minimum $0.00405, enough for one file of up to 10 MiB). A larger file needs room for its other 10 MiB blocks as well. The cap limits event charges. At the cap, already stored files remain. A file that would not fit is not stored and is not charged. Remaining URLs get BUDGET_EXCEEDED or cancelled rows, and RETRY_INPUT lists them for a new run with a higher limit.

Example agent ask: “Download these public PDF URLs in parallel with maxTotalChargeUsd=0.05, then read RUN_SUMMARY and return Dataset rows with storage_url and sha256 for SUCCESS items only.”

After the run, read Key-Value Store RUN_SUMMARY first (status, successful / failed, chargedEventCounts, nextAction), then the default Dataset. Output schema also exposes runSummary, billingReceipt and conditional retryInput links.

Failure semantics

error_codeMeaning
SECURITY_REJECTEDURL/host/IP failed SSRF policy (localhost, private, metadata, bad scheme).
HTTP_404 / HTTP_403 / …Remote server returned that status.
HTTP_ERRORUnusable HTTP response, including partial HTTP 206 or HTTP 204/205 without a file.
RATE_LIMITEDHTTP 429 after retry budget.
TIMEOUTPer-attempt source-download deadline exhausted, including DNS, redirects and body transfer.
FILE_TOO_LARGEContent-Length or streamed bytes exceeded maxFileSizeMb.
STORAGE_FAILEDKey-Value Store write failed after a successful download.
BUDGET_EXCEEDEDSpending limit stopped storing/charging further successful files. If the limit cannot cover even one file, every URL gets this code, the run succeeds with RUN_SUMMARY.status: BUDGET_EXCEEDED and RETRY_INPUT holds all URLs.
BILLING_UNCERTAINCharge API failed after a store write; the run fails closed (no replay).
RUN_TIME_LIMITThe run timeout arrived before this file finished. Nothing was stored or charged for it; continue in a new run.
NOT_AN_IMAGEWeb page mode: the image link returned something else, such as an HTML page. Not stored or charged.
NOT_SELECTED_TYPEWeb page mode with other pageFileTypes: the link returned a type you did not select, such as an HTML page for a .pdf link. Not stored or charged.
ROBOTS_DISALLOWED / ROBOTS_UNAVAILABLEWeb page mode: robots.txt does not allow the page, or could not be read. The page was not fetched.
MAX_PAGES_REACHEDWeb page mode: the page is beyond maxPages; RETRY_PAGES_INPUT lists it for the next run.
NO_VIDEO_TRACKreddit preset: the Reddit video's playlist lists no video track, for example a removed video. Not stored or charged.
INTERNAL_ERRORThe Actor itself failed on this one file or page; error_message names the error type. Not charged. The other files continue; retry the row, and open an Issue with the run ID if it repeats.

NO_RESULTS is not used: when every URL and page input is empty, runtime validation returns INVALID_INPUT and the run fails without downloading files. In web page mode, pages without images end as NO_IMAGES_FOUND with their page rows. When every page failed with a network, server or robots.txt error, the run ends COMPLETED_WITH_FAILURES, like a list of failed file URLs: each page row keeps its error and nothing is downloaded or charged. Malformed text lists and field bounds are rejected by the platform before a run starts. An ordinary item failure never cancels independent downloads, and a run in which every file failed for its own reason (a dead link, a wrong file type, a malformed URL) still succeeds as COMPLETED_WITH_FAILURES with one row per file. The run itself fails as SOURCE_FAILED only when nothing was stored and the Actor's own storage or code failed (STORAGE_FAILED, INTERNAL_ERROR). A BILLING_UNCERTAIN outcome stops the batch, cancels and drains sibling workers, deletes files known to be uncharged, and retains the uncertain file plus its Dataset row for reconciliation.

timeoutSeconds excludes queue time, retry backoff and storage uploads. Slow sources may need a higher value. A valid empty file returned with HTTP 200 is accepted; partial downloads and no-content responses are not complete files.

Pricing

Pay per event. Platform usage (compute, transfer and storage) is included in these prices. There is no separate platform usage charge.

EventPrice
apify-actor-start$0.00005 per run at 1 GiB. Apify charges one event per GiB of memory.
file-downloaded$0.002 per successfully stored unique file or image.
stored-10mib$0.002 for each started 10 MiB of a stored file, at least one per file.
s3-uploaded-10mibS3 export only: $0.004 for each started 10 MiB the run uploads to your S3 bucket, counted over the whole run.

Every stored file pays file-downloaded once and one stored-10mib for each started 10 MiB, so 1,000 typical files up to 10 MiB cost $4.00.

Stored fileEvent charges
10 MiB or smaller$0.004
25 MiB$0.008
100 MiB$0.022
256 MiB, the maximum size$0.054

Not charged: failed URLs, retries, redirects, security rejections, oversized responses, web pages, images that are not images or are below minImageBytes, or duplicate copies of a URL or image that was already stored in the same run.

s3-uploaded-10mib is charged only when you choose "mode": "s3", and only for objects the run actually uploaded (see Product images to your Amazon S3 bucket). Downloads and ZIP archives never pay it. A ZIP export adds no event of its own.

Security and SSRF protection

  • Only http and https on ports 80/443.
  • Localhost, loopback, RFC1918, link-local, multicast, reserved, IPv6 ULA and cloud metadata targets are rejected.
  • Every redirect is re-validated (scheme + DNS + address policy), max 5 hops. HTTPS to HTTP downgrades are rejected.
  • TCP connects use DNS pinning so validation cannot silently reconnect to a different address (DNS rebinding defense).
  • Filenames are sanitized; remote path segments are never used as local paths.
  • Query strings may contain signed URL parameters; logs redact query values. Dataset rows intentionally retain the exact source_url and final_url for retry/provenance. Keep the Dataset private and avoid signed URLs if retaining those values would expose credentials.
  • Key-Value Store uploads are serialized because the Apify client buffers each record body; concurrency is also capped by run memory versus maxFileSizeMb.

Responsible use and retention

Download only pages and files you are authorized to copy and re-host. The Actor does not bypass authentication, paywalls, robots controls, or access restrictions, and robots.txt is respected for every web page it reads. A public URL does not by itself grant copyright or redistribution rights; product photos and gallery images usually belong to their creators. Inputs, Dataset rows, and generated storage URLs can contain personal, confidential, or regulated data. Keep run storages private, apply the shortest practical retention period, delete unneeded Key-Value Stores/Datasets, and do not use the Actor for prohibited or illegal content.

Limits

  • 1-500 URLs from urlsText or urls, up to 10,000 Dataset rows per batch, or up to 500 web pages per list with maxPages (default 100, at most 500) scanned per run, maxImagesPerPage (default 50, at most 100) and maxFilesPerPage (default 50, at most 100). Provide a URL, page or Dataset source, or resumeFromStoreId; this is checked at runtime.
  • Concurrency 1–20 (default 5).
  • Exact duplicate URLs are downloaded, stored, and billed once; duplicate rows share the first occurrence's storage key and fileName.
  • Max file size 1–256 MiB (default 100). Runs require at least 1 GiB memory because the Apify KVS client buffers one serialized upload body.
  • No browser, FTP, Drive/Dropbox auth, cookies or HTTP byte-range resume in this version. Web page mode reads the HTML of each listed page only; it does not crawl further pages or run JavaScript. Links to video or audio platforms are pages, not files: only direct media file URLs are downloaded.