Chromium prints the page when the wait condition is met, not when your chart library feels finished. On RelayPDF the documented waits are options.waitUntil, options.waitForSelector, and options.waitForTimeout on POST /v1/pdf. waitUntil defaults to networkidle0. timeout is the Chromium render budget, milliseconds, max 60000. If the job misses that budget it returns render_failed and is not billed. Field names are identical in JSON, Node, and Python. Product page: https://relaypdf.com/html-to-pdf. Field list: https://relaypdf.com/docs/options.
What is actually on the page
POST /v1/pdf takes exactly one source: html, url, markdown, or templateId. HTML is a UTF-8 JSON string, not Base64. JavaScript in that HTML runs in Chromium. A first paint that only has a loading spinner, an empty canvas, or a webfont still swapping is a valid print if you stop too early. The wait options exist so you print after the network and the DOM you care about have settled. They are not a second source and they are not a script you inject.
PDF and images share waitUntil and timeout. The same waitForSelector and waitForTimeout fields apply on POST /v1/images. This how-to is the PDF path. Office files do not go here; they go to POST /v1/convert. A report assembled from several PDFs is a later merge, not a wait option. Report and deck workflow: https://relaypdf.com/use-cases/reports.
waitUntil
waitUntil is a Chromium lifecycle event. The rendering-options table default is networkidle0. The HTML to PDF product page lists four values: load, domcontentloaded, networkidle0, networkidle2. Do not invent others. Do not wrap the value in a gotoOptions object; on RelayPDF it is options.waitUntil.
| waitUntil | When Chromium treats navigation as done |
|---|---|
| domcontentloaded | DOM parsed. Scripts and fonts may still be in flight. |
| load | The load event. Images and stylesheets that participate in load have finished. |
| networkidle0 | No network connections for about 500 ms. Default. |
| networkidle2 | At most two connections for about 500 ms. |
Use networkidle0 when the page fetches chart data, webfonts, and images and then goes quiet. Use networkidle2 when something on the page keeps a long-lived connection and networkidle0 never fires. Use load when you only need the load event. Use domcontentloaded only when the printable markup is already in the HTML and you do not care about late CSS or fonts. A page that polls forever will not satisfy networkidle0. That is a selector problem, not a reason to invent a fourth wait type.
waitForSelector
waitForSelector is a CSS selector, or an object { selector, timeout, visible }. The docs do not list hidden, state, or a function predicate. Wait for an element that exists only when the client render is done: a chart container with a data-ready attribute, a table tbody that your script inserts, a #report-ready flag you set yourself. If the selector is already in the static HTML, the wait returns immediately and you have gained nothing.
visible, when you pass the object form, waits until the element is present and visible. A canvas that is in the DOM but still display:none is not enough. Put the ready marker on the painted node. timeout on the object is how long that wait may run; it still sits inside options.timeout, the job-level render budget (max 60000). There is no documented way to wait for document.fonts.ready or for a Chart.js animation frame. If you need those, have the page set a selector when they complete.
waitForTimeout
waitForTimeout is an extra wait in milliseconds after the other waits, max 30000. It is a delay, not a condition. Use it when a webfont or a chart paints after the selector is already in the tree and you cannot add a better marker. A fixed 2000 ms on every invoice is wasted budget. A 300 ms settle after #chart-ready is visible is the intended use. The cap is 30000, not the 60000 on options.timeout. Those are different clocks: timeout is the whole Chromium render; waitForTimeout is an extra pause inside it.
timeout
options.timeout is the Chromium render budget, max 60 seconds. It is not an SDK HTTP timeout. Stack waitUntil, waitForSelector, and waitForTimeout so they finish inside that budget. A selector that never appears burns the budget and the job fails with render_failed. Failed jobs are not billed. If the HTML is huge or the URL is slow, raise timeout toward 60000. Do not assume a default timeout value; the options table leaves the default blank and only states the max.
curl
Binary response writes the PDF to disk. Auth is a bearer key. Host is https://api.relaypdf.com. The sample waits for network idle, then for a chart marker, then 400 ms for paint.
curl https://api.relaypdf.com/v1/pdf
-H "Authorization: Bearer pdf_live_..."
-H "Content-Type: application/json"
-d '{
"html": "<html><body><div id=\"chart-ready\">...</div></body></html>",
"filename": "report.pdf",
"options": {
"format": "letter",
"printBackground": true,
"waitUntil": "networkidle0",
"waitForSelector": { "selector": "#chart-ready", "visible": true },
"waitForTimeout": 400,
"timeout": 30000
}
}'
--output report.pdf
The default response is application/pdf with x-relaypdf-id, x-relaypdf-size, and content-disposition. response: url returns JSON with a public download that expires in 24 hours. response: async returns 202 and a poll URL. Those modes do not change the wait fields. A public url source still has to be public; private, loopback, and metadata hosts are rejected. extraHTTPHeaders and cookies are the documented way to send credentials on a url fetch, not a wait knob.
Node
Package is @relaypdf/sdk. options field names match REST. fromHtml sends the HTML string as JSON. The same options object works on fromUrl, fromMarkdown, and fromTemplate.
import { RelayPDF } from "@relaypdf/sdk";
const client = new RelayPDF({
apiKey: process.env.RELAYPDF_API_KEY!,
});
const pdf = await client.pdf.fromHtml(html, {
filename: "report.pdf",
options: {
waitUntil: "networkidle0",
waitForSelector: { selector: "#chart-ready", visible: true },
waitForTimeout: 400,
timeout: 30000,
},
});
await pdf.save("report.pdf");
Charts, fonts, and what is not documented
A chart library that appends a canvas and then animates for 800 ms needs a ready marker after the last draw, plus a short waitForTimeout if the marker is set before paint. A Google Font or self-hosted @font-face file is a network request; networkidle0 usually covers it. If the file is cached and the swap happens after idle, wait for a class you add in document.fonts.ready in your own HTML, then waitForSelector that class. RelayPDF does not expose document.fonts.ready as an option.
- No waitForFunction, evaluate, or injected script field on https://relaypdf.com/docs/options.
- No waitForNetworkIdle separate from waitUntil.
- No hidden form of waitForSelector in the public table.
- No per-resource timeout, font subsetting flag, or chart-engine preset.
- wkhtmltopdf on POST /v1/convert is a different engine. These Chromium waits do not apply there.
A working order
Leave waitUntil at networkidle0 unless a beacon keeps the network busy. Put a single ready node in the HTML you control. Point waitForSelector at that node, object form, visible true. Add waitForTimeout only if a measured render still clips a font or a last animation frame. Keep the sum inside timeout. If you are printing a dashboard URL you do not control and cannot add a marker, waitUntil networkidle2 plus a small waitForTimeout is the documented toolkit. That is weaker than a selector you own.
For scheduled reports, convert Word or Excel sources on POST /v1/convert, print HTML sections with the waits above, then merge and bookmark. The use-case write-up is https://relaypdf.com/use-cases/reports. Endpoint contract: https://relaypdf.com/docs/pdf. Option names stay on https://relaypdf.com/docs/options.