01 · Diagnose the problem

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.
02 / Do it yourself

Attach the credential, then wait for the report

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();
03 / Where it breaks

Where handling the credential yourself breaks

The call above is small. Everything around it is the system — and the system is what you would be signing up to own.

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.
04 / 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
This demo runs one fixed request. Press the button to capture it.
Result
Your capture will appear here and stay in view.
waiting

No result yet

Run the demo. 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 '{"url":"https://www.pagecapture.dev/api/fixtures/authenticated-report","authorization":"Bearer pagecapture-public-fixture","format":"pdf","wait_for_selector":"[data-report-ready]","pdf_print_background":true,"pdf_paper_format":"a4"}' \
  -o capture.pdf
05 / Output, limits, cost, failures

What the authenticated PDF gives you

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

06 / Take it for a run

Try it against your own page

Swap the example for a page you care about. The runner keeps every option from this guide, and carries the whole configuration into the playground — no retyping, no starting over.

Return to the configured runner
07 / Verification
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-08-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.