PDF and reporting

Invoice PDFs your application can reproduce

An invoice is a document a customer keeps and an auditor may read years later. It is generated from data your application already owns, so there is no page to point a renderer at — and the same invoice regenerated next month must not look different.

5 min readPublished Updated
What you actually see

Totals drift by a decimal between the screen and the PDF, a currency symbol renders as a box, the layout that fit one page on your laptop spills onto two in production, or the amount is right and the thousands separator belongs to another locale.

Reproduce it

  1. 01Render an invoice containing €, R$ and a right-to-left customer name in a container image that ships no font packages, and inspect which glyphs survive.
  2. 02Format the same total with the platform locale on your machine and on the server, then compare — locale-dependent formatting is the most common silent difference.
  3. 03Regenerate an invoice from three months ago and diff it against the archived copy.
The DIY version

Start with the smallest thing that works

Build the markup from the invoice record, set it directly on the page so nothing has to be hosted, and print with explicit paper and margins.

invoice.mts
import { chromium } from "playwright";

// Formate no seu código, com locale explícito. O default do sistema muda
// entre a sua máquina e o container e ninguém percebe até o cliente reclamar.
const total = new Intl.NumberFormat("pt-BR", {
  style: "currency",
  currency: "BRL",
}).format(invoice.totalCents / 100);

const browser = await chromium.launch();
const page = await browser.newPage();

await page.setContent(renderInvoiceHtml({ ...invoice, total }), { waitUntil: "load" });
await page.evaluateHandle(() => document.fonts.ready);

await page.pdf({
  path: "invoice.pdf",
  format: "A4",
  printBackground: true,
  margin: { top: "0.6in", bottom: "0.6in", left: "0.5in", right: "0.5in" },
});
await browser.close();
Where it breaks

The call is small. Everything around it is the system.

The font is not in the image

A slim base image ships almost no fonts. Chromium substitutes silently, so the invoice renders — with different metrics, different line wrapping and, for some currency and non-Latin glyphs, empty boxes. Nothing errors.

`document.fonts.ready` is not the whole story

It resolves for fonts the document requested. A face requested by CSS that never loads still resolves the promise, and the fallback is what gets printed.

Reproducibility is a browser-version property

A Chromium upgrade can change text shaping and pagination. For a document a customer keeps, 'renders correctly today' is weaker than it sounds — you need the archived bytes or a pinned engine.

A browser is a large dependency for a fixed template

If the invoice is one stable layout with no browser CSS requirement, a native PDF library is smaller, faster and more predictable than a browser. This is the case where DIY genuinely wins.

Keep the DIY version when

  • The invoice layout is fixed and a native PDF library already models it — a browser adds cost without adding capability.
  • Volume is low, the fonts are Latin-only, and an operator sees every document before it is sent.
The recipe

generate an invoice PDF from application data

Send the markup your application already builds; nothing has to be hosted. Paper, margins and background are request parameters, so the document is defined by the request rather than by whatever the rendering machine happened to have installed.

Capture settings
Enter the source, choose your options, then run the capture.
Result
Your capture will appear here and stay in view.
waiting

No result yet

Complete the settings and run the tool. Images, PDFs, text, and video all preview in this panel.

Send this exact request
cURL, TypeScript and Python are generated from the same configuration as the demo.
curl --fail-with-body "https://api.pagecapture.dev/v1/take" \
  -H "X-Access-Key: $PAGECAPTURE_KEY" \
  -H "Content-Type: application/json" \
  --data '{"html":"<style>body{font:15px/1.5 system-ui;color:#111;margin:0}header{display:flex;justify-content:space-between;border-bottom:2px solid #111;padding-bottom:12px}table{width:100%;border-collapse:collapse;margin-top:28px}th,td{text-align:left;padding:8px 0;border-bottom:1px solid #ddd}td.n,th.n{text-align:right}tfoot td{border:0;font-weight:700;font-size:18px}</style><header><div><strong>Invoice 1042</strong><br><small>Issued 2026-08-12 · Due 2026-09-11</small></div><div style=\"text-align:right\"><strong>Northwind Ltda</strong><br><small>CNPJ 12.345.678/0001-90</small></div></header><table><thead><tr><th>Description</th><th class=\"n\">Qty</th><th class=\"n\">Amount</th></tr></thead><tbody><tr><td>Capture credits — August</td><td class=\"n\">25,000</td><td class=\"n\">R$ 1.250,00</td></tr><tr><td>Overage — 3,000 credits</td><td class=\"n\">3</td><td class=\"n\">R$ 180,00</td></tr></tbody><tfoot><tr><td colspan=\"2\">Total</td><td class=\"n\">R$ 1.430,00</td></tr></tfoot></table>","format":"pdf","pdf_paper_format":"a4","pdf_print_background":true,"pdf_margin_top":0.6,"pdf_margin_bottom":0.6,"pdf_margin_left":0.5,"pdf_margin_right":0.5}' \
  -o capture.pdf
What you get, and what it costs

Output, limits, cost, failures

Output

A PDF whose text is selectable and searchable — an auditor can copy the total out of it. The engine version that produced it is recorded on the request, so a document can be traced back to how it was rendered.

Cost

One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.

1 creditper successful non-cached capture

Limits

  • The request body is capped at 100 MiB; reference images by HTTPS URL instead of inlining large data URIs.
  • Remote assets in your markup go through the same egress policy as any capture — a private asset host is not reachable.
  • PDF margins are inches, at most 10 per side.
  • The engine version is part of cache identity, so an engine upgrade never silently serves a document rendered by the previous one.

Common errors

request_not_validnever billed

`html` was combined with `url`, or a PDF option was sent with a non-PDF format.

HTTP 400 · terminal

timeout_errornever billed

A remote font or image in the markup never resolves and consumes the job budget.

HTTP 500 · retryable

Images were inlined as data URIs instead of referenced by URL.

HTTP 413 · terminal

network_errornever billed

An asset in the markup points at a private or unreachable host.

HTTP 500 · retryable

Try it against your own page

The runner above carries this exact configuration into the playground — no retyping, no starting over.

Return to the configured runner
Tested and reviewed

Written by PageCapture Engineering. Reviewed by PageCapture PDF maintainers. Verified 2026-08-12 by running the published code against a fixture.

The published cURL, TypeScript and Python are executed against the inline-HTML path in CI and the artifact is checked to be a real PDF; the request is normalized by the same contract as the public API.

engine-crunknown-pwunknown-f2026-07-1-b2026-07-1StatusPDF rendering options referenceEditorial method

Sources and verification basis

Sources support the browser and API behaviors named above. PageCapture-specific limits and billing are taken from the public contract; external sources are used for the underlying browser behavior.

  1. Defines explicit locale and currency formatting, rather than the host default.

  2. Defines page-oriented layout for printed documents.

  3. PDF optionsPageCapture

    Canonical parameter names, paper formats and margin limits.