Use case
Report PDF API
Generate analytics reports, financial statements, and data summaries as print-quality PDFs with one HTTP call. Your HTML template, your data, your design.
Full CSS support
Charts rendered with a charting library, data tables, custom fonts, multi-column layouts, and cover pages with background colors all render exactly as in a browser.
Accurate page breaks
The renderer runs under print emulation. break-inside: avoid stops tables from splitting mid-row. @page rules set custom margins for cover pages vs body pages.
Store and share
Add store: true to get a stable URL instead of raw bytes. Email it, save it to your database, or return it in an API response. Retention window depends on your plan.
Batch for scale
Starter+ plans can batch up to 10-500 reports per call. A webhook fires when all renders complete so you can update your database without polling.
Usage alerts at 80% and 95%
You get an email when your account hits 80% and 95% of the monthly limit, so you can upgrade before reports start failing at month-end.
No quota burn on failure
If a render fails (network error, timeout, renderer capacity), the document is not counted against your quota. Retry safely.
1. Build the report template
A cover page, KPI cards, and a data table. The renderer supports web fonts, CSS Grid, background colors, and print-media rules like break-inside: avoid that stop tables from splitting mid-row.
<!-- templates/monthly-report.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
body { font-family: 'Inter', system-ui, sans-serif; color: #1a1a1a; margin: 0; }
.cover { background: #1d1812; color: #f5efe2; padding: 80px 60px; page-break-after: always; }
.cover h1 { font-size: 48px; margin: 0 0 12px; font-weight: 700; }
.cover .period { font-size: 18px; opacity: 0.6; }
.section { padding: 48px 60px; border-bottom: 1px solid #eee; }
.section h2 { font-size: 22px; font-weight: 700; margin: 0 0 24px; }
.kpi-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-bottom: 40px; }
.kpi { background: #f9f9f9; border-radius: 12px; padding: 24px; }
.kpi .value { font-size: 36px; font-weight: 700; color: #d23a1d; }
.kpi .label { font-size: 13px; color: #666; margin-top: 4px; }
.kpi .delta { font-size: 12px; margin-top: 8px; }
.kpi .delta.up { color: #16a34a; }
.kpi .delta.down { color: #dc2626; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; padding: 10px 12px; background: #f5f5f5; font-size: 12px; font-weight: 600; }
td { padding: 10px 12px; border-bottom: 1px solid #f0f0f0; font-size: 13px; }
.footer { padding: 32px 60px; font-size: 12px; color: #999; }
</style>
</head>
<body>
<div class="cover">
<div class="period">{{reportPeriod}}</div>
<h1>{{reportTitle}}</h1>
<div style="margin-top:40px;font-size:14px;opacity:0.5">Prepared {{preparedDate}}</div>
</div>
<div class="section">
<h2>Key metrics</h2>
<div class="kpi-grid">
{{#each kpis}}
<div class="kpi">
<div class="value">{{value}}</div>
<div class="label">{{label}}</div>
<div class="delta {{deltaClass}}">{{delta}}</div>
</div>
{{/each}}
</div>
</div>
<div class="section">
<h2>Detail breakdown</h2>
<table>
<thead>
<tr>{{#each tableHeaders}}<th>{{this}}</th>{{/each}}</tr>
</thead>
<tbody>
{{#each tableRows}}
<tr>{{#each this}}<td>{{this}}</td>{{/each}}</tr>
{{/each}}
</tbody>
</table>
</div>
<div class="footer">Confidential · {{reportTitle}} · {{reportPeriod}}</div>
</body>
</html>2. Compile and POST: store the PDF
Pass store: true to get back a stable URL instead of raw bytes. Useful for emailing links, saving to a database, or returning from an API endpoint.
// Generate a monthly report PDF and store it for sharing
import Handlebars from "handlebars";
import { readFileSync } from "fs";
const tmpl = Handlebars.compile(readFileSync("./templates/monthly-report.html", "utf-8"));
export async function generateMonthlyReport(data: ReportData): Promise<string> {
const html = tmpl({
reportTitle: data.title,
reportPeriod: data.period,
preparedDate: new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
kpis: data.kpis.map((k) => ({
...k,
deltaClass: k.delta.startsWith("+") ? "up" : "down",
})),
tableHeaders: data.tableHeaders,
tableRows: data.tableRows,
});
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,
filename: `${data.period}-report.pdf`,
options: {
format: "A4",
margin: { top: "0", bottom: "0", left: "0", right: "0" },
print_background: true,
},
}),
});
if (!resp.ok) throw new Error(`PDF generation failed: ${resp.status}`);
const { document_url } = await resp.json();
return document_url; // stable URL to email or save to your DB
}3. Schedule month-end reports
// Scheduled report: run on the first of each month (cron job or queue)
// cron: 0 6 1 * * → 06:00 on the 1st of every month
export async function monthEndJob() {
const period = getPreviousMonthLabel(); // e.g. "June 2026"
const data = await buildReportData(period);
const pdfUrl = await generateMonthlyReport(data);
// Notify the team
await sendSlackMessage({
channel: "#reports",
text: `📊 ${period} report is ready: ${pdfUrl}`,
});
// Save to your database for the dashboard
await db.reports.create({
period,
pdfUrl,
generatedAt: new Date(),
});
}Batch-generate reports for many accounts
Growth+ plans can batch up to 50 reports per call, with a webhook notification when all complete. Useful for SaaS products that send monthly reports to all customers.
// Batch generate reports for all accounts in one call (Growth+ plans)
const accounts = await db.accounts.findMany({ where: { reportEnabled: 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({
options: { format: "A4", print_background: true },
requests: accounts.slice(0, 50).map((account) => ({
html: tmpl(buildAccountData(account)),
filename: `${account.id}-report.pdf`,
})),
webhook_url: "https://yourapp.com/webhooks/batch-complete",
}),
});
const { results } = await resp.json();
// results[i].url is the stored PDF URL for each accountPricing
| Plan | Price | Reports / month | Batch size |
|---|---|---|---|
| Hobby | Free | 500 | None |
| Starter | $19 | 3,000 | 10 per call |
| Growth | $49 | 15,000 | 50 per call |
| Scale | $149 | 50,000 | 100 per call |
| Business | $499 | 100,000 | 250 per call |
Start generating reports
500 reports a month free. No credit card. Key issued instantly after signup.