Use case
Bulk PDF API
Generate invoices, reports, or certificates for all your accounts in a single call. The POST /v1/pdf/batch endpoint renders every document and returns a permanent URL for each one. No queue to manage, no per-document round-trips.
Batch request and response
Send a requests array. Each item has its ownhtml, filename, and options. Top-level options are merged with per-item options as defaults.
Request
POST /v1/pdf/batch HTTP/1.1
Authorization: Bearer pp_live_...
Content-Type: application/json
{
"requests": [
{
"html": "<html><!-- invoice 1001 --></html>",
"filename": "invoice-1001.pdf",
"options": { "format": "A4" }
},
{
"html": "<html><!-- invoice 1002 --></html>",
"filename": "invoice-1002.pdf",
"options": { "format": "A4" }
}
],
"options": { "format": "A4" }
}Response
// Response: all documents stored, URLs ready
{
"results": [
{
"index": 0,
"status": "ok",
"id": "aB3xY...",
"url": "https://api.pdfpipe.xyz/v1/documents/aB3xY...",
"filename": "invoice-1001.pdf",
"size_bytes": 48210,
"expires": "2027-06-13T00:00:00.000Z"
},
{
"index": 1,
"status": "ok",
"id": "cD5zW...",
"url": "https://api.pdfpipe.xyz/v1/documents/cD5zW...",
"filename": "invoice-1002.pdf",
"size_bytes": 51340,
"expires": "2027-06-13T00:00:00.000Z"
}
],
"usage": {
"rendered": 2,
"total_this_month": 144,
"limit": 15000
}
}Month-end invoice run (Node.js)
A typical pattern: chunk your customers into batches matching your plan limit, run a batch per chunk, persist the returned URL so your customer portal always has a download link without you storing the bytes.
// scripts/month-end-invoices.ts — run on the 1st of every month
import Mustache from "mustache";
import { readFileSync } from "fs";
const template = readFileSync("./templates/invoice.html", "utf-8");
const PDFPIPE_KEY = process.env.PDFPIPE_KEY!;
const BATCH_SIZE = 50; // Growth plan allows 50 items per batch
async function runMonthEndInvoicing() {
const customers = await db.customers.findActive();
console.log(`Generating ${customers.length} invoices...`);
for (let i = 0; i < customers.length; i += BATCH_SIZE) {
const chunk = customers.slice(i, i + BATCH_SIZE);
const requests = chunk.map((c) => ({
html: Mustache.render(template, invoiceData(c)),
filename: `invoice-${c.id}-${period()}.pdf`,
options: { format: "A4" },
}));
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
method: "POST",
headers: { Authorization: `Bearer ${PDFPIPE_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ requests }),
});
if (!res.ok) throw new Error(`Batch error ${res.status}: ${await res.text()}`);
const { results, usage } = await res.json();
const failed = results.filter((r: any) => r.status === "error").length;
if (failed > 0) console.warn(`${failed} failures in batch ${i / BATCH_SIZE + 1}`);
// Persist document URLs for the customer portal
await db.invoices.bulkCreate(
results
.filter((r: any) => r.status === "ok")
.map((r: any, idx: number) => ({
customer_id: chunk[idx].id,
period: period(),
pdf_id: r.id,
pdf_url: r.url,
expires_at: new Date(r.expires),
}))
);
console.log(`Batch ${i / BATCH_SIZE + 1}: ${results.length - failed}/${results.length} ok — ${usage.total_this_month}/${usage.limit} used`);
}
}
function period() { return new Date().toISOString().slice(0, 7); }Python async batch
Use httpx with an async client. Each chunk is awaited in sequence to stay within the batch-size limit.
import os, httpx, asyncio
from pathlib import Path
PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
BATCH_SIZE = 50 # match your plan limit
template = Path("templates/report.html").read_text()
async def batch_render(customers: list[dict]) -> list[dict]:
requests = [
{
"html": render_template(template, c),
"filename": f"report-{c['id']}.pdf",
"options": {"format": "A4"},
}
for c in customers
]
async with httpx.AsyncClient(timeout=120) as client:
res = await client.post(
"https://api.pdfpipe.xyz/v1/pdf/batch",
headers={"Authorization": f"Bearer {PDFPIPE_KEY}"},
json={"requests": requests},
)
res.raise_for_status()
return res.json()["results"]
async def run_month_end():
customers = await db.get_active_customers()
all_results = []
for i in range(0, len(customers), BATCH_SIZE):
chunk = customers[i : i + BATCH_SIZE]
results = await batch_render(chunk)
all_results.extend(results)
print(f"Batch {i // BATCH_SIZE + 1}: done")
return all_resultsWebhook callback when the batch completes
Pass webhook_url and an optionalwebhook_secret. The API POSTs the full results to your endpoint once all documents are rendered, signed with an HMAC-SHA-256 signature you can verify server-side.
// Optional: get notified when the batch finishes
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
method: "POST",
headers: { Authorization: `Bearer ${PDFPIPE_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
requests,
webhook_url: "https://yourapp.example.com/webhooks/batch-complete",
webhook_secret: process.env.BATCH_WEBHOOK_SECRET,
}),
});
// Your webhook receives:
// POST /webhooks/batch-complete
// X-PDFPipe-Signature: sha256=...
// {
// "event": "batch.complete",
// "results": [...],
// "usage": { "rendered": 50, "total_this_month": 194, "limit": 15000 }
// }Batch limits by plan
| Plan | Documents/mo | Max batch size | Storage | Price |
|---|---|---|---|---|
| Hobby | 500 | — | 1 day | Free |
| Starter | 3,000 | 10 | 30 days | $19/mo |
| Growth | 15,000 | 50 | 365 days | $49/mo |
| Scale | 50,000 | 100 | 2 years | $149/mo |
| Business | 100,000 | 250 | 2 years | $499/mo |
Need more than 100,000 documents a month or a batch size over 250? Contact us for enterprise pricing.