Merge files
Join several PDFs into one through the API, in the order you choose, with the limits of each plan.
Before you start
- An API key with the
tools:runscope to upload and run, andjobs:readto follow the run and download the result. Authentication shows how to make one. - A Team, Business or Enterprise account. The API is included in those plans. The largest file and the number of files in one run follow the plan: for the Team plan, 250 MB and 50. The rate limits page lists every plan.
- The tool id is
merge.GET https://api.pdfloft.com/v1/tools/mergereturns the files and options it takes, and the Tools and options page lists them all.
The whole script
The script uploads three PDFs, starts a run of merge, waits for it and saves the first result. Upload and run are separate calls: POST /v1/files returns a file id, and POST /v1/tools/merge/runs takes the ids. The Idempotency-Key header lets you send the run call again after a network failure without starting a second run. A run is queued, running, then succeeded, failed or cancelled.
FILE0=$(curl -s -X POST https://api.pdfloft.com/v1/files \
-H "Authorization: Bearer $PDFLOFT_API_KEY" \
-H "Content-Type: application/pdf" \
--data-binary @cover.pdf | jq -r .id)
FILE1=$(curl -s -X POST https://api.pdfloft.com/v1/files \
-H "Authorization: Bearer $PDFLOFT_API_KEY" \
-H "Content-Type: application/pdf" \
--data-binary @body.pdf | jq -r .id)
FILE2=$(curl -s -X POST https://api.pdfloft.com/v1/files \
-H "Authorization: Bearer $PDFLOFT_API_KEY" \
-H "Content-Type: application/pdf" \
--data-binary @appendix.pdf | jq -r .id)
JOB=$(curl -s -X POST https://api.pdfloft.com/v1/tools/merge/runs \
-H "Authorization: Bearer $PDFLOFT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"files":["'"$FILE0"'","'"$FILE1"'","'"$FILE2"'"],"options":{}}' | jq -r .id)
until [ "$(curl -s https://api.pdfloft.com/v1/jobs/$JOB -H "Authorization: Bearer $PDFLOFT_API_KEY" | jq -r .status)" = "succeeded" ]; do sleep 1; done
URL=$(curl -s https://api.pdfloft.com/v1/jobs/$JOB/results/0 -H "Authorization: Bearer $PDFLOFT_API_KEY" | jq -r .url)
curl -s -o merged.pdf "$URL"import { readFile, writeFile } from 'node:fs/promises';
const apiKey = '<your API key>'; // read it from your secret store, never from source
const headers = { Authorization: `Bearer ${apiKey}` };
async function upload(path, contentType) {
const response = await fetch('https://api.pdfloft.com/v1/files', {
method: 'POST',
headers: { ...headers, 'Content-Type': contentType },
body: await readFile(path),
});
return response.json();
}
const file0 = await upload('cover.pdf', 'application/pdf');
const file1 = await upload('body.pdf', 'application/pdf');
const file2 = await upload('appendix.pdf', 'application/pdf');
let job = await fetch('https://api.pdfloft.com/v1/tools/merge/runs', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ files: [file0.id, file1.id, file2.id], options: {} }),
}).then((r) => r.json());
while (job.status === 'queued' || job.status === 'running') {
await new Promise((resolve) => setTimeout(resolve, 1000));
job = await fetch(`https://api.pdfloft.com/v1/jobs/${job.id}`, { headers }).then((r) => r.json());
}
if (job.status !== 'succeeded') throw new Error(`The run ended as ${job.status}: ${job.error}`);
const link = await fetch(`https://api.pdfloft.com/v1/jobs/${job.id}/results/0`, { headers }).then((r) => r.json());
await writeFile('merged.pdf', Buffer.from(await fetch(link.url).then((r) => r.arrayBuffer())));import os
import time
import uuid
import requests
headers = {"Authorization": f"Bearer {os.environ['PDFLOFT_API_KEY']}"}
def upload(path, content_type):
with open(path, "rb") as handle:
return requests.post("https://api.pdfloft.com/v1/files", headers={**headers, "Content-Type": content_type}, data=handle).json()
file0 = upload("cover.pdf", "application/pdf")
file1 = upload("body.pdf", "application/pdf")
file2 = upload("appendix.pdf", "application/pdf")
job = requests.post(
"https://api.pdfloft.com/v1/tools/merge/runs",
headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
json={"files": [file0["id"], file1["id"], file2["id"]], "options": {}},
).json()
while job["status"] in ("queued", "running"):
time.sleep(1)
job = requests.get(f"https://api.pdfloft.com/v1/jobs/{job['id']}", headers=headers).json()
if job["status"] != "succeeded":
raise RuntimeError(f"The run ended as {job['status']}: {job['error']}")
link = requests.get(f"https://api.pdfloft.com/v1/jobs/{job['id']}/results/0", headers=headers).json()
open("merged.pdf", "wb").write(requests.get(link["url"]).content)Order and limits
The files are joined in the order of the files array. A merge needs at least two files and takes at most the files per run of the plan (for example 25 on Pro and 50 on Team). Merging three files into one PDF counts one task, because a task is one output file.
To join the files of a folder in a fixed order, sort the names before you upload. To combine files of more than one run, save each result as a document with POST /v1/documents and merge the downloads, or merge the result files of the earlier runs by uploading them again.
If a call fails
run.too_few_files: a merge needs two files or more. The details give the minimum.run.too_many_files: more files than one run takes on the plan. The details give the limit; merge in two steps.file.type_refused: a file is not a PDF. Convert it first, as in the guide to converting a file.- Every error code is listed on the errors page, and a run that failed carries its code in the
errorfield of the run.