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.

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

  1. Query the data

    Fetch exactly the rows the report needs, with totals computed in the query or code.

  2. Render HTML with print CSS

    Build the page with escaped values and a stylesheet that sets the page size, breaks and table styles.

  3. Convert and validate

    Send the HTML to the API, then confirm the result is a PDF with the page count and content you expect.

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
# 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)} &middot; {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.

validate.py
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.

Subscribe to Our Newsletter for Fast HTML to PDF Updates

Stay updated on new features for our fast, simple, and affordable HTML to PDF API.