Use case
Payslip PDF API
Generate monthly payslips, salary slips, and tax statements as print-quality PDFs from HTML. Render one document or your entire headcount in a single call. Built for HR, payroll, and HRIS platforms with recurring volume.
One run, every employee
Render a payslip for your whole headcount in a single POST to /v1/pdf/batch. Documents render in parallel and each comes back with its own stable download URL. No per-employee round-trips, no queue to babysit.
Your layout, your branding
Send the HTML payslip your designers already built, with the company logo, fonts, color, and the earnings and deductions breakdown your finance team expects. A full browser renders it, so CSS Grid, Flexbox, and custom fonts come out exactly as previewed.
Built for monthly volume
Payslips recur every pay cycle for every employee, so cost predictability matters. Flat monthly plans, no per-call surprises, and renders that fail or time out are never counted against your quota.
Data stays in your stack
Salary figures live in your payroll system. You compute net pay, then send only the finished HTML to render. Nothing is logged from the document body, and stored PDFs expire automatically based on your plan retention.
Statements and letters too
The same endpoint covers annual tax statements, bonus and increment letters, and full-and-final settlements. One template engine, one API, every HR document your platform emits.
Accurate multi-page layout
Rendering runs under print emulation, so break-inside: avoid and @page rules are honored. Long deduction tables and year-to-date summaries do not split mid-row across pages.
1. Write the payslip template
Any HTML template engine works. Here is a Handlebars template that covers a typical monthly payslip: company header, employee identity, earnings, deductions, and net pay.
<!-- templates/payslip.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { font-family: system-ui, sans-serif; color: #1a1a1a; margin: 0; padding: 40px; font-size: 13px; }
.header { display: flex; justify-content: space-between; align-items: start; border-bottom: 2px solid #1a1a1a; padding-bottom: 16px; }
.company { font-size: 22px; font-weight: 700; }
.doc-title { text-align: right; font-size: 13px; color: #666; }
.doc-title strong { display: block; font-size: 16px; color: #1a1a1a; }
.employee { display: flex; justify-content: space-between; margin: 28px 0; }
.field-label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.04em; }
.field-value { font-size: 14px; font-weight: 600; margin-top: 2px; }
table { width: 100%; border-collapse: collapse; margin: 24px 0; }
th { text-align: left; padding: 8px 12px; background: #f5f5f5; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
th.amount, td.amount { text-align: right; }
td { padding: 10px 12px; border-bottom: 1px solid #eee; }
.net-row td { font-weight: 700; font-size: 15px; border-top: 2px solid #1a1a1a; border-bottom: none; background: #fafafa; }
.footer { font-size: 11px; color: #888; margin-top: 40px; border-top: 1px solid #eee; padding-top: 12px; }
</style>
</head>
<body>
<div class="header">
<div class="company">Acme Corp</div>
<div class="doc-title">
<strong>Payslip</strong>
<div>Pay period: {{payPeriod}}</div>
<div>Pay date: {{payDate}}</div>
</div>
</div>
<div class="employee">
<div>
<div class="field-label">Employee</div>
<div class="field-value">{{employee.name}}</div>
</div>
<div>
<div class="field-label">Employee ID</div>
<div class="field-value">{{employee.id}}</div>
</div>
<div>
<div class="field-label">Department</div>
<div class="field-value">{{employee.department}}</div>
</div>
</div>
<table>
<thead>
<tr><th>Earnings</th><th class="amount">Amount</th></tr>
</thead>
<tbody>
<tr><td>Basic salary</td><td class="amount">{{basicSalary}}</td></tr>
<tr><td>Allowances</td><td class="amount">{{allowances}}</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Deductions</th><th class="amount">Amount</th></tr>
</thead>
<tbody>
<tr><td>Total deductions</td><td class="amount">{{deductions}}</td></tr>
</tbody>
<tfoot>
<tr class="net-row">
<td>Net pay</td>
<td class="amount">{{netPay}}</td>
</tr>
</tfoot>
</table>
<div class="footer">
This payslip is computer generated and reflects earnings for the stated pay period.
Keep it for your records. For queries, contact your HR department.
</div>
</body>
</html>2. Compile and POST (Node.js)
Compute the pay figures in your payroll logic, fill the template, and post the HTML. You get the PDF bytes back.
// Node.js: compile the payslip template and call the API
import Handlebars from "handlebars";
import { readFileSync } from "fs";
const tmpl = Handlebars.compile(readFileSync("./templates/payslip.html", "utf-8"));
export async function generatePayslipPdf(data: {
payPeriod: string;
payDate: string;
employee: { name: string; id: string; department: string };
basicSalary: string;
allowances: string;
deductions: string;
netPay: string;
}): 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: "16mm", bottom: "16mm", left: "16mm", right: "16mm" },
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_payslip_pdf(data: dict) -> bytes:
html = jinja.get_template("payslip.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": "16mm", "bottom": "16mm", "left": "16mm", "right": "16mm"},
"print_background": True,
},
},
timeout=60.0,
)
resp.raise_for_status()
return resp.contentRun payroll for every employee in one call
Payroll is inherently a batch job. Post a requests array to /v1/pdf/batch and every payslip renders in parallel. Each result carries a stored URL you can persist so the employee self-service portal always has a download link, without you holding the bytes.
// Run payroll: render a payslip for every employee in one call
import Handlebars from "handlebars";
import { readFileSync } from "fs";
const tmpl = Handlebars.compile(readFileSync("./templates/payslip.html", "utf-8"));
const period = "2026-06"; // June 2026
// The payroll rows you already compute for the period
const employees = await db.payroll.forPeriod(period);
// each row: { name, id, department, basicSalary, allowances, deductions, netPay, payDate }
const requests = employees.map((e) => ({
html: tmpl({
payPeriod: "June 2026",
payDate: e.payDate,
employee: { name: e.name, id: e.id, department: e.department },
basicSalary: e.basicSalary,
allowances: e.allowances,
deductions: e.deductions,
netPay: e.netPay,
}),
filename: `payslip-${e.id}-${period}.pdf`,
options: { format: "A4", print_background: true },
}));
const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ requests, options: { format: "A4" } }),
});
if (!resp.ok) throw new Error(`Batch failed: ${resp.status}`);
const { results, usage } = await resp.json();
// Persist each stored URL so the employee self-service portal has a download link
await db.payslips.bulkCreate(
results
.filter((r) => r.status === "ok")
.map((r, idx) => ({
employee_id: employees[idx].id,
period,
pdf_id: r.id,
pdf_url: r.url,
expires_at: new Date(r.expires),
}))
);
console.log(`Payroll ${period}: ${results.length} payslips, ${usage.total_this_month}/${usage.limit} used`);Pricing
| Plan | Price | Payslips / 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 payslips
500 documents a month free. No credit card. Key issued instantly after signup. Batch your whole headcount on a single call from day one.