Use case: reports
PDF report generation from your data
Weekly summaries, client statements and exports all start as data. Render that data as an HTML page with a table or two, use CSS to control the print layout, and convert it to a PDF that people can download or receive by email.
Q3 2026 · Page 1
| Region | Orders | Revenue |
|---|---|---|
| North | 1,240 | $48,300.00 |
| South | 980 | $36,910.00 |
| West | 1,105 | $41,275.00 |
| Total | 3,325 | $126,485.00 |
A report structure that converts well
Cover and summary
Title, period and the two or three numbers a reader needs first.
Chapters on their own pages
Start each major section on a new page with break-before: page.
Tables and charts
Tables for exact figures. Render charts as SVG or an inline image so they print crisply.
Appendix
Long detail tables go last, where splitting across pages is expected.
The data to HTML to PDF workflow
Query the data
Fetch exactly the rows the report needs, with totals computed in the query or code.
Render HTML with print CSS
Build the page with escaped values and a stylesheet that sets the page size, breaks and table styles.
Convert and validate
Send the HTML to the API, then confirm the result is a PDF with the page count and content you expect.
Tables, page breaks, headers and footers
The converter prints your page with a browser engine, so standard print CSS applies. We tested the following against the live service:
- Page size.
@page { size: A4 }andsize: A4 landscapeare honoured. Otherwise the page size comes from thewidthandheightrequest fields. - Page breaks.
break-before: pagestarts a new page, andbreak-inside: avoidkeeps a row from splitting. - Repeating header. An element with
position: fixedappeared on every page.
Not available: automatic page numbers and “Page X of Y” footers, and a separate header/footer option. In our test, a table’s <thead> was not repeated on later pages, so for long tables add the column labels to each section, or split the table into chunks that each carry a header row.
@page { size: A4; margin: 18mm 14mm; } /* page size and margins */
body { font: 12px/1.5 Arial, sans-serif; color: #111827; }
.running-header { /* repeats on every page */
position: fixed; top: -12mm; left: 0; right: 0;
font-size: 10px; color: #6b7280; border-bottom: 1px solid #e5e7eb;
}
section.chapter { break-before: page; } /* each chapter starts a new page */
section.chapter:first-of-type { break-before: auto; }
h2, h3 { break-after: avoid; } /* keep headings with their content */
tr, figure { break-inside: avoid; } /* never split a row or chart */
table { width: 100%; border-collapse: collapse; }
td, th { padding: 6px 8px; border-bottom: 1px solid #e5e7eb; text-align: left; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }Large-document considerations
Keep the HTML lean
Avoid inlining thousands of styled elements. Plain tables with a shared stylesheet render faster and produce smaller PDFs.
Optimise images
Resize images to the size they print at, and avoid very large embedded data: URIs.
Split very long reports
Generate chapters or date ranges as separate PDFs rather than one huge document, so a failure only affects one part.
Allow for time
Large documents take longer to render. Set generous client timeouts and generate in the background. Per-plan limits are not published, so test with your largest real report.
Integration example
This script queries a database, renders the HTML and converts it using the client from the Python guide. The same approach works in Node.js, PHP and Laravel.
# report.py: data -> HTML -> PDF
import html, sqlite3
from cheappdf import CheapPdf # client from the Python integration guide
def build_html(rows, title, period):
e = html.escape
body = "".join(
f"<tr><td>{e(r['region'])}</td><td class='num'>{r['orders']:,}</td>"
f"<td class='num'>${r['revenue']:,.2f}</td></tr>"
for r in rows
)
total = sum(r["revenue"] for r in rows)
return f"""<!DOCTYPE html><html><head><meta charset="utf-8"><style>{open('report.css').read()}</style></head>
<body>
<div class="running-header">{e(title)} · {e(period)}</div>
<section class="chapter">
<h1>{e(title)}</h1><p>Period: {e(period)}</p>
<h2>Revenue by region</h2>
<table><thead><tr><th>Region</th><th class="num">Orders</th><th class="num">Revenue</th></tr></thead>
<tbody>{body}</tbody></table>
<p><strong>Total revenue: ${total:,.2f}</strong></p>
</section>
</body></html>"""
db = sqlite3.connect("sales.db"); db.row_factory = sqlite3.Row
rows = db.execute("SELECT region, COUNT(*) AS orders, SUM(total) AS revenue FROM orders GROUP BY region").fetchall()
pdf = CheapPdf().from_html(build_html(rows, "Quarterly sales", "Q3 2026"), filename="q3-sales.pdf")
open("q3-sales.pdf", "wb").write(pdf)Batch processing
For per-client monthly statements, loop over clients from a scheduled job, convert one at a time or with a small worker pool, and record success or failure per client. Make the job safe to re-run by skipping clients whose PDF already exists. The Node.js guide shows a concurrency-limited batch pattern. The free plan allows 10 conversions per day, so regular batches need the Premium plan.
Error handling and output validation
A request can succeed and still produce the wrong document, for example when a database query returned no rows. Check the output before you send it to anyone: confirm the file starts with %PDF, the page count is plausible, and a known piece of text is present.
Treat 400 responses as permanent and fix the input. Retry network errors and 5xx responses with backoff. See the API error reference.
from pypdf import PdfReader # pip install pypdf
import io
def validate_pdf(pdf: bytes, expect_text: str, max_pages: int = 200) -> None:
if not pdf.startswith(b"%PDF"):
raise ValueError("Not a PDF")
reader = PdfReader(io.BytesIO(pdf))
if not 1 <= len(reader.pages) <= max_pages:
raise ValueError(f"Unexpected page count: {len(reader.pages)}")
if expect_text not in (reader.pages[0].extract_text() or ""):
raise ValueError("Expected content missing: the data may not have loaded")PDF report generation FAQ
How do I create a PDF report from data?
Query your data, render it into an HTML page with tables and print CSS, and send the HTML to the CheapPDF API. The response contains the PDF, which you decode and save.
Can I control page breaks in the PDF?
Yes. Standard CSS such as break-before: page and break-inside: avoid worked in our tests, and @page size is honoured.
Can I add page numbers?
Page numbers and Page X of Y footers are not available. A position: fixed element can repeat static text, such as a title, on every page.
Will table headers repeat on every page?
In our testing they did not. Add column labels to each section or split long tables into chunks, each with its own header row.
How large can a report be?
Per-plan size and time limits for HTML strings are not published. Test with your largest report, and split very large reports into several PDFs.
Automate your reports
Create an API key and turn your first dataset into a PDF.