Bulk Image and File Downloader (ZIP + SHA-256) avatar

Bulk Image and File Downloader (ZIP + SHA-256)

Pricing

from $4.00 / 1,000 saved files

Go to Apify Store
Bulk Image and File Downloader (ZIP + SHA-256)

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

SteadyScrape

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

an hour ago

Last modified

Features

Limited permissions

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.

FieldMeaning
statusExplicit outcome, such as saved, saved_type_mismatch, or rejected_type_mismatch
savedWhether the file is in the ZIP
partialWhether bytes were counted even though the file was not saved
sourceUrlSanitized URL, or null when the URL contained credential material
detectedMediaTypeType read from the bytes, not from the filename alone
byteLengthDecoded bytes read, including partial failures
sha256SHA-256 of those decoded bytes
zipEntryNameSanitized name inside the ZIP
httpStatusLast 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

ApproachWhat you get
Saving files one by one in a browserNo checksums, no record of which links failed, and HTML error pages saved as if they were images
A quick wget or curl scriptFast, but you have to handle redirects to private addresses, fake image bodies, size caps, and file names yourself
This ActorOne 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

  1. Open the input and paste up to 20 direct http or https file URLs. Use port 80 for http and port 443 for https.
  2. Lower a cap if you want a smaller run. You cannot raise the caps.
  3. Run the Actor.
  4. Read the dataset for per-URL status. Download the PACK record for the ZIP and MANIFEST for the JSON copy.

Locally, from this project:

npm ci
npm run build
apify 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:

EventPriceWhen it is charged
saved-file$0.004Once per file stored in the ZIP
run-with-saved-file$0.003Once 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.

CapMaximum
URLs processed20
Decoded bytes per file10 MB (10485760)
Decoded plus partial bytes per run25 MB (26214400)
ZIP size32 MB (33554432)
Concurrency2
Runtime120 seconds
Redirects5
Connect and header timeout10 seconds
Read idle timeout10 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:

  • saved and saved_type_mismatch are the only statuses that put a file in the ZIP, and they are the only download rows that can be charged.
  • skipped_charge_limit means the charge ceiling could not cover that saved file, so it was left out of the ZIP and was not charged.
  • rejected_credentials omits the URL from the row and the log.
  • rejected_nonpublic_address, rejected_nonpublic_host, rejected_mixed_dns, and rejected_port stop before a private or odd destination is used.
  • rejected_type_mismatch and rejected_disallowed_type mean the body was not an allowed file. An HTML body with an image Content-Type is the mismatch case.
  • rejected_oversize, rejected_budget, and rejected_zip_limit mean a cap was hit. Partial bytes remain on the run total.
  • rejected_length_mismatch means the body ended cleanly at a different size than Content-Length.
  • rejected_timeout means the connect or the read went idle.
  • partial_failure means bytes arrived and then the transfer failed, including a connection that closed before a declared Content-Length was complete. The file is not saved. There is no second try after that.
  • rejected_http_status covers responses other than 200.
  • aborted_deadline, skipped_deadline, and aborted_run mean the run stopped. Completed rows are left as they were.
  • skipped URLs past maxUrls are not fetched and are not copied into the dataset. OUTPUT.omittedUrlCount is 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, and PACK records.
  • 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

See every SteadyScrape (steady-scrape) Actor on our Apify profile.