Skip to main content
Ssnap Docs
Use cases

Invoice and report PDFs

Turn an authenticated HTML page into a paginated PDF, without a PDF library.

Generating an invoice with a PDF library means laying out a document in code: absolute coordinates, manual page breaks, a font subsystem. Rendering the invoice as an HTML page and capturing it means you keep CSS, and the same template can serve the web view and the download.

The basic render

curl -G https://ssnap.cc/api/v1/screenshot \
  -H "Authorization: Bearer $SSNAP_API_KEY" \
  --data-urlencode "url=https://your-app.com/invoices/1042/print?signature=..." \
  --data-urlencode "format=pdf" \
  --data-urlencode "network_idle=true" \
  --output invoice-1042.pdf

format=pdf returns application/pdf bytes. The document paginates itself, so full_page does not apply and selector is ignored — a PDF is always the whole document.

Authentication is the first problem

Ssnap fetches the url as an anonymous public visitor. It carries none of your cookies, so a page behind session auth renders as your login screen, and a private url is rejected outright: the target must be publicly routable. See URL restrictions.

The workable pattern is a signed, time-limited, public route that renders one document:

Laravel
URL::temporarySignedRoute('invoices.print', now()->addMinutes(5), ['invoice' => $invoice]);

Same idea anywhere else: a token in the url, short expiry, single document, no session. Keep the window tight — the url is a bearer credential for that invoice while it lives.

The page is rendered by Chromium, so @media print rules apply. That is where pagination actually gets controlled:

@media print {
  nav, .cookie-banner, .download-button { display: none; }

  .invoice-line-items { break-inside: auto; }
  .invoice-total,
  .signature-block { break-inside: avoid; }

  thead { display: table-header-group; }
  tfoot { display: table-footer-group; }
}

display: table-header-group is the one worth remembering: it repeats the table header on every page of a long line-item list, which is otherwise the most common complaint about an HTML-rendered invoice.

Image styling does not apply

background, border_width, border_color and every watermark_* parameter are image post-processing. They are skipped entirely for PDFs, as is quality — PDF is not a lossy raster format.

A "PAID" or "DRAFT" watermark therefore belongs in the page's own CSS:

@media print {
  .watermark {
    position: fixed;
    inset: 0;
    display: grid;
    place-content: center;
    font-size: 8rem;
    color: rgb(0 0 0 / 8%);
    transform: rotate(-30deg);
    pointer-events: none;
  }
}

position: fixed repeats it on every printed page, which is usually what you want.

Viewport still matters

width/height and device_id apply in PDF mode, because they set the rendering viewport, which drives your responsive breakpoints. A template that collapses to a single-column mobile layout at 390px will produce a single-column PDF. Pin a desktop viewport for documents:

  --data-urlencode "width=1240" \
  --data-urlencode "height=1754"

theme applies too — theme=dark emulates prefers-color-scheme: dark, which is almost never what you want for something that will be printed on paper. Leave it at light.

Long reports: go async

A multi-hundred-page report will not finish inside the 30-second synchronous render timeout. Pass a callback_url: the request returns 202 at once, and the finished PDF is POSTed to your endpoint, with a 120-second job budget.

curl -X POST https://ssnap.cc/api/v1/screenshot \
  -H "Authorization: Bearer $SSNAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/reports/2026-q1?signature=...",
    "format": "pdf",
    "network_idle": true,
    "delay": 1500,
    "callback_url": "https://your-app.com/hooks/ssnap"
  }'

Two cautions on the async path. The signed url on your side must still be valid when the job actually runs, so give it more headroom than you would for a synchronous call. And the webhook is public — verify its signature before writing the file anywhere. See Async callbacks and Verify webhook signatures.

Charts and fonts

Anything drawn client-side needs to have finished drawing. network_idle=true covers the data fetch; delay covers the render after it. For a report full of charts, 1000–2000 ms is a realistic starting point — then measure, because the delay counts against the render timeout.

Caching and storage

cache_ttl works for PDFs exactly as for images, and format is part of the cache key, so a PDF and a PNG of the same page cache separately. For an invoice this matters less than you would think: the document is usually generated once and then belongs in your own storage. Do not rely on Ssnap as the system of record — captures are pruned per Storage and retention, and a signed url expires after 24 hours.

Checklist

  • Public, signed, short-lived route — not a session-authenticated page.
  • format=pdf; expect selector, full_page, quality and all image styling to be ignored.
  • Watermark and page breaks in @media print, not in API parameters.
  • Desktop width/height so the template does not render its mobile layout.
  • network_idle=true, plus a delay if anything is drawn client-side.
  • Async with callback_url for anything long, with signature verification on the receiver.
  • Store the bytes yourself.

Next