How to Merge PDFs with the PodPDF API
Combine PDFs into one file in your order, with page ranges per file: in the dashboard, with one API call, or as a background job for large bundles.
Merging PDFs sounds like the simplest document task there is, until you do it at work. The invoice is three pages but you only need the first one. The contract has to come before the appendix, not after it. The scanned receipts are 40 MB. And the whole thing needs to happen automatically every time a deal closes.
PodPDF merges PDFs in the dashboard and through the API. You choose the order, pick the pages you need from each file, and get one PDF back.
What Merge Does
You send two or more PDFs. PodPDF:
- Checks every file — damaged files, files that aren’t PDFs, and password-protected files are refused with a clear message naming the file
- Takes the pages you selected from each file, in the order you listed the files
- Copies them into one new PDF without re-rendering: page sizes, rotation and content stay exactly as they were
- Returns the merged file directly, or stores it and gives you a download link
Nothing is rasterized or recompressed, which is why a merge of hundreds of pages takes seconds.
Merge in the Dashboard
Open PDF Tools → Merge in the dashboard and drop your files in.
- Each file is checked as soon as you add it, and its page count appears next to it
- Use the arrows to reorder files, or Sort by name for numbered files like
statement-01.pdf,statement-02.pdf - Leave Pages empty to use every page, or enter a range such as
1-3,7 - Give the output a name and click Merge PDFs
Small merges finish immediately with a preview. Larger ones upload first and run in the background; you can leave the page and pick up the file from Jobs.
Merge with One API Call
For files up to 4 MB in total, POST /pdf/merge returns the merged PDF in the response. Send files as multipart form data — it is the most efficient format for binary files:
curl -X POST https://api.podpdf.com/pdf/merge \
-H "X-API-Key: YOUR_API_KEY" \
-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"}' \
-o deal-pack.pdf
Files are merged in the order of the files parts. page_ranges is optional; when present it has one entry per file, and null means every page. The response headers tell you what you got:
| Header | Meaning |
|---|---|
X-PDF-Pages | Pages in the merged PDF |
X-Job-Id | The job, visible in Jobs and through GET /jobs/{id} |
X-PDF-Warnings | Content that couldn’t be carried over (see below) |
JavaScript
import fs from 'node:fs';
const form = new FormData();
for (const name of ['contract.pdf', 'appendix.pdf', 'invoice.pdf']) {
form.append('files', new Blob([fs.readFileSync(name)], { type: 'application/pdf' }), name);
}
form.append('page_ranges', JSON.stringify([null, '2-end', '1']));
const response = await fetch('https://api.podpdf.com/pdf/merge', {
method: 'POST',
headers: { 'X-API-Key': process.env.PODPDF_API_KEY },
body: form,
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
fs.writeFileSync('deal-pack.pdf', Buffer.from(await response.arrayBuffer()));
Python
import json
import requests
files = [('files', (name, open(name, 'rb'), 'application/pdf'))
for name in ['contract.pdf', 'appendix.pdf', 'invoice.pdf']]
response = requests.post(
'https://api.podpdf.com/pdf/merge',
headers={'X-API-Key': 'YOUR_API_KEY'},
files=files,
data={'page_ranges': json.dumps([None, '2-end', '1'])},
)
response.raise_for_status()
open('deal-pack.pdf', 'wb').write(response.content)
If your platform can only send JSON, use inputs with base64 data instead: {"inputs": [{"pdf_base64": "...", "filename": "contract.pdf", "pages": "1-3"}]}. Base64 is a third larger than the file, so keep multipart for anything near the size limit.
Page Ranges
Ranges use the same syntax everywhere in PodPDF:
| Range | Pages |
|---|---|
3 | page 3 |
1-3 | pages 1, 2, 3 |
8-end | page 8 to the last page |
5,1 | page 5, then page 1 |
1,1 | page 1 twice |
Pages are used in the order you write them. A range that runs backwards (5-3) or past the end of the document is rejected with an error that names the file and the part that’s wrong — PodPDF never guesses what you meant.
Large Bundles: Upload and Queue a Job
Merges above 4 MB run as background jobs, with up to 100 files and 150 MB in total. Upload each file to its own signed URL, then submit the job:
# 1. Get an upload URL for each file
curl -X POST https://api.podpdf.com/pdf/upload-url \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"content_length_bytes": 48211934}'
# 2. Upload the file to the returned upload_url
curl -X PUT "UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @exhibits.pdf
# 3. Queue the merge with the returned s3_key values
curl -X POST https://api.podpdf.com/pdf/jobs \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"operation": "merge", "inputs": [{"s3_key": "pdf-uploads/...", "filename": "exhibits.pdf"}, {"s3_key": "pdf-uploads/...", "filename": "brief.pdf", "pages": "1-12"}]}'
The response is 202 with a job_id. Poll GET /jobs/{job_id} until the status is completed, then call GET /jobs/{job_id}/download for a fresh download link — or subscribe a webhook to job.completed and skip polling. Up to 2 merge or split jobs can run at the same time per account.
What Isn’t Carried Over
Merging copies pages. Some things live outside the pages and don’t come along:
- Fillable form fields — the merged file shows the page, but the fields are no longer editable
- Bookmarks (the document outline)
- Digital signatures — any change to a signed PDF invalidates the signature
When an input has any of these, the result includes a warning (FORM_FIELDS_NOT_PRESERVED, BOOKMARKS_NOT_PRESERVED, SIGNATURE_INVALIDATED) so it never comes as a surprise.
Password-Protected PDFs
Bank statements and documents exported from many business systems are often encrypted, even when they open without a password. Merging an encrypted file without decrypting it produces pages that look blank. PodPDF refuses these files with PDF_ENCRYPTED and names the file. Remove the protection, then merge again.
Common Errors
| Code | What to do |
|---|---|
PDF_ENCRYPTED | Remove the password protection from the named file |
INVALID_PDF | The named file is damaged or isn’t a PDF — re-export it |
PAGE_OUT_OF_RANGE | A range goes past the end of the file; details.total_pages has the real count |
TOO_MANY_FILES | Split the merge into smaller batches, or use a job |
PDF_INPUT_TOO_LARGE | Use the upload URL and job flow |
Pricing
A merge is billed once per operation, based on the number of pages processed — not per input file. Merging 20 one-page invoices costs the same as merging 2. See pricing for current rates.
Get Started
- Try it now in the dashboard: PDF Tools → Merge
- Read the API reference
- Learn more about merging PDFs with PodPDF, or go the other way and split a PDF