PDFPipe

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
<!-- 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.

report.ts
// 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

cron-job.ts
// 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-reports.ts
// 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 account

Pricing

PlanPriceReports / monthBatch size
HobbyFree500None
Starter$193,00010 per call
Growth$4915,00050 per call
Scale$14950,000100 per call
Business$499100,000250 per call

Start generating reports

500 reports a month free. No credit card. Key issued instantly after signup.

Related use case

Invoice PDF API →

Related guide

PDF in Next.js →