Merge PDFs with the API
Combine multiple existing PDFs into one document. Send publicly accessible HTTPS URLs or Base64-encoded files in the required order, and receive the merged PDF or a temporary download link. Merge reports with their attachments or combine multiple invoices into one PDF.
Upload PDFs or add PDF URLs, arrange them in the order you need, and merge them without writing code.
Try in PlaygroundEndpoints
| Method | Endpoint | Successful response |
|---|---|---|
POST | /v1/tools/merge/direct | PDF bytes with Content-Type: application/pdf |
POST | /v1/tools/merge/sync | JSON with result metadata and a download URL when using default storage |
Both endpoints wait for merging to complete before responding.
Authentication
Use a conversion API key in the API-KEY header.
API-KEY: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Find your key on the API Credentials page in your Dashboard.
Merge PDFs from URLs
The examples combine two sample PDFs and save the result as merged.pdf. Replace the sample URLs with direct HTTPS URLs to your files. PDFs are merged in the order listed in inputs.
The examples use the Direct endpoint. To receive a download URL, replace /direct with /sync and read the response as JSON. The documentUrl field contains the download URL.
- Request body
- cURL
- Node.js
- Python
- PHP
- Java
- C#
- Go
- Rust
{
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"}
]
}
curl --fail --show-error \
'https://api.pdfbolt.com/v1/tools/merge/direct' \
-H 'API-KEY: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' \
-H 'Content-Type: application/json' \
-d '{
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"}
]
}' \
-o merged.pdf
Requires Node.js 18 or later.
const fs = require('fs');
async function mergePdfs() {
const requestData = {
inputs: [
{url: 'https://pdfbolt.com/samples/merge-sample-1.pdf'},
{url: 'https://pdfbolt.com/samples/merge-sample-2.pdf'}
]
};
const response = await fetch('https://api.pdfbolt.com/v1/tools/merge/direct', {
method: 'POST',
headers: {
'API-KEY': 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status} - ${errorText}`);
}
const pdfBuffer = await response.arrayBuffer();
fs.writeFileSync('merged.pdf', Buffer.from(pdfBuffer));
console.log('PDFs merged successfully');
}
mergePdfs().catch(console.error);
Install requests with pip install requests.
import requests
url = "https://api.pdfbolt.com/v1/tools/merge/direct"
headers = {
"API-KEY": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"Content-Type": "application/json"
}
data = {
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"}
]
}
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
with open('merged.pdf', 'wb') as f:
f.write(response.content)
print("PDFs merged successfully")
except requests.exceptions.HTTPError:
print(f"HTTP {response.status_code}")
print(f"Error Message: {response.text}")
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
Install Guzzle with composer require guzzlehttp/guzzle.
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$headers = [
'API-KEY' => 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
'Content-Type' => 'application/json',
];
$body = [
'inputs' => [
['url' => 'https://pdfbolt.com/samples/merge-sample-1.pdf'],
['url' => 'https://pdfbolt.com/samples/merge-sample-2.pdf'],
],
];
try {
$client = new Client();
$response = $client->post('https://api.pdfbolt.com/v1/tools/merge/direct', [
'headers' => $headers,
'json' => $body,
]);
file_put_contents('merged.pdf', (string) $response->getBody());
echo "PDFs merged successfully\n";
} catch (RequestException $e) {
if ($e->hasResponse()) {
echo "HTTP " . $e->getResponse()->getStatusCode() . "\n";
echo "Error Message: " . $e->getResponse()->getBody() . "\n";
} else {
echo "Error: " . $e->getMessage() . "\n";
}
}
Requires Java 17 or later.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
public class PDFBoltIntegration {
public static void main(String[] args) throws Exception {
String jsonBody = """
{
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"}
]
}
""";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.pdfbolt.com/v1/tools/merge/direct"))
.header("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() == 200) {
Files.write(Paths.get("merged.pdf"), response.body());
System.out.println("PDFs merged successfully");
} else {
System.err.println("HTTP " + response.statusCode());
System.err.println("Error Message: " + new String(response.body()));
}
}
}
Use a .NET 6 or later console project.
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class PDFBoltIntegration {
public static async Task Main(string[] args) {
using var client = new HttpClient();
var requestData = new {
inputs = new[] {
new {url = "https://pdfbolt.com/samples/merge-sample-1.pdf"},
new {url = "https://pdfbolt.com/samples/merge-sample-2.pdf"}
}
};
using var request = new HttpRequestMessage {
Method = HttpMethod.Post,
RequestUri = new Uri("https://api.pdfbolt.com/v1/tools/merge/direct"),
Content = new StringContent(
JsonSerializer.Serialize(requestData),
Encoding.UTF8,
"application/json"
)
};
request.Headers.Add("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
try {
using var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode) {
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"HTTP {(int)response.StatusCode}");
Console.WriteLine($"Error Message: {errorContent}");
return;
}
var pdfBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("merged.pdf", pdfBytes);
Console.WriteLine("PDFs merged successfully");
} catch (Exception ex) {
Console.WriteLine($"Error: {ex.Message}");
}
}
}
Requires Go 1.16 or later.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
data := map[string]interface{}{
"inputs": []map[string]interface{}{
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"},
},
}
jsonBody, err := json.Marshal(data)
if err != nil {
log.Fatal(err)
}
req, err := http.NewRequest("POST", "https://api.pdfbolt.com/v1/tools/merge/direct", bytes.NewReader(jsonBody))
if err != nil {
log.Fatal(err)
}
req.Header.Add("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("HTTP %d\n", resp.StatusCode)
fmt.Printf("Error Message: %s\n", string(body))
return
}
file, err := os.Create("merged.pdf")
if err != nil {
log.Fatal(err)
}
defer file.Close()
if _, err := io.Copy(file, resp.Body); err != nil {
log.Fatal(err)
}
fmt.Println("PDFs merged successfully")
}
Add these dependencies to Cargo.toml:
[dependencies]
reqwest = { version = "0.13", features = ["json"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
use reqwest::Client;
use serde_json::json;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let url = "https://api.pdfbolt.com/v1/tools/merge/direct";
let request_data = json!({
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"}
]
});
let response = client
.post(url)
.header("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
.header("Content-Type", "application/json")
.json(&request_data)
.send()
.await?;
if !response.status().is_success() {
println!("HTTP {}", response.status().as_u16());
let error_text = response.text().await?;
println!("Error Message: {}", error_text);
return Ok(());
}
let pdf_bytes = response.bytes().await?;
fs::write("merged.pdf", pdf_bytes)?;
println!("PDFs merged successfully");
Ok(())
}
Merge Local PDF Files
Encode each PDF as Base64 and use the encoded data in the inputs[].pdf fields shown below, without the data:application/pdf;base64, prefix.
- Request body
- cURL
- Node.js
- Python
- PHP
- Java
- C#
- Go
- Rust
{
"inputs": [
{"pdf": "BASE64_FIRST_PDF"},
{"pdf": "BASE64_SECOND_PDF"}
]
}
curl --fail --show-error \
'https://api.pdfbolt.com/v1/tools/merge/direct' \
-H 'API-KEY: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' \
-H 'Content-Type: application/json' \
-d '{
"inputs": [
{"pdf": "BASE64_FIRST_PDF"},
{"pdf": "BASE64_SECOND_PDF"}
]
}' \
-o merged.pdf
Requires Node.js 18 or later.
const fs = require('fs');
async function mergePdfs() {
const requestData = {
inputs: [
{pdf: 'BASE64_FIRST_PDF'},
{pdf: 'BASE64_SECOND_PDF'}
]
};
const response = await fetch('https://api.pdfbolt.com/v1/tools/merge/direct', {
method: 'POST',
headers: {
'API-KEY': 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status} - ${errorText}`);
}
const pdfBuffer = await response.arrayBuffer();
fs.writeFileSync('merged.pdf', Buffer.from(pdfBuffer));
console.log('PDFs merged successfully');
}
mergePdfs().catch(console.error);
Install requests with pip install requests.
import requests
url = "https://api.pdfbolt.com/v1/tools/merge/direct"
headers = {
"API-KEY": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"Content-Type": "application/json"
}
data = {
"inputs": [
{"pdf": "BASE64_FIRST_PDF"},
{"pdf": "BASE64_SECOND_PDF"}
]
}
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
with open('merged.pdf', 'wb') as f:
f.write(response.content)
print("PDFs merged successfully")
except requests.exceptions.HTTPError:
print(f"HTTP {response.status_code}")
print(f"Error Message: {response.text}")
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
Install Guzzle with composer require guzzlehttp/guzzle.
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$headers = [
'API-KEY' => 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
'Content-Type' => 'application/json',
];
$body = [
'inputs' => [
['pdf' => 'BASE64_FIRST_PDF'],
['pdf' => 'BASE64_SECOND_PDF'],
],
];
try {
$client = new Client();
$response = $client->post('https://api.pdfbolt.com/v1/tools/merge/direct', [
'headers' => $headers,
'json' => $body,
]);
file_put_contents('merged.pdf', (string) $response->getBody());
echo "PDFs merged successfully\n";
} catch (RequestException $e) {
if ($e->hasResponse()) {
echo "HTTP " . $e->getResponse()->getStatusCode() . "\n";
echo "Error Message: " . $e->getResponse()->getBody() . "\n";
} else {
echo "Error: " . $e->getMessage() . "\n";
}
}
Requires Java 17 or later.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
public class PDFBoltIntegration {
public static void main(String[] args) throws Exception {
String jsonBody = """
{
"inputs": [
{"pdf": "BASE64_FIRST_PDF"},
{"pdf": "BASE64_SECOND_PDF"}
]
}
""";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.pdfbolt.com/v1/tools/merge/direct"))
.header("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() == 200) {
Files.write(Paths.get("merged.pdf"), response.body());
System.out.println("PDFs merged successfully");
} else {
System.err.println("HTTP " + response.statusCode());
System.err.println("Error Message: " + new String(response.body()));
}
}
}
Use a .NET 6 or later console project.
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class PDFBoltIntegration {
public static async Task Main(string[] args) {
using var client = new HttpClient();
var requestData = new {
inputs = new[] {
new {pdf = "BASE64_FIRST_PDF"},
new {pdf = "BASE64_SECOND_PDF"}
}
};
using var request = new HttpRequestMessage {
Method = HttpMethod.Post,
RequestUri = new Uri("https://api.pdfbolt.com/v1/tools/merge/direct"),
Content = new StringContent(
JsonSerializer.Serialize(requestData),
Encoding.UTF8,
"application/json"
)
};
request.Headers.Add("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
try {
using var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode) {
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"HTTP {(int)response.StatusCode}");
Console.WriteLine($"Error Message: {errorContent}");
return;
}
var pdfBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("merged.pdf", pdfBytes);
Console.WriteLine("PDFs merged successfully");
} catch (Exception ex) {
Console.WriteLine($"Error: {ex.Message}");
}
}
}
Requires Go 1.16 or later.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
data := map[string]interface{}{
"inputs": []map[string]interface{}{
{"pdf": "BASE64_FIRST_PDF"},
{"pdf": "BASE64_SECOND_PDF"},
},
}
jsonBody, err := json.Marshal(data)
if err != nil {
log.Fatal(err)
}
req, err := http.NewRequest("POST", "https://api.pdfbolt.com/v1/tools/merge/direct", bytes.NewReader(jsonBody))
if err != nil {
log.Fatal(err)
}
req.Header.Add("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("HTTP %d\n", resp.StatusCode)
fmt.Printf("Error Message: %s\n", string(body))
return
}
file, err := os.Create("merged.pdf")
if err != nil {
log.Fatal(err)
}
defer file.Close()
if _, err := io.Copy(file, resp.Body); err != nil {
log.Fatal(err)
}
fmt.Println("PDFs merged successfully")
}
Add these dependencies to Cargo.toml:
[dependencies]
reqwest = { version = "0.13", features = ["json"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
use reqwest::Client;
use serde_json::json;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let url = "https://api.pdfbolt.com/v1/tools/merge/direct";
let request_data = json!({
"inputs": [
{"pdf": "BASE64_FIRST_PDF"},
{"pdf": "BASE64_SECOND_PDF"}
]
});
let response = client
.post(url)
.header("API-KEY", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
.header("Content-Type", "application/json")
.json(&request_data)
.send()
.await?;
if !response.status().is_success() {
println!("HTTP {}", response.status().as_u16());
let error_text = response.text().await?;
println!("Error Message: {}", error_text);
return Ok(());
}
let pdf_bytes = response.bytes().await?;
fs::write("merged.pdf", pdf_bytes)?;
println!("PDFs merged successfully");
Ok(())
}
Request Parameters
Set the Content-Type header to application/json.
inputs
Type: array of objects
Required: Yes
Details: An array of 2–20 PDF sources in merge order. Each item must contain either url or pdf, not both. You can mix both source types in the same request. All pages from each source are included.
Usage:
{
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"pdf": "BASE64_SECOND_PDF"}
]
}
inputs[].url
Type: string
Required: Yes, if pdf is omitted from the same item
Details: Direct, publicly accessible HTTPS URL of the input PDF. Maximum length: 2,048 characters.
URLs must not contain credentials or fragments such as #page=2. Redirects and private network addresses are not supported. Custom headers cannot be supplied when fetching the PDF.
inputs[].pdf
Type: string
Required: Yes, if url is omitted from the same item
Details: Base64-encoded contents of the input PDF, without the data:application/pdf;base64, prefix.
filename
Type: string
Required: No
Default Value: merge.pdf
Details: Output PDF filename in the Content-Disposition header. Maximum 255 characters. Allowed characters: A–Z, a–z, 0–9, ., _, and -. Provide the filename with or without the .pdf extension.
contentDisposition
Type: string
Required: No
Default Value: inline
Details: Specifies whether the browser should display or download the PDF.
"inline": Display the PDF in the browser, if supported."attachment": Prompt a download.
Usage:
{
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"}
],
"filename": "combined-report.pdf",
"contentDisposition": "attachment"
}
customS3PresignedUrl
Type: string
Required: No
Details: Uploads the merged PDF to your S3-compatible bucket using an HTTPS pre-signed PUT URL. Available only with /v1/tools/merge/sync on paid plans. Maximum 2048 characters.
The URL must remain valid until the upload completes. On success, isCustomS3Bucket is true, and documentUrl and expiresAt are null. Omit this parameter to use PDFBolt's temporary storage.
See Uploading to Your S3 Bucket for setup details.
Usage:
{
"inputs": [
{"url": "https://pdfbolt.com/samples/merge-sample-1.pdf"},
{"url": "https://pdfbolt.com/samples/merge-sample-2.pdf"}
],
"filename": "combined-report.pdf",
"contentDisposition": "attachment",
"customS3PresignedUrl": "https://your-bucket.s3.amazonaws.com/combined-report.pdf?<presigned-query-params>"
}
Successful Response
The response format depends on the endpoint.
- Direct
- Sync
Returns HTTP 200 with the merged PDF in the response body.
Content-Type: application/pdf
Save the response body as a PDF file. The result is not stored for later download.
Returns HTTP 200 with a JSON response containing the result metadata.
Content-Type: application/json
With PDFBolt's default storage, download the PDF from documentUrl before expiresAt (valid for 24 hours). The example below uses default storage.
{
"requestId": "679bd05f-99ae-49a8-b0b7-eb0d694e284b",
"status": "SUCCESS",
"errorCode": null,
"errorMessage": null,
"documentUrl": "https://s3.pdfbolt.com/pdfbolt_679bd05f-99ae-49a8-b0b7-eb0d694e284b_2026-11-04T10-00-00Z.pdf",
"expiresAt": "2026-11-05T10:00:00Z",
"isAsync": false,
"duration": 420,
"documentSizeMb": 0.08,
"isCustomS3Bucket": false
}
With customS3PresignedUrl, the PDF is uploaded to your bucket. On success, isCustomS3Bucket is true, and documentUrl and expiresAt are null.
See Sync response fields for field descriptions.
Failure Response
If any input fails validation or cannot be downloaded, the entire request fails. No partial PDF is returned and no credits are charged. Check errorCode and errorMessage for details before retrying.
| HTTP status | Common cause | Action |
|---|---|---|
400 | Invalid parameters, fewer than 2 or more than 20 inputs, an inaccessible URL or PDF download timeout, or an encrypted or invalid PDF. | Check errorMessage and correct the input or settings. |
400 | Custom S3 upload failed (CUSTOM_S3_UPLOAD_ERROR, Sync only). | Check the pre-signed URL and upload permissions. |
413 | The combined input PDFs, request body, or output PDF exceeds its size limit. | Reduce the combined file size or request size. See Limits and Credits. |
422 | Processing exceeded its time limit (CONVERSION_TIMEOUT). | Use smaller or less complex PDFs. |
Input errors may include inputs[index] to identify the affected PDF.
Example validation error:
{
"timestamp": "2026-11-04T10:00:00Z",
"httpErrorCode": 400,
"errorCode": "BAD_REQUEST",
"errorMessage": "Expected 2-20 PDF files."
}
See Error Handling for the error response format and other common errors.
Limits and Credits
| Limit | Value |
|---|---|
| Input files per request | 2–20 PDFs |
| Combined input PDF size | 100 MB |
Encrypted, empty, and invalid PDFs are rejected. On the Free API plan, the output PDF is limited to 2 MB.
Successful merges cost one credit per started 5 MB of output. Failed operations consume no credits.
Requests share your account's rate limits. The x-pdfbolt-conversion-cost response header reports the credits charged.
Related Documentation
📄️ Compress PDFs
Reduce the size of existing PDF files.
📄️ Convert to PDF/X
Convert existing PDFs to PDF/X-4 or PDF/X-1a.
📄️ OpenAPI Reference
Explore endpoint schemas and response formats.