Chaining actions
Feed one action's output PDF straight into the next — merge, then watermark, then password — without touching disk.
Every action takes a PDF in and returns a PDF out, so one action’s output is a legal input to the next. That lets you build a multi-step pipeline out of single-purpose actions: merge a few files, stamp a watermark on the result, then encrypt it — all in one script, passing bytes from call to call in memory.
Because the API is stateless — nothing is stored between requests — there is no
handle to reuse and no cleanup to do. Each stage simply hands its response body
to the next stage as the file field.
How it works
A single-output action responds with 200 OK and Content-Type: application/pdf. The body is a complete PDF. To chain, you take that body and
send it as the file part of the next request instead of reading a file from
disk. Only the very first input and the very last output need to touch the
filesystem; everything in between stays in memory.
A three-stage pipeline
This example turns two source files into one encrypted, watermarked document.
POST /v1/merge_documents with cover.pdf and report.pdf returns a single
combined PDF. See Merge PDF documents for the
input options.
Send that PDF as the file field to POST /v1/add_text_watermark. The
response is the same document with a watermark on every page.
Send the watermarked PDF to POST /v1/add_password with a password. The
response is the final, protected document — write it to disk.
Merge accepts the input PDFs as repeated file parts, or — where repeating a
field name is awkward (PHP, Ruby) — as numbered file_1 through file_10
fields. Both are shown below; use repeated file when you have more than ten.
# Each stage reads the previous PDF from stdin via `-F 'file=@-'`.
curl -sS https://api.pdfblocks.com/v1/merge_documents \
-H 'X-API-Key: your_api_key' \
-F file=@cover.pdf \
-F file=@report.pdf |
curl -sS https://api.pdfblocks.com/v1/add_text_watermark \
-H 'X-API-Key: your_api_key' \
-F 'file=@-;filename=merged.pdf;type=application/pdf' \
-F line_1='CONFIDENTIAL' \
-F line_2='ACME, Inc.' |
curl -sS https://api.pdfblocks.com/v1/add_password \
-H 'X-API-Key: your_api_key' \
-F 'file=@-;filename=watermarked.pdf;type=application/pdf' \
-F password='pa$$word' \
-o final.pdf# pip install requests
import requests
BASE = 'https://api.pdfblocks.com'
HEADERS = {'X-API-Key': 'your_api_key'}
# 1. Merge cover.pdf and report.pdf into one document.
with open('cover.pdf', 'rb') as cover, open('report.pdf', 'rb') as report:
merged = requests.post(
f'{BASE}/v1/merge_documents',
headers=HEADERS,
files=[('file', cover), ('file', report)],
)
merged.raise_for_status()
# 2. Watermark the merged bytes — no temp file.
watermarked = requests.post(
f'{BASE}/v1/add_text_watermark',
headers=HEADERS,
files={'file': ('merged.pdf', merged.content, 'application/pdf')},
data={'line_1': 'CONFIDENTIAL', 'line_2': 'ACME, Inc.'},
)
watermarked.raise_for_status()
# 3. Encrypt the watermarked bytes.
final = requests.post(
f'{BASE}/v1/add_password',
headers=HEADERS,
files={'file': ('watermarked.pdf', watermarked.content, 'application/pdf')},
data={'password': 'pa$$word'},
)
final.raise_for_status()
with open('final.pdf', 'wb') as output:
output.write(final.content)// Node.js 18+
import { readFile, writeFile } from 'node:fs/promises';
const BASE = 'https://api.pdfblocks.com';
const headers = { 'X-API-Key': 'your_api_key' };
// Post a multipart form and return the response PDF as bytes.
async function post(path, form) {
const response = await fetch(BASE + path, { method: 'POST', headers, body: form });
if (!response.ok) throw new Error(`${path} failed: ${response.status}`);
return new Uint8Array(await response.arrayBuffer());
}
// 1. Merge cover.pdf and report.pdf.
const mergeForm = new FormData();
mergeForm.append('file', new Blob([await readFile('cover.pdf')]), 'cover.pdf');
mergeForm.append('file', new Blob([await readFile('report.pdf')]), 'report.pdf');
const merged = await post('/v1/merge_documents', mergeForm);
// 2. Watermark the merged bytes.
const watermarkForm = new FormData();
watermarkForm.append('file', new Blob([merged]), 'merged.pdf');
watermarkForm.append('line_1', 'CONFIDENTIAL');
watermarkForm.append('line_2', 'ACME, Inc.');
const watermarked = await post('/v1/add_text_watermark', watermarkForm);
// 3. Encrypt the watermarked bytes.
const passwordForm = new FormData();
passwordForm.append('file', new Blob([watermarked]), 'watermarked.pdf');
passwordForm.append('password', 'pa$$word');
const final = await post('/v1/add_password', passwordForm);
await writeFile('final.pdf', final);<?php
$base = 'https://api.pdfblocks.com';
$headers = ['X-API-Key: your_api_key'];
// Post a multipart form and return the response body, or throw on error.
function post(string $url, array $headers, array $fields): string {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $fields,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status !== 200) {
throw new RuntimeException("Request to $url failed with status $status.");
}
return $body;
}
// 1. Merge cover.pdf and report.pdf (numbered fields for the array input).
$merged = post("$base/v1/merge_documents", $headers, [
'file_1' => new CURLFile('cover.pdf', 'application/pdf'),
'file_2' => new CURLFile('report.pdf', 'application/pdf'),
]);
// 2. Watermark the merged bytes in memory (CURLStringFile, PHP 8.1+).
$watermarked = post("$base/v1/add_text_watermark", $headers, [
'file' => new CURLStringFile($merged, 'merged.pdf', 'application/pdf'),
'line_1' => 'CONFIDENTIAL',
'line_2' => 'ACME, Inc.',
]);
// 3. Encrypt the watermarked bytes.
$final = post("$base/v1/add_password", $headers, [
'file' => new CURLStringFile($watermarked, 'watermarked.pdf', 'application/pdf'),
'password' => 'pa$$word',
]);
file_put_contents('final.pdf', $final);# gem install http
require 'http'
require 'stringio'
BASE = 'https://api.pdfblocks.com'
HEADERS = { 'X-API-Key' => 'your_api_key' }
def post(path, fields)
response = HTTP.headers(HEADERS).post("#{BASE}#{path}", form: fields)
raise "#{path} failed: #{response.status}" unless response.status.success?
response.body.to_s
end
# 1. Merge cover.pdf and report.pdf (numbered fields for the array input).
merged = post('/v1/merge_documents',
file_1: HTTP::FormData::File.new('cover.pdf'),
file_2: HTTP::FormData::File.new('report.pdf'))
# 2. Watermark the merged bytes in memory.
watermarked = post('/v1/add_text_watermark',
file: HTTP::FormData::File.new(StringIO.new(merged),
filename: 'merged.pdf', content_type: 'application/pdf'),
line_1: 'CONFIDENTIAL',
line_2: 'ACME, Inc.')
# 3. Encrypt the watermarked bytes.
final = post('/v1/add_password',
file: HTTP::FormData::File.new(StringIO.new(watermarked),
filename: 'watermarked.pdf', content_type: 'application/pdf'),
password: 'pa$$word')
File.write('final.pdf', final)package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
const base = "https://api.pdfblocks.com"
// post sends a multipart form and returns the response PDF bytes.
func post(path string, build func(*multipart.Writer)) []byte {
var buf bytes.Buffer
form := multipart.NewWriter(&buf)
build(form)
form.Close()
req, _ := http.NewRequest("POST", base+path, &buf)
req.Header.Set("Content-Type", form.FormDataContentType())
req.Header.Set("X-API-Key", "your_api_key")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%s failed: %d", path, res.StatusCode))
}
return body
}
func addFile(form *multipart.Writer, name string, data []byte) {
part, _ := form.CreateFormFile("file", name)
part.Write(data)
}
func main() {
cover, _ := os.ReadFile("cover.pdf")
report, _ := os.ReadFile("report.pdf")
// 1. Merge cover.pdf and report.pdf.
merged := post("/v1/merge_documents", func(form *multipart.Writer) {
addFile(form, "cover.pdf", cover)
addFile(form, "report.pdf", report)
})
// 2. Watermark the merged bytes.
watermarked := post("/v1/add_text_watermark", func(form *multipart.Writer) {
addFile(form, "merged.pdf", merged)
form.WriteField("line_1", "CONFIDENTIAL")
form.WriteField("line_2", "ACME, Inc.")
})
// 3. Encrypt the watermarked bytes.
final := post("/v1/add_password", func(form *multipart.Writer) {
addFile(form, "watermarked.pdf", watermarked)
form.WriteField("password", "pa$$word")
})
os.WriteFile("final.pdf", final, 0644)
}using System.Net.Http.Headers;
const string Base = "https://api.pdfblocks.com";
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "your_api_key");
// Post a multipart form and return the response PDF bytes.
async Task<byte[]> Post(string path, Action<MultipartFormDataContent> build)
{
using var form = new MultipartFormDataContent();
build(form);
var response = await client.PostAsync(Base + path, form);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
static void AddFile(MultipartFormDataContent form, string name, byte[] data)
{
var content = new ByteArrayContent(data);
content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(content, "file", name);
}
// 1. Merge cover.pdf and report.pdf.
var merged = await Post("/v1/merge_documents", form =>
{
AddFile(form, "cover.pdf", File.ReadAllBytes("cover.pdf"));
AddFile(form, "report.pdf", File.ReadAllBytes("report.pdf"));
});
// 2. Watermark the merged bytes.
var watermarked = await Post("/v1/add_text_watermark", form =>
{
AddFile(form, "merged.pdf", merged);
form.Add(new StringContent("CONFIDENTIAL"), "line_1");
form.Add(new StringContent("ACME, Inc."), "line_2");
});
// 3. Encrypt the watermarked bytes.
var final = await Post("/v1/add_password", form =>
{
AddFile(form, "watermarked.pdf", watermarked);
form.Add(new StringContent("pa$$word"), "password");
});
await File.WriteAllBytesAsync("final.pdf", final);Check each stage before feeding it onward
A failed stage returns a 4XX with an application/problem+json body, not a
PDF. If you pipe that error body into the next request as file, the next stage
will just fail too, with a confusing message. Check the status of every response
before passing its body along — the examples above raise on any non-200. See
Errors for the response shape.
Watermark and merge before you encrypt. Once Add password has encrypted a document, later actions can’t open it — so any password step belongs at the end of the pipeline. See Protecting and unlocking documents for the full ordering rules.
When to chain, and when not to
Chain when each step is a distinct transformation you want applied in sequence. Keep every step a pure, single-purpose call — that is what makes the output of one a valid input to the next, and what keeps failures easy to localize.
Some jobs look like chaining but aren’t. Reordering and dropping pages in one pass is a single Reorder pages call, not an extract followed by a merge. Reach for a pipeline only when no single action does the whole job.