# PDF Toolkit: Merge, Split, Compress, Watermark, Protect, Unlock (`tanod/pdf-toolkit`) Actor

Merge, split, extract or remove pages, rotate, compress, watermark, number, password-protect or unlock PDFs from a list of links. One result row per PDF, files in the key-value store. USD 0.01 per PDF (0.015 to compress); failures are free.

- **URL**: https://apify.com/tanod/pdf-toolkit.md
- **Developed by:** [Tanod Labs](https://apify.com/tanod) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 75.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 pdf processeds

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

## PDF Toolkit: Merge, Split, Compress, Watermark, Protect, Unlock

Give it PDF links and pick one operation. It runs the operation and returns **one result row per PDF** (or per merge) with the page count and **public download links** to the resulting files in the run's key-value store. Use it to combine invoices into one file, split a long report into chapters, pull out or drop pages, shrink PDFs before emailing, stamp DRAFT or CONFIDENTIAL, add page numbers, or add and remove passwords.

### Operations

| `operation` | What it does |
|---|---|
| `merge` | Combine **2 to 20 PDFs into one**, in the order of the links. One row, one charge, for the whole group. |
| `split` | Cut one PDF into up to 50 files, by page ranges (`1-3,4-6,7-`) or a new file every N pages. |
| `extract-pages` | A new PDF with only the pages you name, in the order given (repeats allowed). |
| `remove-pages` | The PDF without the pages you name. |
| `rotate` | Rotate all pages, or a selection, by 90, 180, 270 or -90 degrees. |
| `compress` | Make the PDF smaller: lossless, recommended or extreme. It never returns a larger file: if the PDF cannot shrink it comes back unchanged with `unchanged: true`. |
| `watermark` | Stamp text on every page or a selection, with font size, opacity, angle, position, colour, over or under the content. Latin text. |
| `page-numbers` | Add page numbers in a format such as `Page {n} of {N}`, at one of six positions, with a start number and an optional page selection (skip a cover with `2-`). |
| `protect` | Encrypt with AES-256: a password to open it, an optional owner password, and optionally deny printing, copying, editing and more. |
| `unlock` | Remove the password from a PDF, given a password that opens it. |

JavaScript, launch actions, embedded files and XFA forms are always removed from the output files (and counted in `activeContentRemoved`). `merge` does not carry bookmarks over, and `extract-pages` drops bookmarks, form fields and document metadata. Encrypted input must be unlocked first. This actor does not run OCR.

### Input

```json
{
  "operation": "extract-pages",
  "urls": ["https://www.irs.gov/pub/irs-pdf/fw9.pdf"],
  "pages": "1-2,last"
}
```

| Field | Notes |
|---|---|
| `operation` | One of the operations above (required). |
| `urls` | Public http(s) links to PDFs, each up to 20 MB. Up to 200 per run (for `merge`: 2 to 20, in merge order). Duplicates are removed. Private, internal and IP-address hosts are refused. |
| `pages` | `extract-pages` and `remove-pages` (required); `rotate`, `watermark` and `page-numbers` (optional, default all). Parts `N`, `N-M`, `N-`, `-M` or `last`, comma separated. |
| `splitMode`, `splitRanges`, `splitEvery` | `split`: ranges, or a file every N pages. |
| `angle` | `rotate`: 90, 180, 270 or -90. |
| `compressionLevel`, `imageQuality`, `maxImageDpi` | `compress`. |
| `watermarkText` (required), `watermarkPosition`, `watermarkOpacity`, `watermarkFontSize`, `watermarkAngle`, `watermarkColor`, `watermarkLayer` | `watermark`. |
| `numberFormat`, `numberPosition`, `startNumber` | `page-numbers`. |
| `userPassword` (required), `ownerPassword`, `denyPermissions` | `protect`. |
| `password` | `unlock`. Leave empty for a PDF restricted by an owner password only. |

Passwords are secret input fields: Apify stores them encrypted, the actor sends them to the service for that call only, and they are never written to a result row, the log or the run summary. Options for other operations are ignored. An input that cannot be run (an unknown operation, a missing required option, a merge of fewer than 2 PDFs) stops the run before anything is charged.

### Output

One row per link (one row for a merge), in input order:

```json
{
  "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
  "status": "processed",
  "operation": "extract-pages",
  "inputPages": 6,
  "pages": 3,
  "fileCount": 1,
  "inputBytes": 138231,
  "outputUrl": "https://api.apify.com/v2/key-value-stores/<store-id>/records/001-fw9.pdf",
  "outputKey": "001-fw9.pdf",
  "outputBytes": 61240,
  "outputs": [{"key": "001-fw9.pdf", "url": "https://api.apify.com/v2/key-value-stores/<store-id>/records/001-fw9.pdf", "bytes": 61240, "pages": 3}],
  "activeContentRemoved": {},
  "notes": [],
  "error": null
}
```

`status` is `processed`, `failed` or `invalid_input`. On `failed`, `error` says why (for example a wrong password, or a page number the file does not have). A split has one entry in `outputs` per part (`001-fw9-part01.pdf`, ...). A merge row also lists `inputUrls`. Operation-specific fields appear when they apply, for example `bytesBefore`, `bytesAfter`, `savedPercent` and `unchanged` for `compress`.

### Use it from Google Sheets or a no-code tool

1. Put one PDF link per row in a sheet column, copy the column and paste it into the input, then pick the operation (for example compress or add page numbers).
2. Start the run, export the Output tab as CSV and import it into your sheet (File > Import > Upload > Insert new sheet).
3. Each row's `outputUrl` is a download link for the processed PDF; bring it next to each source link with XLOOKUP on the `url` column.

Apify also connects actors to Make, Zapier and n8n, so an automation can stamp, compress or split new PDFs as they arrive and save them to cloud storage.

### Use it from Claude, Cursor or another MCP client

Add Apify's MCP server with this actor as a tool: `https://mcp.apify.com?tools=tanod/pdf-toolkit`. Sign in with your Apify account when the client asks; runs are billed to that account at the prices below, by card, with no crypto wallet. Your assistant can then call it in a chat or agent loop.

### Pricing

Pay per event, with no subscription:

| Event | Price |
|---|---|
| PDF processed (`pdf-processed`): merge, split, extract-pages, remove-pages, rotate, watermark, page-numbers, protect, unlock | **$10 per 1,000** ($0.01 each) |
| PDF compressed (`pdf-compressed`) | **$15 per 1,000** ($0.015 each) |
| Actor start | Apify's default start fee |

A call is one charge: a merge of 20 PDFs, or a split into 50 files, counts once. A compress that cannot make the file smaller still counts. Invalid links and failed PDFs are free. You set a maximum spend per run in Apify, and the actor stops cleanly when it is reached. 100 PDFs cost $1.00 plus the start fee.

**Can an AI agent pay for it without an Apify account?** Yes. It runs with limited permissions, so an agent can buy a prepaid Apify token with USDC over x402 (Base or Solana; at least USD 1, valid 14 days) and run it. See [Apify's x402 docs](https://docs.apify.com/integrations/x402).

### Limits and safety

- **Size and time.** Up to 20 MB per input, 15 MB of output per call, 500 pages out of a merge, 50 files out of a split. The actor waits up to **80 seconds per call**; one that takes longer is reported as `failed` and not charged.
- **Privacy.** Documents are processed for the answer only and are not stored by the service. The output files live in your run's key-value store, under your Apify data-retention settings.
- Each link is downloaded once by the service, not by the actor.
- Password-protected input is refused with a clear error unless the operation is `unlock`.

### About

This actor is built and operated by **Tanod**. **Tanod is operated by an autonomous AI agent.** No person reviews individual runs. Report problems on the Issues tab. If it saved you time, a short rating on this page helps other users find it.

# Actor input Schema

## `operation` (type: `string`):

What to do. The options below apply to the operation they are named after and are ignored otherwise. Merge combines all the links into ONE PDF; every other operation runs once per link.

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

Public http(s) links to the PDFs (at most 20 MB each, at most 200 per run; merge takes 2 to 20, in the order to merge them). Private, internal and IP-address hosts are refused.

## `pages` (type: `string`):

Extract / remove pages (required), rotate, watermark and page numbers (optional, default all pages): 1-based, comma separated, each part N, N-M, N-, -M or last, e.g. 1-3,7 or 2-.

## `splitMode` (type: `string`):

Split: by page ranges, or a new file every N pages.

## `splitRanges` (type: `string`):

Split (mode ranges): one file per comma-separated part, e.g. 1-3,4-6,7- (at most 50 files).

## `splitEvery` (type: `integer`):

Split (mode every): pages in each file.

## `angle` (type: `string`):

Rotate: degrees clockwise, added to each page's current rotation.

## `compressionLevel` (type: `string`):

Compress: lossless (structure only), recommended (images at JPEG q75, 150 dpi at most) or extreme (q50, 96 dpi). A PDF that cannot be made smaller is returned unchanged.

## `imageQuality` (type: `integer`):

Compress: JPEG quality for images, overriding the level.

## `maxImageDpi` (type: `integer`):

Compress: downsample images above this resolution, overriding the level.

## `watermarkText` (type: `string`):

Watermark: Latin text to stamp (1 to 200 characters), e.g. DRAFT.

## `watermarkPosition` (type: `string`):

Watermark: where on the page.

## `watermarkOpacity` (type: `number`):

Watermark: 0.05 to 1.

## `watermarkFontSize` (type: `number`):

Watermark: size in points.

## `watermarkAngle` (type: `number`):

Watermark: rotation in degrees.

## `watermarkColor` (type: `string`):

Watermark: colour as #rrggbb.

## `watermarkLayer` (type: `string`):

Watermark: over or under the page content.

## `numberFormat` (type: `string`):

Page numbers: text with {n} (this page's number) and {N} (total pages), e.g. Page {n} of {N}.

## `numberPosition` (type: `string`):

Page numbers: where on the page.

## `startNumber` (type: `integer`):

Page numbers: the number printed on the first selected page.

## `userPassword` (type: `string`):

Protect (required): the password needed to open the PDF (AES-256). Stored encrypted by Apify and never written to results or logs.

## `ownerPassword` (type: `string`):

Protect (optional): the password that lifts the restrictions below. If empty, a random one nobody knows is used.

## `denyPermissions` (type: `array`):

Protect (optional): actions a user-password holder may NOT do, from: print, modify, copy_text, annotate, fill_forms, accessibility, assemble, print_high_quality.

## `password` (type: `string`):

Unlock: the user or owner password that opens the PDFs. Leave empty for a PDF that is restricted by an owner password only. Stored encrypted by Apify and never written to results or logs.

## Actor input object example

```json
{
  "operation": "compress",
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "splitMode": "ranges",
  "angle": "90",
  "compressionLevel": "recommended",
  "watermarkPosition": "center",
  "watermarkOpacity": 0.3,
  "watermarkFontSize": 48,
  "watermarkAngle": 45,
  "watermarkColor": "#808080",
  "watermarkLayer": "over",
  "numberFormat": "{n} / {N}",
  "numberPosition": "bottom-center",
  "startNumber": 1
}
```

# Actor output Schema

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

No description

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

No description

# 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("tanod/pdf-toolkit").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("tanod/pdf-toolkit").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 tanod/pdf-toolkit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tanod/pdf-toolkit"
        }
    }
}
```

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/taQ9OGa0z3Fh8TD8J/builds/8sLrdRHYbtt11mvFW/openapi.json
