Bulk Image and File Downloader (ZIP + SHA-256)
Pricing
from $4.00 / 1,000 saved files
Bulk Image and File Downloader (ZIP + SHA-256)
Download public image, PDF and file URLs into one ZIP with SHA-256 checksums and a per-file status manifest. $0.004 per saved file; failed downloads are free.
Pricing
from $4.00 / 1,000 saved files
Rating
0.0
(0)
Developer
SteadyScrape
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
an hour ago
Last modified
Categories
Features
Share
What does Bulk Image and File Downloader do?
Bulk Image and File Downloader fetches a short list of public direct file URLs and packs the files that actually downloaded into one ZIP. Each URL gets a status row with the byte count, detected type, SHA-256 checksum, and a sanitized source URL. The same manifest is stored as JSON and inside the ZIP.
It is built for people who already have a list of image, PDF, or data-file links: catalog and e-commerce teams, researchers, QA and compliance staff, and developers who need a checksummed file pack from an API call. Price: $0.004 per file saved in the ZIP plus $0.003 once per run that saves at least one file. Rejected, failed, and partial downloads are free, and a run that saves nothing costs nothing.
The Actor does not crawl, search, follow links, render pages, or log in. It does not read cookies, tokens, or custom headers. It does not bypass DRM or paywalls, unpack archives, or execute anything it downloads. Public access is not a copyright license. You need the right to fetch and reuse each file. This Actor does not scan for malware and does not clear copyright.
SteadyScrape publishes a separate Actor for turning a page into JSON: AI Page to JSON. That is a different job. This downloader only retrieves the file URLs you supply.
Why use Bulk Image and File Downloader?
Use it when you already have the file addresses and you want one bounded, checksummed pack:
- Pull public product images you are licensed to reuse into a ZIP for a catalog import.
- Collect public PDF spec sheets and CSV or JSON data files for an offline review.
- Save public Office files and zip, gzip, 7z, or rar archives as-is, without opening them.
- Hand a teammate a manifest that shows which URLs failed, instead of a folder of unnamed partial files.
- Keep a dated, hashed copy of public files (terms pages, price lists, published reports) so you can later prove what the file contained.
- Feed image URLs found by another Actor, such as Google Images results, into one ZIP from a workflow or an AI agent.
There is no proxy setting, no browser, and no scheduler inside this Actor.
What does the downloader return?
The dataset has one row per processed URL. The default key-value store has three fixed keys: OUTPUT (counts only), MANIFEST (the full JSON manifest), and PACK (the ZIP, when it fits). File bytes are not copied into the key-value store a second time. Keys are not built from the source URLs.
| Field | Meaning |
|---|---|
status | Explicit outcome, such as saved, saved_type_mismatch, or rejected_type_mismatch |
saved | Whether the file is in the ZIP |
partial | Whether bytes were counted even though the file was not saved |
sourceUrl | Sanitized URL, or null when the URL contained credential material |
detectedMediaType | Type read from the bytes, not from the filename alone |
byteLength | Decoded bytes read, including partial failures |
sha256 | SHA-256 of those decoded bytes |
zipEntryName | Sanitized name inside the ZIP |
httpStatus | Last HTTP status, when one was received |
A row is not labeled as a successful image when the body is an HTML challenge. That case is rejected_type_mismatch and the file is left out of the ZIP.
How it compares
| Approach | What you get |
|---|---|
| Saving files one by one in a browser | No checksums, no record of which links failed, and HTML error pages saved as if they were images |
A quick wget or curl script | Fast, but you have to handle redirects to private addresses, fake image bodies, size caps, and file names yourself |
| This Actor | One ZIP, a SHA-256 per file, an explicit status per URL, private-network and type checks, hard caps, and charges only for files that were saved |
How to download public files
- Open the input and paste up to 20 direct
httporhttpsfile URLs. Use port 80 for http and port 443 for https. - Lower a cap if you want a smaller run. You cannot raise the caps.
- Run the Actor.
- Read the dataset for per-URL status. Download the
PACKrecord for the ZIP andMANIFESTfor the JSON copy.
Locally, from this project:
npm cinpm run buildapify run
Put the input at storage/key_value_stores/default/INPUT.json before apify run. npm start is only the container entry point. Use apify run for a local run so storage is set up the same way. The offline fixture runner needs the Apify CLI on PATH, or the executable at $HOME/.local/bin/apify. Those checks used CLI 1.11.0. apify info still requires a login; schema checks and local runs do not.
How much does the bulk file downloader cost?
This Actor uses pay-per-event pricing:
| Event | Price | When it is charged |
|---|---|---|
saved-file | $0.004 | Once per file stored in the ZIP |
run-with-saved-file | $0.003 | Once per run, only when at least one file was saved |
With pay-per-event pricing these events are the whole Actor price; there is no separate start fee and no compute charge on top.
Worked examples:
- 1 saved file: $0.004 + $0.003 = $0.007.
- A full run of 20 saved files: 20 × $0.004 + $0.003 = $0.083.
- 100 files split over 5 runs of 20: 5 × $0.083 = $0.415.
- 20 URLs where 15 save and 5 fail: 15 × $0.004 + $0.003 = $0.063. The 5 failures are free.
Rejected, failed, and partial URLs are not charged. A run that saves nothing is not charged. There is no per-byte event. If the run's maximum charge cannot cover another saved file together with the run event, that file is left out of the ZIP and is not charged (skipped_charge_limit). A ZIP that fails to store is not charged. A restarted or resurrected process stops before new downloads and charges. It does not replace the ZIP or the manifest. If the earlier OUTPUT can be read, its delivery fields stay and finishedReason becomes stopped_uncertain_prior_run, with the earlier reason kept as priorFinishedReason. This is not an exactly-once resume. Start a new run to download again.
The dataset, manifest, and logs do not carry prices.
Input
See the input tab for the fields. urls is required. Every numeric cap has a hard maximum and a default equal to that maximum.
There is no field for headers, cookies, proxy, a DNS resolver, localhost, or a fixture path. Unknown fields are ignored. A URL with userinfo, a nonempty credential query value (token, signature, api_key, password, and similar names), or a fragment that carries token material is rejected and is not written to the dataset, manifest, or log.
The prefilled example downloads three small public files (a W3C PNG icon, a W3C test PDF, and RFC 2606 as plain text), so a first run shows a real ZIP and manifest.
Output
You can download the dataset as JSON, CSV, or the other formats Apify provides for a dataset. The ZIP is the PACK key. The example below is a fixture shape, not a live download:
{"index": 0,"status": "saved","saved": true,"partial": false,"sourceUrl": "https://cdn.example/files/report.pdf","detectedMediaType": "pdf","byteLength": 1024,"sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae","zipEntryName": "report.pdf","httpStatus": 200}
OUTPUT uses billingStrategy pay-per-event for the events this Actor is built to emit, and platformPricing active or inactive for whether this run's platform pricing is on. inactive means those events are not being billed. Neither field contains a price, a charge ceiling, or account usage. The run log is the normal run log. It is not a private owner channel. Byte and elapsed counts are not written to that log, the dataset, or the key-value store.
Downloaded files are not rendered in the dataset. The ZIP entry is application/zip. HTML, SVG, and JavaScript are rejected rather than previewed.
Limits and security
Defaults are the hard maximums. Input can lower them and cannot raise them.
| Cap | Maximum |
|---|---|
| URLs processed | 20 |
| Decoded bytes per file | 10 MB (10485760) |
| Decoded plus partial bytes per run | 25 MB (26214400) |
| ZIP size | 32 MB (33554432) |
| Concurrency | 2 |
| Runtime | 120 seconds |
| Redirects | 5 |
| Connect and header timeout | 10 seconds |
| Read idle timeout | 10 seconds |
Content-Length is not trusted and is not used to allocate memory. Chunked responses, missing lengths, and short or huge declared lengths are judged by the bytes actually read. One socket read can cross a cap by a single chunk (the stream high-water mark is 16 KB). The socket is then destroyed and those bytes stay on the run budget.
HTTP Content-Encoding of gzip, deflate, or br is decoded, and the decoded size counts toward the caps. Any other encoding, including two encodings at once, is rejected. A .gz or .zip file is stored whole. It is not extracted, so a compressed archive cannot expand inside the Actor. The 25 MB cap still applies to the bytes of that archive.
Only public http and https are allowed. Each target and every redirect is resolved. If any address is private, loopback, link-local, CGNAT, documentation, multicast, reserved, or otherwise not public unicast, the URL is rejected. Mixed public and private answers are rejected. IPv4-mapped IPv6 is unwrapped and checked. NAT64, 6to4, and Teredo addresses are rejected. Decimal, hex, octal, and other literal forms are normalized before the check. The connection uses the approved IP only, with the original hostname kept for the Host header and for TLS SNI. Certificate verification stays on. Proxy variables and user headers are not forwarded. There is no localhost switch.
Names inside the ZIP are sanitized. .., slashes, drive prefixes, NUL and control characters, and Windows device names cannot become entry paths. Content-Disposition is only a name hint. Collisions get a numeric suffix. manifest.json is reserved.
On a deadline, abort, or cap, open streams and temp files are closed and the scratch directory is removed. Rows that already finished keep their status. Bytes from a failed URL are not given back to the budget. A connection is retried once only when the failure is a connect failure and no bytes were received.
File types, archives, and rights
Saved types are JPEG, PNG, GIF, WebP, BMP, TIFF, PDF, plain text, CSV, JSON, ZIP, gzip, 7z, RAR, DOCX, XLSX, PPTX, and legacy OLE Office files when the name or content type says so. HTML, SVG, JavaScript, scripts, WASM, and executables are rejected. An unknown binary is rejected. A declared type and a sniffed type that disagree are recorded. If either side is an active rejected type, the file is not saved.
You must be entitled to fetch and reuse the files. The Actor does not grant that right, does not check licenses, and does not claim the files are safe to open. Archives are not unpacked. Nothing that was downloaded is executed or treated as instructions.
Retention
This Actor uses the run's default unnamed dataset and key-value store. It does not name them, does not set expiresAt, and does not delete the dataset, the ZIP, or the manifest. It does not create a cleanup task or a schedule.
How long the files remain is the retention period of the Apify plan on the account that runs the Actor. Checked against Apify's storage documentation: unnamed storages and runs beyond the 10 most recent are deleted automatically, and named storages stay until someone deletes them. On the Free plan, the 10 most recent runs are retained for 4 months. On a paid plan, all of that account's data, including the 10 most recent runs, follows the retention period configured in that account's billing settings. Unnamed storages beyond those 10 runs are deleted when that period ends. The runs documentation also says the ten most recent runs stay accessible, and older runs are deleted with their default storages after the plan period. This Actor does not invent a shorter TTL.
Failure semantics
A URL that is rejected does not become a saved file. Common statuses:
savedandsaved_type_mismatchare the only statuses that put a file in the ZIP, and they are the only download rows that can be charged.skipped_charge_limitmeans the charge ceiling could not cover that saved file, so it was left out of the ZIP and was not charged.rejected_credentialsomits the URL from the row and the log.rejected_nonpublic_address,rejected_nonpublic_host,rejected_mixed_dns, andrejected_portstop before a private or odd destination is used.rejected_type_mismatchandrejected_disallowed_typemean the body was not an allowed file. An HTML body with an imageContent-Typeis the mismatch case.rejected_oversize,rejected_budget, andrejected_zip_limitmean a cap was hit. Partial bytes remain on the run total.rejected_length_mismatchmeans the body ended cleanly at a different size thanContent-Length.rejected_timeoutmeans the connect or the read went idle.partial_failuremeans bytes arrived and then the transfer failed, including a connection that closed before a declaredContent-Lengthwas complete. The file is not saved. There is no second try after that.rejected_http_statuscovers responses other than 200.aborted_deadline,skipped_deadline, andaborted_runmean the run stopped. Completed rows are left as they were.skippedURLs pastmaxUrlsare not fetched and are not copied into the dataset.OUTPUT.omittedUrlCountis the count.
If the ZIP cannot be stored, saved rows are marked rejected_zip_limit and PACK is absent. The manifest and dataset still describe the run. An all-failure run still writes a ZIP that contains only manifest.json.
API, scheduling, and integrations
- API: start a run with the input JSON above through the Apify API or the Apify clients for JavaScript and Python, then read the dataset and the
OUTPUT,MANIFEST, andPACKrecords. - Scheduling: use an Apify schedule to download the same public files every day or week and keep dated, hashed copies.
- Make, Zapier, and n8n: use the Apify integrations to send a list of URLs and pick up the ZIP when the run finishes.
- AI agents and MCP: the Apify MCP server can call this Actor, so an agent can hand over a list of file links and get a checksummed pack back.
The Actor itself does not create schedules, webhooks, or a standby web server.
FAQ and disclaimers
Does it crawl a site or discover files? No. You provide each direct file URL.
Can it use a signed or expiring link? Query names such as token, signature, X-Amz-Signature, and X-Goog-Signature are rejected so those secrets are not stored. Use a URL you are allowed to fetch that does not put a credential in the query or fragment.
Will it open the ZIP it downloaded? No. Archive bytes are stored. They are not extracted.
Are the files checked for malware or copyright? No.
What if a download fails? That row records the failure and any bytes already read. Other URLs keep the status they earned. A rejected, failed, or partial URL is not charged.
How many URLs per run? Up to 20. Start more runs for longer lists.
Can I prove a file did not change? Compare the sha256 in the dataset with a later run. Any change in the bytes changes the hash.
Questions or a file that should have worked? Open an issue on this Actor's Issues tab.
More from SteadyScrape
- Google Images API: Google Images results as JSON, with real image sizes, source pages, site names and related searches.
- PDF to Text Markdown Extractor: extract plain text and Markdown from digital PDFs you supply by URL.
- Website Screenshot Evidence Packs: desktop and mobile PNG screenshots of public pages with SHA-256 hashes and HTTP status.
- AI Page to JSON: fetch a public page and extract structured JSON with your own schema or field list.
See every SteadyScrape (steady-scrape) Actor on our Apify profile.