PDF Tools: Merge, Split, Protect and Unlock
Merge several PDFs into one, split one PDF into several files, or add and remove a PDF's password. Pages are copied, not re-rendered, so sizes, rotation and content stay exactly as they were.
| Endpoint | What it does |
|---|---|
POST /pdf/merge | Merge up to 20 PDFs (4 MB in total) and get the result immediately |
POST /pdf/split | Split a PDF of up to 4 MB into up to 100 files |
POST /pdf/protect | Add a password to a PDF, with optional restrictions on printing, copying and editing |
POST /pdf/unlock | Remove a PDF's password, given that password |
POST /pdf/inspect | Page count and password protection, free |
POST /pdf/upload-url | Upload a large PDF (up to 150 MB) |
POST /pdf/jobs | Merge or split uploaded PDFs as a background job |
When to Use
✅ Use the PDF tools when:
- You already have PDFs and need to combine them, reorder them, or take pages out
- You're assembling document packs — contract + appendix + invoice
- You're splitting a statement or payslip run into one file per person
- You need only some pages of a long document
- A PDF has to be password-protected before it is sent, or unlocked before it can be merged, split or edited
❌ Use something else when:
- You need to create a PDF from HTML, Markdown, images or a URL →
POST /quickjob - You need to convert many HTML or Markdown files →
POST /bulkjob
Requests with up to 4 MB of PDFs run instantly with /pdf/merge and /pdf/split. Anything larger is uploaded with /pdf/upload-url and processed as a background job with /pdf/jobs.
/pdf/protect and /pdf/unlock never queue: a larger PDF is uploaded with /pdf/upload-url, then protected or unlocked in one request.
The PDF tools must be enabled on your plan. GET /plans/{plan_id} returns pdf_tools_enabled; without it every /pdf route returns 403 PDF_TOOLS_NOT_ENABLED.
Quick Example
curl -X POST https://api.podpdf.com/pdf/merge \
-H "X-API-Key: your_api_key_here" \
-F "files=@contract.pdf" \
-F "files=@appendix.pdf" \
-o merged.pdf
Authentication
Send either an API key or a Cognito ID token:
X-API-Key: your_api_key_here
Authorization: Bearer <cognito_id_token>
Page Ranges
Every page selection uses one syntax: comma-separated parts, each a page (3), a range (1-3) or an open range (8-end). Pages are 1-based and used in the order written.
| Range | Pages |
|---|---|
3 | 3 |
1-3 | 1, 2, 3 |
8-end | 8 to the last page |
1,3,5-7 | 1, 3, 5, 6, 7 |
5,1 | 5, then 1 |
2,2 | 2 twice |
Rejected with 400 INVALID_PAGE_RANGE: page 0, negative or decimal numbers, empty parts (1,,2), a range that runs backwards (5-3) and more than 500 parts. A page past the end of the document returns 400 PAGE_OUT_OF_RANGE with details.total_pages.
POST /pdf/merge
Merge 2 to 20 PDFs, up to 4 MB in total, in the order given.
Request
Multipart (recommended)
| Field | Required | Description |
|---|---|---|
files | Yes | One part per PDF, in merge order |
page_ranges | No | JSON array with one entry per file: a page range, or null for every page |
options | No | JSON object, see below |
store | No | true to store the result and return JSON with a download link |
curl -X POST https://api.podpdf.com/pdf/merge \
-H "X-API-Key: your_api_key_here" \
-F "files=@contract.pdf" \
-F "files=@appendix.pdf" \
-F "files=@invoice.pdf" \
-F 'page_ranges=[null, "2-end", "1"]' \
-F 'options={"output_filename": "deal-pack", "title": "Deal pack"}' \
-o deal-pack.pdf
JSON with base64
{
"inputs": [
{ "pdf_base64": "JVBERi0xLjcK...", "filename": "contract.pdf" },
{ "pdf_base64": "JVBERi0xLjcK...", "filename": "appendix.pdf", "pages": "2-end" }
],
"options": { "output_filename": "deal-pack" },
"store": false
}
Base64 adds a third to the size, so the 4 MB limit applies to the decoded files.
| Option | Description |
|---|---|
output_filename | Name of the merged file, without .pdf (default merged) |
title | Document title stored in the PDF |
Response
Merged PDF (200) — returned directly when the result is smaller than 4.4 MB and store is not set:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="deal-pack.pdf"
X-PDF-Pages: 12
X-PDF-Output-Count: 1
X-PDF-Warnings:
X-Job-Id: 0f6c5a1e-4b1d-4c9e-a2f4-6d7e8f9a0b1c
Stored result (200 JSON) — with store: true, or when the merged PDF is too large to return directly:
{
"job_id": "0f6c5a1e-4b1d-4c9e-a2f4-6d7e8f9a0b1c",
"operation": "pdf_merge",
"status": "completed",
"input_pages": 12,
"output_count": 1,
"outputs": [
{
"index": 0,
"name": "deal-pack.pdf",
"pages": 12,
"page_range": "1-12",
"size_bytes": 348210,
"warnings": [],
"download_url": "https://...deal-pack.pdf?X-Amz-Signature=..."
}
],
"zip_url": null,
"download_url_expires_at": "2026-09-13T11:00:00.000Z",
"warnings": []
}
POST /pdf/split
Split one PDF of up to 4 MB and 500 pages into up to 100 files.
Request
Multipart (recommended)
| Field | Required | Description |
|---|---|---|
file | Yes | The PDF |
split | Yes | JSON object, see modes below |
options | No | JSON object: filename_pattern |
store | No | true to store a single-file result instead of returning it directly |
curl -X POST https://api.podpdf.com/pdf/split \
-H "X-API-Key: your_api_key_here" \
-F "file=@statements.pdf" \
-F 'split={"mode": "every_n", "every_n": 2}' \
-F 'options={"filename_pattern": "statement-{index:03}"}'
JSON with base64
{
"input": { "pdf_base64": "JVBERi0xLjcK...", "filename": "statements.pdf" },
"split": { "mode": "ranges", "ranges": ["1-3", "4-9", "10-end"] }
}
Split modes
mode | Settings | Output |
|---|---|---|
extract | pages: a page range | One PDF with those pages |
ranges | ranges: array of page ranges | One PDF per range (ranges may overlap) |
every_n | every_n: whole number ≥ 1 | A new file every N pages; the last one may be shorter |
each_page | — | One PDF per page |
File names
filename_pattern supports {basename} (input name without .pdf), {index}, {index:03} (zero-padded to 3 digits) and {pages} (the page range). The default is {basename}-{index:03}.pdf: scan.pdf becomes scan-001.pdf, scan-002.pdf, … Unsafe characters are replaced and duplicate names get a -2, -3 suffix.
Response
A split that produces one file returns the PDF directly, exactly like merge.
A split that produces several files stores them and returns JSON with a link per file and a ZIP of all of them:
{
"job_id": "7a1c0e52-9d3b-4b0f-8c11-2f6a9e7d4c3b",
"operation": "pdf_split",
"status": "completed",
"input_pages": 10,
"output_count": 3,
"outputs": [
{ "index": 0, "name": "statements-001.pdf", "pages": 3, "page_range": "1-3", "size_bytes": 91822, "warnings": [], "download_url": "https://..." },
{ "index": 1, "name": "statements-002.pdf", "pages": 6, "page_range": "4-9", "size_bytes": 170114, "warnings": [], "download_url": "https://..." },
{ "index": 2, "name": "statements-003.pdf", "pages": 1, "page_range": "10", "size_bytes": 30557, "warnings": [], "download_url": "https://..." }
],
"zip_url": "https://...podpdf-7a1c0e52-....zip?X-Amz-Signature=...",
"download_url_expires_at": "2026-09-13T11:00:00.000Z",
"warnings": []
}
Download links last one hour. The files stay available for 7 days through GET /jobs/{job_id}/files/{index}/download and GET /jobs/{job_id}/download.
Each output is a complete PDF, so shared content — a letterhead image, embedded fonts — is included in every file. Splitting a templated document page by page can add up to more than the original. Splits whose files would total more than 300 MB (1 GB for jobs) are refused up front with OUTPUT_TOO_LARGE.
POST /pdf/protect
Add a password to one PDF. The result is encrypted with AES-256 and opens in every current PDF reader.
There are two passwords, and you can set either or both:
| What it does | |
|---|---|
user_password | Needed to open the PDF |
owner_password | Needed to change or remove the protection. Set it together with permissions to restrict what readers can do |
Request
Multipart (recommended)
| Field | Required | Description |
|---|---|---|
file | Yes | The PDF, up to 4 MB. For a larger file send JSON with source_key, below |
user_password | One of the two | The password to open the PDF |
owner_password | One of the two | The password that owns the PDF. Required when permissions restricts anything, and must differ from user_password |
permissions | No | JSON object, see below |
options | No | JSON object: output_filename (without .pdf; default is the input name with -protected) |
store | No | true to store the result and get a download link instead of the file |
curl -X POST https://api.podpdf.com/pdf/protect \
-H "X-API-Key: your_api_key_here" \
-F "file=@payslip.pdf" \
-F "user_password=correct-horse-battery" \
-o payslip-protected.pdf
JSON with base64
{
"input": { "pdf_base64": "JVBERi0xLjcK...", "filename": "board-pack.pdf" },
"user_password": "correct-horse-battery",
"owner_password": "a-different-one",
"permissions": { "print": "none", "copy": false }
}
A file over 4 MB is uploaded with POST /pdf/upload-url first and named by its key. Up to 150 MB and 5000 pages, still in one request:
{
"source_key": "pdf-uploads/a1b2c3/01JB7Q8ZK3M5N7P9R1T3V5X7Z9.pdf",
"filename": "scan.pdf",
"user_password": "correct-horse-battery"
}
Permissions
Everything is allowed unless you restrict it. Restrictions are enforced by PDF readers and lifted with the owner password.
| Key | Values | Default |
|---|---|---|
print | full, low (low resolution only), none | full |
copy | true, false — copying text and images | true |
modify | true, false — editing the document | true |
annotate | true, false — comments and annotations | true |
fill_forms | true, false — filling in form fields | true |
assemble | true, false — inserting, deleting and rotating pages | true |
Screen readers are always allowed to read the text.
Which passwords to send
| You send | Result |
|---|---|
user_password only | The PDF needs that password to open. Nothing is restricted |
user_password, owner_password and permissions | The PDF needs user_password to open, with the restrictions applied. owner_password lifts them |
owner_password and permissions | The PDF opens for anyone, with the restrictions applied |
A password can be up to 127 bytes and cannot contain line breaks or other control characters. A password with characters outside ASCII works, with the warning NON_ASCII_PASSWORD: some older readers prepare such passwords differently and may not accept it.
Response
The protected PDF, with the same headers as merge (X-PDF-Pages, X-PDF-Warnings, X-Job-Id).
With store: true, or for a source_key request, the PDF is stored and the response is JSON:
{
"job_id": "7a1c0e52-9d3b-4b0f-8c11-2f6a9e7d4c3b",
"operation": "pdf_protect",
"status": "completed",
"input_pages": 12,
"output_count": 1,
"download_url": "https://...scan-protected.pdf?X-Amz-Signature=...",
"outputs": [
{ "index": 0, "name": "scan-protected.pdf", "pages": 12, "page_range": "1-12", "size_bytes": 5241077, "warnings": [], "download_url": "https://..." }
],
"zip_url": null,
"download_url_expires_at": "2026-10-13T11:00:00.000Z",
"warnings": []
}
PodPDF uses a password to write the PDF and then forgets it. It is never stored, logged or shown on the job, so it cannot be recovered: keep it somewhere safe.
A protected PDF is refused by merge, split and the editor (PDF_ENCRYPTED), and a PDF that is already protected cannot be protected again. Do the other work first, or unlock the file.
POST /pdf/unlock
Remove the password from a PDF. You get back an ordinary PDF with no password and no restrictions, which merge, split and the editor accept.
A correct password is always required. PodPDF removes protection for someone who holds the password; it does not break it.
| The PDF | password must be |
|---|---|
| Needs a password to open | That password, or the owner password |
| Opens without a password, but cannot be printed, copied or edited | The owner password |
Request
Multipart (recommended)
| Field | Required | Description |
|---|---|---|
file | Yes | The protected PDF, up to 4 MB. For a larger file send JSON with source_key |
password | Yes | The PDF's password |
options | No | JSON object: output_filename (without .pdf; default is the input name with -unlocked) |
store | No | true to store the result and get a download link instead of the file |
curl -X POST https://api.podpdf.com/pdf/unlock \
-H "X-API-Key: your_api_key_here" \
-F "file=@statement.pdf" \
-F "password=correct-horse-battery" \
-o statement-unlocked.pdf
JSON
{
"input": { "pdf_base64": "JVBERi0xLjcK...", "filename": "statement.pdf" },
"password": "correct-horse-battery"
}
or, for a file uploaded with /pdf/upload-url (up to 150 MB and 5000 pages):
{
"source_key": "pdf-uploads/a1b2c3/01JB7Q8ZK3M5N7P9R1T3V5X7Z9.pdf",
"filename": "statement.pdf",
"password": "correct-horse-battery"
}
Response
The unlocked PDF, or the same JSON as protect with "operation": "pdf_unlock" when the result is stored.
A wrong password returns 422 PDF_PASSWORD_INCORRECT and is not charged. Older protection (40- and 128-bit RC4, 128-bit AES) is unlocked the same way as AES-256. PDFs protected with a certificate rather than a password return 422 PDF_ENCRYPTION_UNSUPPORTED.
POST /pdf/inspect
Check a PDF before merging or splitting it. Free, and not recorded as a job.
Send a PDF of up to 4 MB as multipart file (or JSON input.pdf_base64), or {"s3_key": "...", "filename": "..."} for a file already uploaded with /pdf/upload-url.
curl -X POST https://api.podpdf.com/pdf/inspect \
-H "X-API-Key: your_api_key_here" \
-F "file=@statements.pdf"
{
"filename": "statements.pdf",
"size_bytes": 1832200,
"page_count": 48,
"encrypted": false,
"has_forms": false,
"has_signatures": false,
"first_page": { "width": 595.28, "height": 841.89 },
"warnings": [],
"max_pages": 500
}
An encrypted file returns 200 with "encrypted": true and page_count: null. max_pages is the page limit for the path the file would take: 500 instant, 5000 for a job.
Opening a document for the editor is the same call with "for": "edit": it adds the page ids, sizes and rotation every edit refers to, and the document's form fields. See PDF Editor.
POST /pdf/upload-url
Get a signed URL to upload one PDF of up to 150 MB.
curl -X POST https://api.podpdf.com/pdf/upload-url \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"content_length_bytes": 48211934}'
{
"upload_url": "https://podpdf-prod-pdfs.s3.eu-central-1.amazonaws.com/pdf-uploads/...?X-Amz-Signature=...",
"s3_key": "pdf-uploads/01HZXUSERID/01J8K2Q9ZP6X7Y3M4N5V6W7A8B.pdf",
"expires_at": "2026-09-13T10:15:00.000Z",
"max_bytes": 157286400,
"required_headers": { "Content-Type": "application/pdf" }
}
Then upload the file:
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @exhibits.pdf
content_length_bytes must be the exact file size, and the URL is valid for 15 minutes. Do not send your API key to the upload URL. Uploads are deleted once a job that uses them finishes, and after one day otherwise.
POST /pdf/jobs
Merge or split uploaded PDFs in the background.
| Field | Required | Description |
|---|---|---|
operation | Yes | merge or split |
inputs | Yes | [{ "s3_key", "filename", "pages" }] — 2 to 100 for merge, exactly 1 for split. pages (merge only) is a page range |
split | For split | Same object as /pdf/split |
options | No | output_filename, title (merge) or filename_pattern (split) |
curl -X POST https://api.podpdf.com/pdf/jobs \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"operation": "split",
"inputs": [{ "s3_key": "pdf-uploads/01HZXUSERID/01J8K2Q9ZP6X7Y3M4N5V6W7A8B.pdf", "filename": "payslips.pdf" }],
"split": { "mode": "each_page" },
"options": { "filename_pattern": "payslip-{index:03}" }
}'
{
"job_id": "5b2d8f40-1e7c-4a8b-9c3d-6e0f1a2b3c4d",
"status": "queued",
"job_type": "pdfop",
"operation": "split",
"message": "PDF job queued for processing"
}
Tracking the job
Poll GET /jobs/{job_id} until status is completed or failed, or subscribe a webhook to job.completed and job.failed. While the job runs, output_count and processed_count show progress.
When it's done:
GET /jobs/{job_id}/fileslists every output fileGET /jobs/{job_id}/files/{index}/downloadreturns a link to one fileGET /jobs/{job_id}/downloadreturns the merged PDF, or a ZIP of all split files
A job either produces every file or fails as a whole; failed jobs have error_code and error_message, and /files contains one failed row naming the input. Up to 2 merge/split jobs can run at the same time per account.
Error Responses
{
"error": {
"code": "PDF_ENCRYPTED",
"message": "\"statement.pdf\" is password-protected or encrypted. Unlock it with POST /pdf/unlock first, then upload the unlocked copy.",
"details": { "filename": "statement.pdf", "input_index": 1 }
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | MISSING_PDFS | A merge needs at least 2 PDFs |
| 400 | MISSING_PDF | No PDF in the request |
| 400 | TOO_MANY_FILES | More than 20 files (100 for a job) |
| 400 | PDF_INPUT_TOO_LARGE | Over 4 MB instant, 150 MB per upload or 150 MB per job |
| 400 | PDF_PAGE_LIMIT_EXCEEDED | More than 500 pages processed (5000 for a job) |
| 400 | INVALID_PAGE_RANGE | A page range can't be read; details.token shows the part |
| 400 | PAGE_OUT_OF_RANGE | A page past the end; details.total_pages has the count |
| 400 | INVALID_SPLIT_MODE | split.mode missing or unknown |
| 400 | OUTPUT_COUNT_EXCEEDED | The split would make more than 100 files (500 for a job) |
| 400 | OUTPUT_TOO_LARGE | The output files would be too large in total |
| 400 | INVALID_PARAMETER | A field has the wrong shape; for protect, also passwords and permissions that would not do what they say (details.parameter) |
| 400 | PDF_PASSWORD_REQUIRED | Unlock was sent without password |
| 403 | PDF_TOOLS_NOT_ENABLED | The PDF tools aren't enabled on the plan |
| 403 | INSUFFICIENT_CREDITS | Not enough allowance or credits for the operation |
| 403 | FORBIDDEN | The s3_key or source_key doesn't belong to your account |
| 404 | UPLOAD_NOT_FOUND | Nothing was uploaded to that s3_key or source_key, or it expired |
| 408 | PDFOP_TIMEOUT | An instant request took longer than 25 seconds; for merge and split, use a job |
| 409 | PDF_JOB_ALREADY_ACTIVE | 2 merge/split jobs are already running |
| 413 | — | The request is larger than the API accepts (about 6 MB); use an upload |
| 422 | PDF_ENCRYPTED | A file is password-protected or encrypted. Unlock it first |
| 422 | PDF_PASSWORD_INCORRECT | Unlock: the password is wrong, or the PDF needs its owner password (details.needs) |
| 422 | PDF_NOT_ENCRYPTED | Unlock: the PDF has no password protection |
| 422 | PDF_ENCRYPTION_UNSUPPORTED | Unlock: the PDF is protected with a certificate, not a password |
| 422 | PDF_SECURITY_FAILED | Protect or unlock: the file could not be rewritten, usually because it is damaged |
| 422 | INVALID_PDF | A file is damaged, truncated or not a PDF |
| 422 | PDF_NO_PAGES | A file has no pages |
| 422 | PDF_TOO_COMPLEX | A file is too large or complex to process |
input_index and filename in details identify which file caused the error.
Many PDFs are encrypted even though they open without a password — bank statements and exports from document systems often are. Their pages can't be copied without decrypting them, so merge and split refuse them with PDF_ENCRYPTED rather than producing blank pages. Unlock the file with POST /pdf/unlock and its password, then use the unlocked copy.
What Is and Isn't Kept
| Result | |
|---|---|
| Page content, size, rotation | Kept exactly |
| Links and annotations on a page | Kept |
| Fillable form fields | Shown, no longer fillable — warning FORM_FIELDS_NOT_PRESERVED |
| Bookmarks | Not kept — warning BOOKMARKS_NOT_PRESERVED |
| Digital signatures | No longer valid — warning SIGNATURE_INVALIDATED |
Protect and unlock rewrite the file without changing its pages, forms or bookmarks. Digital signatures stop validating (SIGNATURE_INVALIDATED), and a damaged file that had to be repaired first is reported with SOURCE_REPAIRED.
Warnings are returned in X-PDF-Warnings (comma-separated), warnings in JSON responses, and on each row from GET /jobs/{job_id}/files. They never fail the request.
Complete Examples
cURL — split a large PDF with a job
API=https://api.podpdf.com
KEY=your_api_key_here
FILE=payslips.pdf
# 1. Upload URL
UPLOAD=$(curl -s -X POST $API/pdf/upload-url -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"content_length_bytes\": $(wc -c < $FILE)}")
# 2. Upload
curl -s -X PUT "$(echo $UPLOAD | jq -r .upload_url)" -H "Content-Type: application/pdf" --data-binary @$FILE
# 3. Submit
JOB=$(curl -s -X POST $API/pdf/jobs -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"operation\": \"split\", \"inputs\": [{\"s3_key\": \"$(echo $UPLOAD | jq -r .s3_key)\"}], \"split\": {\"mode\": \"each_page\"}}" | jq -r .job_id)
# 4. Wait
while STATUS=$(curl -s $API/jobs/$JOB -H "X-API-Key: $KEY" | jq -r .status); [ "$STATUS" = queued ] || [ "$STATUS" = processing ]; do
sleep 3
done
echo "Job $JOB finished: $STATUS"
# 5. Download everything as a ZIP
curl -s -o payslips.zip "$(curl -s $API/jobs/$JOB/download -H "X-API-Key: $KEY" | jq -r .download_url)"
cURL — protect a large PDF, then unlock it
API=https://api.podpdf.com
KEY=your_api_key_here
FILE=scan.pdf
# 1. Upload
UPLOAD=$(curl -s -X POST $API/pdf/upload-url -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"content_length_bytes\": $(wc -c < $FILE)}")
curl -s -X PUT "$(echo $UPLOAD | jq -r .upload_url)" -H "Content-Type: application/pdf" --data-binary @$FILE
# 2. Protect: no printing, no copying, and a password to open
RESULT=$(curl -s -X POST $API/pdf/protect -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"source_key\": \"$(echo $UPLOAD | jq -r .s3_key)\", \"filename\": \"$FILE\", \"user_password\": \"correct-horse-battery\", \"owner_password\": \"a-different-one\", \"permissions\": {\"print\": \"none\", \"copy\": false}}")
curl -s -o scan-protected.pdf "$(echo $RESULT | jq -r .download_url)"
# 3. Unlock a small protected PDF again
curl -s -X POST $API/pdf/unlock -H "X-API-Key: $KEY" \
-F "file=@payslip-protected.pdf" -F "password=correct-horse-battery" -o payslip.pdf
JavaScript (Node 18+)
const API = process.env.PODPDF_API_URL || 'https://api.podpdf.com';
const KEY = process.env.PODPDF_API_KEY;
/** Merge PDFs (Buffers) in order; pages is an optional page range per file */
async function mergePdfs(files) {
const form = new FormData();
for (const { buffer, name } of files) {
form.append('files', new Blob([buffer], { type: 'application/pdf' }), name);
}
form.append('page_ranges', JSON.stringify(files.map((file) => file.pages ?? null)));
const response = await fetch(`${API}/pdf/merge`, { method: 'POST', headers: { 'X-API-Key': KEY }, body: form });
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
return { pdf: Buffer.from(await response.arrayBuffer()), pages: Number(response.headers.get('x-pdf-pages')) };
}
/** Split a small PDF; returns [{ name, pages, download_url }] */
async function splitPdf(buffer, split) {
const form = new FormData();
form.append('file', new Blob([buffer], { type: 'application/pdf' }), 'document.pdf');
form.append('split', JSON.stringify(split));
form.append('store', 'true');
const response = await fetch(`${API}/pdf/split`, { method: 'POST', headers: { 'X-API-Key': KEY }, body: form });
const body = await response.json();
if (!response.ok) throw new Error(JSON.stringify(body));
return body.outputs;
}
/** Split a large PDF with an upload and a background job */
async function splitLargePdf(buffer, split) {
const json = { 'X-API-Key': KEY, 'Content-Type': 'application/json' };
const upload = await (await fetch(`${API}/pdf/upload-url`, {
method: 'POST', headers: json, body: JSON.stringify({ content_length_bytes: buffer.length }),
})).json();
await fetch(upload.upload_url, { method: 'PUT', headers: { 'Content-Type': 'application/pdf' }, body: buffer });
const { job_id: jobId } = await (await fetch(`${API}/pdf/jobs`, {
method: 'POST', headers: json, body: JSON.stringify({ operation: 'split', inputs: [{ s3_key: upload.s3_key }], split }),
})).json();
let job;
do {
await new Promise((resolve) => setTimeout(resolve, 3000));
job = await (await fetch(`${API}/jobs/${jobId}`, { headers: { 'X-API-Key': KEY } })).json();
} while (job.status === 'queued' || job.status === 'processing');
if (job.status !== 'completed') throw new Error(`${job.error_code}: ${job.error_message}`);
return (await (await fetch(`${API}/jobs/${jobId}/files`, { headers: { 'X-API-Key': KEY } })).json()).files;
}
Python
import json
import requests
API = 'https://api.podpdf.com'
HEADERS = {'X-API-Key': 'your_api_key_here'}
# Merge two files, the second from page 2
merged = requests.post(
f'{API}/pdf/merge',
headers=HEADERS,
files=[('files', open('contract.pdf', 'rb')), ('files', open('appendix.pdf', 'rb'))],
data={'page_ranges': json.dumps([None, '2-end'])},
)
merged.raise_for_status()
open('merged.pdf', 'wb').write(merged.content)
# Split into files of 10 pages each
split = requests.post(
f'{API}/pdf/split',
headers=HEADERS,
files={'file': open('report.pdf', 'rb')},
data={'split': json.dumps({'mode': 'every_n', 'every_n': 10})},
)
split.raise_for_status()
for output in split.json()['outputs']:
open(output['name'], 'wb').write(requests.get(output['download_url']).content)
Billing
Merge, split, protect and unlock are billed once per operation, not per input or output file. The price is your plan's price_per_pdf_operation (a quarter of price_per_pdf when not set), multiplied by the number of pages processed:
| Pages processed | Multiplier |
|---|---|
| 1–50 | ×1 |
| 51–250 | ×2 |
| 251–1000 | ×4 |
| 1001 and more | ×8 |
Pages processed are the selected pages for a merge and the document's pages for a split, protect or unlock. /pdf/inspect is free, and failed operations aren't charged — including an unlock with the wrong password.
Your PDF usage count goes up by the number of files produced: a merge adds 1, a split into 24 files adds 24, even though it's charged once. Operations are charged to your subscription allowance first, then your credits. A background job needs enough allowance or credits for the highest band when it's submitted; it's charged at the actual band when it finishes.
Limits
Instant (/pdf/merge, /pdf/split) | Background job (/pdf/jobs) | |
|---|---|---|
| Upload size | 4 MB in total | 150 MB per file, 150 MB in total |
| Files in a merge | 20 | 100 |
| Pages processed | 500 | 5000 |
| Files from one split | 100 | 500 |
| Total output size | 300 MB | 1 GB |
| Time limit | 25 seconds | 15 minutes |
| Running at once | — | 2 jobs per account |
| Output retention | 7 days | 7 days |
/pdf/protect and /pdf/unlock take one PDF: up to 4 MB in the request, or up to 150 MB and 5000 pages by source_key, within 25 seconds either way.
See Limits for every limit across the API.
Next Steps
- Merge and split guide — choosing a path, page ranges and common workflows
- Jobs API — status, per-file results and downloads
- Webhooks — get notified when a job finishes