PDF and reporting

Turn an authenticated dashboard into a PDF report

Your product emails customers a PDF of a report that only exists behind a session. The first attempt returns a valid PDF file with the wrong content: a login screen, or a dashboard whose numbers had not arrived yet.

5 min readPublished Updated
What you actually see

Correct paper size, correct margins, and a document showing a login form, an empty shell, or charts still in their loading state.

Reproduce it

  1. 01Point any PDF renderer at a URL that requires a session and open the output instead of only checking the HTTP status — a 200 and a login page look identical to the status code.
  2. 02Add the credential but keep the default navigation wait, and watch the numbers arrive after the print already happened.
  3. 03The fixture below returns 401 without an Authorization header, so both failures are reproducible without touching a customer account.
The DIY version

Start with the smallest thing that works

Attach the credential to the browser context, wait for a marker the report itself renders, and only then print. For one internal report on a machine you already operate, this may be all you need.

report-to-pdf.mts
import { chromium } from "playwright";

const browser = await chromium.launch();
const context = await browser.newContext({
  extraHTTPHeaders: { authorization: `Bearer ${process.env.REPORT_TOKEN!}` },
});
const page = await context.newPage();

await page.goto("https://app.example.com/reports/quarterly", { waitUntil: "load" });

// Sem isto o PDF sai com o esqueleto do relatório, não com o relatório.
await page.waitForSelector("[data-report-ready]");

await page.pdf({ path: "report.pdf", format: "A4", printBackground: true });
await browser.close();
Where it breaks

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

The token outlives the job

In memory it is fine. The failure is operational: the same token reaches a log line, a retry payload, a crash report or a debugging artifact path — and it is a credential for one customer's data.

Browser state leaks between customers

Reusing a context to save startup time is how one tenant's session ends up rendering another tenant's report. Isolation has to be per job, and that is a policy you now own.

Readiness is specific to this report

A fixed delay is a guess that gets slower and still wrong. `networkidle` never settles on a dashboard holding a websocket. The only reliable signal is a marker the report renders when its data is in.

The redirect is not yours

An authenticated report can redirect. Without an egress policy, a redirect to a private address turns your renderer into a proxy into your own network — the standard SSRF shape.

Keep the DIY version when

  • The report is internal, generated by a back-office task on a schedule you control.
  • One page shape, one team owning both the page and the browser, and a failed run a human can simply re-run.
The recipe

generate a PDF of a dashboard behind login

The credential travels in the encrypted POST body, the readiness selector is part of the request contract, and the response is PDF bytes. The runner below executes against the live authenticated fixture, which answers 401 without the header the worker injects.

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 '{"format":"pdf","wait_for_selector":"[data-report-ready]","pdf_print_background":true,"pdf_paper_format":"a4"}' \
  -o capture.pdf
What you get, and what it costs

Output, limits, cost, failures

Output

A real PDF with selectable text and working links. Paper size, margins and background rendering are request parameters, so two runs of the same report produce the same document.

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

  • `wait_for_selector` is bounded by `navigation_timeout` (30 s maximum) and by the overall `timeout` (90 s maximum).
  • Credentials are accepted on POST only — a signed GET cannot carry them, because a query string is logged by every intermediary.
  • Request secrets are encrypted at rest and destroyed when the job reaches a terminal state; they never enter the render spec or the cache key.
  • Authenticated captures are not shared across requests: the response depends on a session, so a shared artifact would be a data leak.

Common errors

The credential did not reach the page or has expired; the page answered 401 or 403.

HTTP 500 · terminal

selector_not_foundnever billed

The readiness marker never appeared — the report renders it later, or it lives inside a shadow root.

HTTP 400 · terminal

timeout_errornever billed

The page never reached the requested navigation state, common on dashboards holding an open websocket.

HTTP 500 · retryable

The Authorization header was hand-built with a missing colon or a stray newline.

HTTP 400 · terminal

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 API maintainers. Verified 2026-08-12 by running the published code against a fixture.

The published cURL, TypeScript and Python run against the authenticated fixture in CI, which returns 401 without the header; the artifact is checked to be a real PDF.

engine-crunknown-pwunknown-f2026-07-1-b2026-07-1StatusAuthenticated pages 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 how a credential is attached to every request in a context.

  2. Canonical parameter contract for cookies, headers and Authorization.

  3. SecurityPageCapture

    States secret lifetime, context isolation and egress policy.