Use case
Invoice PDF API
Generate print-quality invoice PDFs from HTML with a single POST request. No SDK, no browser to run, no template editor to learn.
Your template, your design
Send any HTML: a Handlebars or Jinja2 template you already write for email, a React component rendered to a string, or a static file. The renderer runs a full browser so CSS Grid, Flexbox, custom fonts, and gradients all work exactly as in the browser preview.
One HTTP call, raw bytes back
POST the HTML, get the PDF. No SDK, no webhook to poll, no intermediate format. Every language with an HTTP client works.
Refund on failure
If the render times out or fails, the document is not counted against your quota. Retry safely.
Store and email a link
Add store: true to get a stable download URL instead of bytes. Email it as a link in the invoice notification. Documents are retained based on your plan (1 day free, 30 days Starter, 1 year Growth+).
Password-protected PDFs
Paid plans can add password protection with the options.password field. The PDF requires a password to open.
Accurate page breaks
Rendering runs under print emulation, so break-inside: avoid and @page rules are honored. Long item tables do not split mid-row.
1. Write the HTML template
Any HTML template engine works. Here is a Handlebars template that covers a typical invoice layout: logo, billing address, line items, and total.
<!-- templates/invoice.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { font-family: system-ui, sans-serif; color: #1a1a1a; margin: 0; padding: 40px; }
.header { display: flex; justify-content: space-between; align-items: start; }
.logo { font-size: 24px; font-weight: 700; }
.meta { text-align: right; font-size: 13px; color: #666; }
.invoice-number { font-size: 28px; font-weight: 700; margin: 32px 0 8px; }
.bill-to { margin: 24px 0; }
table { width: 100%; border-collapse: collapse; margin: 32px 0; }
th { text-align: left; padding: 8px 12px; background: #f5f5f5; font-size: 12px; }
td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 13px; }
.total-row td { font-weight: 700; border-top: 2px solid #1a1a1a; border-bottom: none; }
.footer { font-size: 12px; color: #888; margin-top: 48px; }
</style>
</head>
<body>
<div class="header">
<div class="logo">Acme Corp</div>
<div class="meta">
<div>Invoice date: {{invoiceDate}}</div>
<div>Due: {{dueDate}}</div>
</div>
</div>
<div class="invoice-number">Invoice #{{invoiceNumber}}</div>
<div class="bill-to">
<div style="font-size:12px;color:#888;margin-bottom:4px">Bill to</div>
<div>{{customer.name}}</div>
<div>{{customer.email}}</div>
<div>{{customer.address}}</div>
</div>
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Unit price</th><th>Amount</th></tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{description}}</td>
<td>{{quantity}}</td>
<td>${{unitPrice}}</td>
<td>${{amount}}</td>
</tr>
{{/each}}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="3">Total due</td>
<td>${{total}}</td>
</tr>
</tfoot>
</table>
<div class="footer">Payment terms: net 30. Thank you for your business.</div>
</body>
</html>2. Compile and POST (Node.js)
// Node.js: compile template and call the API in one function
import Handlebars from "handlebars";
import { readFileSync } from "fs";
const tmpl = Handlebars.compile(readFileSync("./templates/invoice.html", "utf-8"));
export async function generateInvoicePdf(data: {
invoiceNumber: string;
invoiceDate: string;
dueDate: string;
customer: { name: string; email: string; address: string };
items: { description: string; quantity: number; unitPrice: number; amount: number }[];
total: number;
}): Promise<Uint8Array> {
const html = tmpl(data);
const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: {
format: "A4",
margin: { top: "20mm", bottom: "20mm", left: "20mm", right: "20mm" },
print_background: true,
},
}),
});
if (!resp.ok) throw new Error(`PDF generation failed: ${resp.status}`);
return new Uint8Array(await resp.arrayBuffer());
}2. Compile and POST (Python)
# Python: Jinja2 template + httpx
import httpx
from jinja2 import Environment, FileSystemLoader
jinja = Environment(loader=FileSystemLoader("templates"))
def generate_invoice_pdf(data: dict) -> bytes:
html = jinja.get_template("invoice.html").render(**data)
resp = httpx.post(
"https://api.pdfpipe.xyz/v1/pdf",
headers={"Authorization": f"Bearer {settings.PDFPIPE_API_KEY}"},
json={
"html": html,
"options": {
"format": "A4",
"margin": {"top": "20mm", "bottom": "20mm", "left": "20mm", "right": "20mm"},
"print_background": True,
},
},
timeout=60.0,
)
resp.raise_for_status()
return resp.contentStore the PDF and return a download link
Pass store: true to get a stable URL back instead of raw bytes. Useful when you want to include a download link in the invoice email rather than attaching the file directly.
// Store the invoice and return a download URL — useful for emailing links
const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
store: true, // returns JSON with document_url instead of raw bytes
filename: `invoice-${invoiceId}.pdf`,
options: { format: "A4" },
}),
});
const { document_url, document_id } = await resp.json();
// Send document_url in the invoice email — link works for up to 30 days (Starter plan)Pricing
| Plan | Price | Invoices / month | Overage |
|---|---|---|---|
| Hobby | Free | 500 | Not available |
| Starter | $19 | 3,000 | Not available |
| Growth | $49 | 15,000 | Not available |
| Scale | $149 | 50,000 | Not available |
| Business | $499 | 100,000 | Contact sales |
Start generating invoices
500 invoices a month free. No credit card. Key issued instantly after signup.