Skip to main content

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.

EndpointWhat it does
POST /pdf/mergeMerge up to 20 PDFs (4 MB in total) and get the result immediately
POST /pdf/splitSplit a PDF of up to 4 MB into up to 100 files
POST /pdf/protectAdd a password to a PDF, with optional restrictions on printing, copying and editing
POST /pdf/unlockRemove a PDF's password, given that password
POST /pdf/inspectPage count and password protection, free
POST /pdf/upload-urlUpload a large PDF (up to 150 MB)
POST /pdf/jobsMerge 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
Instant or background

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.

Plan requirement

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.

RangePages
33
1-31, 2, 3
8-end8 to the last page
1,3,5-71, 3, 5, 6, 7
5,15, then 1
2,22 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)

FieldRequiredDescription
filesYesOne part per PDF, in merge order
page_rangesNoJSON array with one entry per file: a page range, or null for every page
optionsNoJSON object, see below
storeNotrue 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.

OptionDescription
output_filenameName of the merged file, without .pdf (default merged)
titleDocument 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)

FieldRequiredDescription
fileYesThe PDF
splitYesJSON object, see modes below
optionsNoJSON object: filename_pattern
storeNotrue 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​

modeSettingsOutput
extractpages: a page rangeOne PDF with those pages
rangesranges: array of page rangesOne PDF per range (ranges may overlap)
every_nevery_n: whole number ≥ 1A 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.

Output size

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_passwordNeeded to open the PDF
owner_passwordNeeded to change or remove the protection. Set it together with permissions to restrict what readers can do

Request​

Multipart (recommended)

FieldRequiredDescription
fileYesThe PDF, up to 4 MB. For a larger file send JSON with source_key, below
user_passwordOne of the twoThe password to open the PDF
owner_passwordOne of the twoThe password that owns the PDF. Required when permissions restricts anything, and must differ from user_password
permissionsNoJSON object, see below
optionsNoJSON object: output_filename (without .pdf; default is the input name with -protected)
storeNotrue 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.

KeyValuesDefault
printfull, low (low resolution only), nonefull
copytrue, false — copying text and imagestrue
modifytrue, false — editing the documenttrue
annotatetrue, false — comments and annotationstrue
fill_formstrue, false — filling in form fieldstrue
assembletrue, false — inserting, deleting and rotating pagestrue

Screen readers are always allowed to read the text.

Which passwords to send​

You sendResult
user_password onlyThe PDF needs that password to open. Nothing is restricted
user_password, owner_password and permissionsThe PDF needs user_password to open, with the restrictions applied. owner_password lifts them
owner_password and permissionsThe 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": []
}
Your passwords are not kept

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.

Protect last

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 PDFpassword must be
Needs a password to openThat password, or the owner password
Opens without a password, but cannot be printed, copied or editedThe owner password

Request​

Multipart (recommended)

FieldRequiredDescription
fileYesThe protected PDF, up to 4 MB. For a larger file send JSON with source_key
passwordYesThe PDF's password
optionsNoJSON object: output_filename (without .pdf; default is the input name with -unlocked)
storeNotrue 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.

FieldRequiredDescription
operationYesmerge or split
inputsYes[{ "s3_key", "filename", "pages" }] — 2 to 100 for merge, exactly 1 for split. pages (merge only) is a page range
splitFor splitSame object as /pdf/split
optionsNooutput_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}/files lists every output file
  • GET /jobs/{job_id}/files/{index}/download returns a link to one file
  • GET /jobs/{job_id}/download returns 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 }
}
}
StatusCodeMeaning
400MISSING_PDFSA merge needs at least 2 PDFs
400MISSING_PDFNo PDF in the request
400TOO_MANY_FILESMore than 20 files (100 for a job)
400PDF_INPUT_TOO_LARGEOver 4 MB instant, 150 MB per upload or 150 MB per job
400PDF_PAGE_LIMIT_EXCEEDEDMore than 500 pages processed (5000 for a job)
400INVALID_PAGE_RANGEA page range can't be read; details.token shows the part
400PAGE_OUT_OF_RANGEA page past the end; details.total_pages has the count
400INVALID_SPLIT_MODEsplit.mode missing or unknown
400OUTPUT_COUNT_EXCEEDEDThe split would make more than 100 files (500 for a job)
400OUTPUT_TOO_LARGEThe output files would be too large in total
400INVALID_PARAMETERA field has the wrong shape; for protect, also passwords and permissions that would not do what they say (details.parameter)
400PDF_PASSWORD_REQUIREDUnlock was sent without password
403PDF_TOOLS_NOT_ENABLEDThe PDF tools aren't enabled on the plan
403INSUFFICIENT_CREDITSNot enough allowance or credits for the operation
403FORBIDDENThe s3_key or source_key doesn't belong to your account
404UPLOAD_NOT_FOUNDNothing was uploaded to that s3_key or source_key, or it expired
408PDFOP_TIMEOUTAn instant request took longer than 25 seconds; for merge and split, use a job
409PDF_JOB_ALREADY_ACTIVE2 merge/split jobs are already running
413—The request is larger than the API accepts (about 6 MB); use an upload
422PDF_ENCRYPTEDA file is password-protected or encrypted. Unlock it first
422PDF_PASSWORD_INCORRECTUnlock: the password is wrong, or the PDF needs its owner password (details.needs)
422PDF_NOT_ENCRYPTEDUnlock: the PDF has no password protection
422PDF_ENCRYPTION_UNSUPPORTEDUnlock: the PDF is protected with a certificate, not a password
422PDF_SECURITY_FAILEDProtect or unlock: the file could not be rewritten, usually because it is damaged
422INVALID_PDFA file is damaged, truncated or not a PDF
422PDF_NO_PAGESA file has no pages
422PDF_TOO_COMPLEXA file is too large or complex to process

input_index and filename in details identify which file caused the error.

Password-protected PDFs

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, rotationKept exactly
Links and annotations on a pageKept
Fillable form fieldsShown, no longer fillable — warning FORM_FIELDS_NOT_PRESERVED
BookmarksNot kept — warning BOOKMARKS_NOT_PRESERVED
Digital signaturesNo 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+)​

merge-split.js
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 processedMultiplier
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 size4 MB in total150 MB per file, 150 MB in total
Files in a merge20100
Pages processed5005000
Files from one split100500
Total output size300 MB1 GB
Time limit25 seconds15 minutes
Running at once—2 jobs per account
Output retention7 days7 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​