# Public PDF Batch Downloader and ZIP Archive (`automation-lab/public-pdf-batch-archive`) Actor

Download supplied public PDF URLs unchanged into one bounded ZIP, preserving filenames in per-input folders with SHA-256 hashes and a completion manifest.

- **URL**: https://apify.com/automation-lab/public-pdf-batch-archive.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.44 / 1,000 pdf archiveds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Public PDF Batch Downloader and ZIP Archive

This PDF batch downloader collects a supplied list of anonymous public PDF URLs into one ZIP of unchanged original documents. It keeps original safe filenames in numbered folders and includes a per-input status manifest with SHA-256 checksums. There is no OCR, conversion or website crawling.

### Who is it for?

Researchers collecting public reports, operations teams assembling document packs, and analysts retaining source documents can use the archive as a repeatable input to their own review workflows. Supply direct document links, not website homepages. Use only documents you are authorized to download and retain.

### Why use this PDF batch downloader?

- One downloadable ZIP instead of manually saving every document.
- Original bytes and filenames, not extracted text or reconstructed PDFs.
- One status row for every supplied input, including failures and duplicates.
- SHA-256 hashes for checking integrity and comparing later collections.
- Identical filenames cannot overwrite each other: each unique file has a numbered directory.

### Getting started

1. Add 1–100 direct, public HTTP or HTTPS PDF URLs to **Direct PDF URLs**.
2. Keep the default size caps or choose smaller caps for your collection.
3. Start the Actor and wait for its terminal status.
4. Open **Run completeness** and check `complete` and `failedCount`.
5. Download **Download ZIP** and inspect `manifest.json` inside it.

Example input:

```json
{
  "urls": ["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"],
  "maxFileSizeMb": 5,
  "maxTotalSizeMb": 20,
  "requestTimeoutSecs": 15
}
```

The public W3C test document is a convenient starting example; its availability is controlled by W3C, not this Actor.

### Input parameters

| Field | Default | Meaning |
| --- | --- | --- |
| `urls` | required | 1–100 direct PDF URL strings in processing order |
| `maxFileSizeMb` | 5 | Maximum original bytes per unique PDF, in binary MiB; range 1–10 |
| `maxTotalSizeMb` | 20 | Maximum summed successful unique original bytes per run, in binary MiB; range 1–32 |
| `requestTimeoutSecs` | 15 | Wall-clock timeout per attempt, range 5–30 seconds |

Zero or unlimited limits are not supported. Each MiB is 1,048,576 bytes. The ZIP may contain up to 1 MiB of bounded packaging/manifest overhead beyond the original-byte cap. PDFs are processed sequentially; a large file that cannot fit is rejected, while a later smaller file can still fit. Every download shares a 220-second run download budget.

### Supported URLs and exclusions

Only anonymous public destinations on standard HTTP/HTTPS ports are allowed. URL credentials and known credential/signature query parameters are rejected, so authenticated, tokenized or signed links are not supported. Anonymous public query parameters such as document IDs are supported. Input and final URLs in output have their queries, userinfo and fragments removed for privacy; use inputIndex to correlate a row with your original list. Fragments are removed. The Actor does not accept cookies, custom headers, private credentials, local files or internal addresses.

All DNS answers and each redirect destination are checked for public reachability; the validated address is pinned for the connection. There are no proxies or browser fallbacks. Supply an anonymous direct PDF URL; a source that requires login or a browser challenge is outside this product's scope.

### Output files

The run's Key-Value Store contains:

| Key | Content |
| --- | --- |
| `ARCHIVE.zip` | ZIP containing unchanged successful PDFs and `manifest.json`; absent if all inputs fail |
| `MANIFEST` | JSON with the completeness summary and a status row for every input |
| `SUMMARY` | JSON counts, successful original-byte total and completeness flag |

The default dataset contains the same per-input status rows. It can be exported as JSON, CSV or Excel for reconciliation. Exporting the dataset does not export the binary PDFs; download the ZIP separately.

### Manifest fields

| Field | Meaning |
| --- | --- |
| `inputIndex` | One-based position in the original list |
| `url` | Input URL with credentials, query and fragment removed; invalid URLs use a safe placeholder |
| `finalUrl` | Final validated public URL, or null on failure |
| `status` | `downloaded`, `duplicate` or `failed` |
| `filename` | Preserved safe original basename, or null |
| `archivePath` | Numbered ZIP path; duplicates reference the first successful member |
| `bytes` | Original PDF byte count, or null |
| `sha256` | SHA-256 digest of original bytes, or null |
| `httpStatus` | Known final HTTP status, or null |
| `attempts` | Attempt count, zero for duplicates or rejected inputs |
| `error` | Safe failure message, or null |
| `archivedAt` | ISO timestamp at the start of input processing |

A representative W3C test-document result:

```json
{
  "inputIndex": 1,
  "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "finalUrl": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "status": "downloaded",
  "filename": "dummy.pdf",
  "archivePath": "001/dummy.pdf",
  "bytes": 13264,
  "sha256": "3df79d34abbca99308e79cb94461c1893582604d68329a41fd4bec1885e6adb4",
  "httpStatus": 200,
  "attempts": 1,
  "error": null,
  "archivedAt": "2026-10-14T20:07:16.679Z"
}
```

### Filenames, duplicates and original bytes

The filename comes from the final server's `Content-Disposition` header, preferring a UTF-8 `filename*`, then from the final URL basename. A URL without a basename uses `document.pdf`. Unsafe filenames, path separators, control characters, reserved device names and names above 200 UTF-8 bytes are rejected instead of silently renamed.

Unique files use paths such as `001/report.pdf` and `002/report.pdf`. Their basenames and PDF bytes are unchanged. Identical normalized URLs reuse the first successful member and incur no additional PDF event charge. Different URLs with identical bytes are separate documents; there is no hash-based deduplication.

Responses must begin with `%PDF-`. HTTP content encoding other than `identity` is rejected rather than decoded. The Actor checks byte completeness but does not parse the entire PDF, repair corrupt documents or guarantee that a viewer will render every supplied file.

### Failure behavior and troubleshooting

Network errors, HTTP 429 and HTTP 5xx responses receive at most one bounded retry. HTTP 4xx other than 429, invalid URLs, unsafe destinations, non-PDF bodies and oversize files are not repeatedly retried. Redirects are limited to five and cannot bypass destination checks.

If at least one unique PDF is archived, the run can succeed with `complete: false`. This is a partial archive, not 100% completion. Inspect every failed status before accepting the batch. If all inputs fail, the run fails after writing its manifest and summary. Malformed top-level input fails before collection begins.

**No ZIP?** Check whether all inputs failed. **HTML instead of PDF?** Use the actual document link, not its landing page. **Size limit?** Raise the limit within the supported range or split the collection. **URL parameters rejected?** Public document ID parameters are supported, but known token, signature and credential keys are rejected; do not paste tokens or private links.

### How much does it cost to archive public PDFs?

The Actor uses pay-per-event pricing: a $0.0005 start event per valid run and one **PDF archived** event per unique PDF saved into the ZIP. The BRONZE rate is $0.0024 per archived PDF. Duplicate inputs, failed downloads and manifest rows have no additional PDF event charge.

At BRONZE, 1 archived PDF costs $0.0029, 10 cost $0.0245, and 100 cost $0.2405 in Actor event charges. Other subscription tiers have their own displayed event prices. Any platform charges shown separately by Apify are not included in these examples. Set a suitable total-charge limit before running.

### Integrations

Use Make or n8n to supply a document URL list and run the Actor. On completion, inspect `SUMMARY`, route failed inputs for review, and copy `ARCHIVE.zip` to your own approved document storage. In an analysis pipeline, keep the original ZIP alongside the dataset export and use the hashes to detect byte changes between runs. The Actor does not schedule itself or emit change alerts.

### API usage

Keep your Apify token in an environment variable; never put it in public document URLs.

```bash
curl -X POST 'https://api.apify.com/v2/acts/automation-lab~public-pdf-batch-archive/runs' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"urls":["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"]}'
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/public-pdf-batch-archive').call({
  urls: ['https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf']
});
const { items } = await client.dataset(run.defaultDatasetId).listItems({ limit: 100 });
console.log(items);
// Download ARCHIVE.zip from this run's defaultKeyValueStoreId after checking SUMMARY.
```

Python:

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/public-pdf-batch-archive').call(run_input={
    'urls': ['https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf']
})
rows = client.dataset(run['defaultDatasetId']).list_items(limit=100).items
print(rows)
```

### MCP use

Select the stable Actor-specific subset:

```bash
claude mcp add --transport http apify \
  'https://mcp.apify.com?tools=automation-lab/public-pdf-batch-archive'
```

Equivalent Claude Desktop, Cursor and VS Code client configuration:

```json
{"mcpServers":{"apify":{"url":"https://mcp.apify.com?tools=automation-lab/public-pdf-batch-archive"}}}
```

Configure authorized authentication in your MCP client. Example prompts can ask for completeness or failure reconciliation; they do not authorize repeated runs. Discover actual tool names and argument schemas with read-only `tools/list`; selecting an Actor also adds run/data tools, not exactly one tool. An example consumer prompt is: “Archive these public direct PDF URLs once, then report the completeness summary and ZIP location.”

Start once and retain the returned run ID and storage IDs. `call-actor` waits at most 45 seconds; IDs are not results. If pending, check that same run with bounded backoff (2, 4, 8, then 10 seconds), a 120-second total deadline, and a client timeout above the selected server wait. Do not restart after a client timeout; recover the known run. At deadline report pending and stop polling.

After successful completion, read `SUMMARY`, then use `get-dataset-items` with the returned run's `defaultDatasetId`. For example, send this argument object using the actual tool name and field format confirmed by `tools/list` (replace the placeholder with the returned dataset ID):

```json
{
  "datasetId": "<returned defaultDatasetId>",
  "limit": 20,
  "offset": 0,
  "fields": ["inputIndex", "status", "filename", "archivePath", "bytes", "sha256", "error"]
}
```

Use untransformed JSON pages of 20 source rows, with a total consumer budget of 100 rows or 64 KiB of serialized UTF-8 result content admitted to model context across all pages, whichever comes first. Process each response host-side before model injection. Advance the source offset by the number of source rows consumed: after a full first page, request offset 20, then 40, 60 and 80. Track source pagination metadata, not counts after any host-side filtering or summarization. Stop at source exhaustion (a short/empty untransformed page or the reported total) or either budget.

Enforce the byte ceiling host-side even for an oversized row/page: omit or summarize with explicit truncation disclosure rather than injecting the full response. If your client cannot intercept responses, do not claim a hard byte guarantee. Report admitted row count, next source offset (including any unadmitted rows), and partial/truncated status when incomplete. Keep full dataset exports and binary ZIP downloads in storage or downstream processing outside model context. These consumer budgets do not reduce Actor collection limits. A failed run's rows are partial evidence, not a complete successful result. Client discovery/caching does not guarantee numeric token savings.

### Legality and privacy

Public access is not permission to redistribute copyrighted documents. Respect source terms, robots guidance where applicable, ownership and applicable law. This tool does not bypass authentication or anti-bot controls. It is not affiliated with or endorsed by any document source. No AI model is used at runtime. Report problems through the Actor's Apify Issues tab, including the run ID but not private credentials. Do not provide private, personal or credential-bearing inputs.

Documents and manifests remain in the run's Apify storage according to your account's access and retention settings. Failed operations send sanitized diagnostic input, exceptions and actor/build/run IDs to our private GlitchTip service for repair; secret fields and URL queries are removed and reports are retained for 30 days.

### FAQ

**Does this discover PDFs on a website?** No. Supply the direct PDF URLs yourself.

**Does it extract text or use OCR?** No. It packages unchanged original PDFs.

**Will it finish every URL?** Not guaranteed. The manifest explicitly reports failures and bounded-limit exclusions. A successful run can be partial.

**Can filenames collide?** Equal basenames are retained in different input-number directories; no successful file overwrites another.

**Can I repeat the collection?** Yes, with a new run. Compare manifests or hashes downstream; no monitoring or persistent history is built in.

### Related Actors

For downstream analysis, [PDF Text Extractor](https://apify.com/automation-lab/pdf-text-extractor) extracts text, while [PDF to Structured Markdown Converter](https://apify.com/automation-lab/pdf-to-structured-markdown-converter) supports a separate document conversion workflow. This Actor's archive remains unconverted.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/public-pdf-batch-archive/changelog.md

# Actor input Schema

## `urls` (type: `array`):

1–100 anonymous public HTTP/HTTPS document URLs without credentials or nonstandard ports. Public query parameters such as document IDs are supported; known credential/signature parameters are rejected. Output URLs have queries removed for privacy. Input order is preserved; identical normalized URLs reuse the first successful archive member without a second item charge. Every input receives a status row.

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

Maximum bytes per unique PDF in binary MiB (1,048,576 bytes). Oversize responses are rejected and uncharged, including when Content-Length is missing. Zero/unlimited is unsupported.

## `maxTotalSizeMb` (type: `integer`):

Global run limit for summed successful unique PDF bytes before ZIP compression, processed in input order. A file exceeding the remaining allowance fails; subsequent smaller PDFs can still fit. ZIP overhead is separately bounded to 1 MiB. Duplicates consume no extra bytes.

## `requestTimeoutSecs` (type: `integer`):

Wall-clock timeout including DNS, up to five redirects and the complete body per attempt. Transient network, 429 and 5xx failures receive at most one retry. All downloads also share a 220-second run budget; unattempted inputs receive failed status when exhausted.

## Actor input object example

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "maxFileSizeMb": 5,
  "maxTotalSizeMb": 20,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

## `archive` (type: `string`):

Download successful original PDFs and manifest.json; absent when all inputs fail.

## `manifest` (type: `string`):

Per-input statuses and run completeness as JSON.

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

Counts, successful bytes and complete flag as JSON.

## `dataset` (type: `string`):

One status/hash row per input for reconciliation.

# 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 = {
    "urls": [
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/public-pdf-batch-archive").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 = { "urls": ["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/public-pdf-batch-archive").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 '{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}' |
apify call automation-lab/public-pdf-batch-archive --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/public-pdf-batch-archive"
        }
    }
}
```

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/OrSsQQCnADEFBUKdh/builds/p4ukwmmqwtJ8u9Gps/openapi.json
