Merge PDF documents
Combine several PDF documents into one, in the order you supply them. Send as many files as you need in a single call; nothing is stored afterwards.
Combine several PDF documents into one. The files are merged in the exact order they appear in the request, so you control the final page sequence. Supply as many files as you need in a single call. The API is stateless: your document is processed in-region and never stored.
Endpoint
/v1/merge_documentsAvailable in every region. See Regions & data residency for routing and data residency.
| Region | URL |
|---|---|
| Global | https://api.pdfblocks.com/v1/merge_documents |
| United States | https://us.api.pdfblocks.com/v1/merge_documents |
| US HIPAA | https://hipaa.api.pdfblocks.com/v1/merge_documents |
| European Union | https://eu.api.pdfblocks.com/v1/merge_documents |
| United Kingdom | https://uk.api.pdfblocks.com/v1/merge_documents |
| Canada | https://ca.api.pdfblocks.com/v1/merge_documents |
| Australia | https://au.api.pdfblocks.com/v1/merge_documents |
| Japan | https://jp.api.pdfblocks.com/v1/merge_documents |
| India | https://in.api.pdfblocks.com/v1/merge_documents |
| Brazil | https://br.api.pdfblocks.com/v1/merge_documents |
Authentication
Authenticate every request with your secret API key in the X-API-Key header,
over HTTPS. Create and manage keys from the
dashboard. See
Authentication for details.
Request
The endpoint accepts a multipart/form-data request body.
filefile[]requiredThe input PDF documents, sent as repeated file parts. Provide at least one;
supply as many files as you need in a single request. The documents are merged
in the exact order the parts appear in the request. See
Working with files for how to send multiple
file parts.
Examples
Merge three PDFs into one, in order:
curl https://api.pdfblocks.com/v1/merge_documents \
-H 'X-API-Key: your_api_key' \
-F file=@chapter-1.pdf \
-F file=@chapter-2.pdf \
-F file=@chapter-3.pdf \
-o merged.pdf# pip install requests
import requests
files = [
('file', open('chapter-1.pdf', 'rb')),
('file', open('chapter-2.pdf', 'rb')),
('file', open('chapter-3.pdf', 'rb')),
]
response = requests.post(
'https://api.pdfblocks.com/v1/merge_documents',
headers={'X-API-Key': 'your_api_key'},
files=files,
)
response.raise_for_status()
with open('merged.pdf', 'wb') as output:
output.write(response.content)// Node.js 18+
import { readFile, writeFile } from 'node:fs/promises';
const body = new FormData();
body.append('file', new Blob([await readFile('chapter-1.pdf')]), 'chapter-1.pdf');
body.append('file', new Blob([await readFile('chapter-2.pdf')]), 'chapter-2.pdf');
body.append('file', new Blob([await readFile('chapter-3.pdf')]), 'chapter-3.pdf');
const response = await fetch('https://api.pdfblocks.com/v1/merge_documents', {
method: 'POST',
headers: { 'X-API-Key': 'your_api_key' },
body,
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
await writeFile('merged.pdf', Buffer.from(await response.arrayBuffer()));<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
use GuzzleHttp\Client;
// Repeat the `file` part once per document: they merge in the order sent.
$response = (new Client())->post('https://api.pdfblocks.com/v1/merge_documents', [
'headers' => ['X-API-Key' => 'your_api_key'],
'multipart' => [
['name' => 'file', 'contents' => fopen('chapter-1.pdf', 'r'), 'filename' => 'chapter-1.pdf'],
['name' => 'file', 'contents' => fopen('chapter-2.pdf', 'r'), 'filename' => 'chapter-2.pdf'],
['name' => 'file', 'contents' => fopen('chapter-3.pdf', 'r'), 'filename' => 'chapter-3.pdf'],
],
]);
if ($response->getStatusCode() === 200) {
file_put_contents('merged.pdf', $response->getBody());
}# gem install http
require 'http'
response = HTTP
.headers('X-API-Key' => 'your_api_key')
.post('https://api.pdfblocks.com/v1/merge_documents', form: {
file: [
HTTP::FormData::File.new('chapter-1.pdf'),
HTTP::FormData::File.new('chapter-2.pdf'),
HTTP::FormData::File.new('chapter-3.pdf'),
],
})
File.write('merged.pdf', response.body) if response.status.success?package main
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
var buf bytes.Buffer
form := multipart.NewWriter(&buf)
for _, name := range []string{"chapter-1.pdf", "chapter-2.pdf", "chapter-3.pdf"} {
file, _ := os.Open(name)
part, _ := form.CreateFormFile("file", name)
io.Copy(part, file)
file.Close()
}
form.Close()
req, _ := http.NewRequest("POST", "https://api.pdfblocks.com/v1/merge_documents", &buf)
req.Header.Set("Content-Type", form.FormDataContentType())
req.Header.Set("X-API-Key", "your_api_key")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := os.Create("merged.pdf")
defer out.Close()
io.Copy(out, res.Body)
}using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "your_api_key");
using var form = new MultipartFormDataContent
{
{ new ByteArrayContent(File.ReadAllBytes("chapter-1.pdf")), "file", "chapter-1.pdf" },
{ new ByteArrayContent(File.ReadAllBytes("chapter-2.pdf")), "file", "chapter-2.pdf" },
{ new ByteArrayContent(File.ReadAllBytes("chapter-3.pdf")), "file", "chapter-3.pdf" },
};
var response = await client.PostAsync(
"https://api.pdfblocks.com/v1/merge_documents", form);
response.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync(
"merged.pdf", await response.Content.ReadAsByteArrayAsync());Response
On success, the response is 200 OK with the merged PDF as the body:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 96124The output is a single PDF whose page count is the sum of the inputs’, laid out in request order. Stream the body straight to a file, as the examples above do; nothing is stored on our side.
Errors
Failed requests return an application/problem+json body. The most common one
for this endpoint is a 400, returned when one of the file parts isn’t a
readable PDF. The errors object names the field:
{
"type": "https://www.pdfblocks.com/docs/api/v1/error/400",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"file": ["Could not parse the PDF document. The file may be invalid or corrupt."]
}
}A missing or invalid X-API-Key returns a 401. See
Errors for every status code and the full response shape.
Recipes
Common variations. Expand one to see it in every language.
Put a cover page in front of a report
curl https://api.pdfblocks.com/v1/merge_documents \
-H 'X-API-Key: your_api_key' \
-F file=@cover.pdf \
-F file=@report.pdf \
-o report-with-cover.pdfimport requests
files = [
('file', open('cover.pdf', 'rb')),
('file', open('report.pdf', 'rb')),
]
response = requests.post(
'https://api.pdfblocks.com/v1/merge_documents',
headers={'X-API-Key': 'your_api_key'},
files=files,
)
response.raise_for_status()
with open('report-with-cover.pdf', 'wb') as output:
output.write(response.content)import { readFile, writeFile } from 'node:fs/promises';
const body = new FormData();
body.append('file', new Blob([await readFile('cover.pdf')]), 'cover.pdf');
body.append('file', new Blob([await readFile('report.pdf')]), 'report.pdf');
const response = await fetch('https://api.pdfblocks.com/v1/merge_documents', {
method: 'POST',
headers: { 'X-API-Key': 'your_api_key' },
body,
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
await writeFile('report-with-cover.pdf', Buffer.from(await response.arrayBuffer()));<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$response = (new Client())->post('https://api.pdfblocks.com/v1/merge_documents', [
'headers' => ['X-API-Key' => 'your_api_key'],
'multipart' => [
['name' => 'file', 'contents' => fopen('cover.pdf', 'r'), 'filename' => 'cover.pdf'],
['name' => 'file', 'contents' => fopen('report.pdf', 'r'), 'filename' => 'report.pdf'],
],
]);
if ($response->getStatusCode() === 200) {
file_put_contents('report-with-cover.pdf', $response->getBody());
}require 'http'
response = HTTP
.headers('X-API-Key' => 'your_api_key')
.post('https://api.pdfblocks.com/v1/merge_documents', form: {
file: [
HTTP::FormData::File.new('cover.pdf'),
HTTP::FormData::File.new('report.pdf'),
],
})
File.write('report-with-cover.pdf', response.body) if response.status.success?package main
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
var buf bytes.Buffer
form := multipart.NewWriter(&buf)
for _, name := range []string{"cover.pdf", "report.pdf"} {
file, _ := os.Open(name)
part, _ := form.CreateFormFile("file", name)
io.Copy(part, file)
file.Close()
}
form.Close()
req, _ := http.NewRequest("POST", "https://api.pdfblocks.com/v1/merge_documents", &buf)
req.Header.Set("Content-Type", form.FormDataContentType())
req.Header.Set("X-API-Key", "your_api_key")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
out, _ := os.Create("report-with-cover.pdf")
defer out.Close()
io.Copy(out, res.Body)
}using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "your_api_key");
using var form = new MultipartFormDataContent
{
{ new ByteArrayContent(File.ReadAllBytes("cover.pdf")), "file", "cover.pdf" },
{ new ByteArrayContent(File.ReadAllBytes("report.pdf")), "file", "report.pdf" },
};
var response = await client.PostAsync(
"https://api.pdfblocks.com/v1/merge_documents", form);
response.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync(
"report-with-cover.pdf", await response.Content.ReadAsByteArrayAsync());