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.
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.
Correct paper size, correct margins, and a document showing a login form, an empty shell, or charts still in their loading state.
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.
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();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.
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.
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.
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.
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.
One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.
The credential did not reach the page or has expired; the page answered 401 or 403.
HTTP 500 · terminal
The readiness marker never appeared — the report renders it later, or it lives inside a shadow root.
HTTP 400 · terminal
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
The runner above carries this exact configuration into the playground — no retyping, no starting over.
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.
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.
Defines how a credential is attached to every request in a context.
Canonical parameter contract for cookies, headers and Authorization.
States secret lifetime, context isolation and egress policy.
The same credential handling when the artifact is an image.
Add running page numbers once the report content is correct.
The full contract for cookies, headers and Authorization.
The failure this recipe is most likely to leave you with.