A dashboard is not an invoice. Cards load data after navigation. Canvas and SVG charts animate. Fonts arrive late. If you print on the first load event, you get empty tiles. That is a wait problem, not a missing OCR or screenshot feature. POST /v1/images is a bitmap of a viewport. Stay on /v1/pdf when the artifact is a dated report someone will archive or print.
Wait for widgets
options.waitUntil defaults to networkidle0. Documented Chromium lifecycle values (same enum as images) are load, domcontentloaded, networkidle0, and networkidle2. networkidle0 is the usual first choice: no in-flight connections for 500 ms. Many chart SDKs keep a heartbeat or analytics socket open. In that case networkidle0 never fires and you hit options.timeout. Switch to load or networkidle2, then wait for a DOM marker you control.
waitForSelector is a CSS selector, or an object { selector, timeout, visible }. Put a class on the last tile when its series is drawn, for example .widget-ready or [data-export="ready"]. visible true waits until the node is not display:none. Do not invent waitForFunction, page.waitForNetworkIdle as a second API, or a parent.startWait hook. Those are other products.
waitForTimeout is an extra delay in milliseconds after the other waits, max 30000. Use it for a last paint frame, not as the only strategy. options.timeout is the Chromium render budget, max 60000. It is not the SDK HTTP timeout. A job that cannot finish the page returns render_failed and is not billed. Full wait notes, including when not to sleep: https://relaypdf.com/blog/html-to-pdf-wait-for-javascript.
Landscape and page box
options.landscape defaults to false. Set true for a KPI row that is wider than it is tall. format defaults to letter. Named values on the options page: letter, A4, legal, tabloid, or another named Chromium format. width and height set a custom page size. scale is 0.1–2 (default 1). margin is top / right / bottom / left as CSS lengths (10mm, 0.5in).
preferCSSPageSize defaults to false. Set true when your stylesheet already has @page { size: … }. That honors CSS page size only. It is not a media-type switch and it is not a substitute for landscape when you have not declared @page. pageRanges (for example 1-3,5) trims a long board after print. headerTemplate and footerTemplate are Chromium margin fragments with pageNumber, totalPages, date, title. Give them extra margin or they clip. They are not a copy of the app chrome.
Screen look, print CSS, backgrounds
Chromium’s print path applies print media. RelayPDF does not document a field that forces screen media. If @media print { .sidebar { display: none } } or { canvas { display: none } } is in the app CSS, the PDF will match that, not the monitor. For an export that should look like the live board, send a dedicated export document: same widgets, styles that do not hide them in print, printBackground true so tile fills and chart backgrounds survive.
printBackground defaults to true. You almost never need to set it unless a caller turned it off. Dark tiles, card shadows, and plot backgrounds disappear when it is false. That is the documented lever for “the PDF looks washed out,” not an undocumented emulateMedia: screen flag.
html versus a public URL
html is a UTF-8 JSON string, not Base64. Render the export view in your app (or a headless worker you already run), then POST the markup. That is the right path for a private dashboard. url fetches a public http(s) page. Private, loopback, and metadata hosts return url_not_allowed. cookies (name, value, optional domain / path / url) and extraHTTPHeaders apply to that Chromium fetch. They are not a login robot and they do not open RFC1918 hosts. Mixing html with url in one body is invalid_request.
filename, if sent, must end in .pdf. response is binary (default), url, or async. binary returns application/pdf with x-relaypdf-id, x-relaypdf-size, and content-disposition. url returns a 24-hour GET /v1/files/:id link. async returns 202; poll GET /v1/jobs/:id. callbackUrl must be https. Heavy boards that sit near the 60 s render cap belong on async. Endpoint: https://relaypdf.com/docs/pdf.
Request
Node @relaypdf/sdk. Field names in options stay camelCase (printBackground, waitForSelector, waitForTimeout). JSON matches REST.
import { RelayPDF } from "@relaypdf/sdk";
const client = new RelayPDF({ apiKey: process.env.RELAYPDF_API_KEY });
const pdf = await client.pdf.fromHtml(dashboardHtml, {
filename: "ops-board.pdf",
options: {
format: "A4",
landscape: true,
printBackground: true,
waitUntil: "networkidle0",
waitForSelector: { selector: ".widget-ready", visible: true },
waitForTimeout: 500,
timeout: 45000,
margin: { top: "12mm", right: "10mm", bottom: "12mm", left: "10mm" },
},
});
await pdf.save("ops-board.pdf");
curl to the same endpoint:
curl -X POST https://api.relaypdf.com/v1/pdf
-H "Authorization: Bearer pdf_live_..."
-H "Content-Type: application/json"
-d '{
"url": "https://example.com/public-board",
"filename": "board.pdf",
"options": {
"landscape": true,
"printBackground": true,
"waitUntil": "load",
"waitForSelector": ".widget-ready",
"timeout": 45000
}
}'
--output board.pdf
Documented wait and print fields
| Field | Default | Use on a dashboard |
|---|---|---|
| waitUntil | networkidle0 | load / networkidle2 if sockets never idle |
| waitForSelector | — | CSS or { selector, timeout, visible } |
| waitForTimeout | — | Extra ms, max 30000 |
| timeout | — | Render budget, max 60000 |
| landscape | false | Wide KPI rows |
| format | letter | A4, legal, tabloid, or named size |
| printBackground | true | Keep tile and chart fills |
| preferCSSPageSize | false | Honor @page size only |
| scale | 1 | 0.1–2 |
| margin | — | CSS lengths |
| width / height | — | Custom page box |
| pageRanges | — | e.g. 1-3,5 |
| headerTemplate / footerTemplate | — | pageNumber, totalPages, date, title |
| extraHTTPHeaders / cookies | — | Public url fetches only |
That is the print/wait surface on https://relaypdf.com/docs/options. Do not add emulateMedia, mediaType, waitForFunction, clip on PDF, or viewport as a PDF page size. viewport and clip are image options on POST /v1/images.
Office reports versus HTML boards
https://relaypdf.com/use-cases/reports is the Office path: POST /v1/convert for Word, Excel, or PowerPoint, response async for large decks, then merge and /v1/pdf/bookmarks. Use that when the source is a spreadsheet or a slide file. Use POST /v1/pdf when the source is HTML you already render in a browser. Convert engines (LibreOffice / wkhtmltopdf) are not a second Chromium wait API. wkhtmltopdf does not run modern chart bundles the way Chromium does.
Errors
Failures return { error: { code, message } }. invalid_request for mixed sources, a filename that is not .pdf, or a wait/print value outside the documented range. url_not_allowed for a private host. render_failed when Chromium times out or cannot print. payment_required on an empty wallet. rate_limited with Retry-After. Failed jobs are not billed. Branch on error.code.
Ship it
Render or fetch the board, wait for a selector you own, print landscape with backgrounds on, and keep the wait budget under 60 s (async if the queue is long). Product CTA: https://relaypdf.com/use-cases/reports. Wait sibling: https://relaypdf.com/blog/html-to-pdf-wait-for-javascript. Options: https://relaypdf.com/docs/options. PDF body: https://relaypdf.com/docs/pdf.