Playwright's page.pdf() prints the current page with Chromium's print CSS. You launch a browser, load HTML or a URL, wait until the page is ready, then write a buffer. That is the whole call. A hosted HTML to PDF API runs the same print path without a browser process on your machine: POST HTML and get a PDF back.
Use Playwright when you already drive Chromium for tests or scripts and PDF is a side effect. Use an API when PDF generation is a production job and callers should not ship a browser. Product surface: https://relaypdf.com/html-to-pdf. Endpoint docs: https://relaypdf.com/docs/pdf.
When to use which
Keep page.pdf when that library is already a test dependency and volume is low.
Call an API when callers should not install Chromium, and you need format, margins, headers, footers, and a wait setting on one request, plus a bearer key and a prepaid wallet.
What page.pdf does
Official docs: it returns a PDF buffer using print media. Screen media needs an explicit media switch first. printBackground defaults to false. format defaults to Letter. displayHeaderFooter defaults to false. PDF output is a Chromium feature, not a Firefox or WebKit print path.
The method does not start a process. You do. Width, height, and margin accept CSS units (px, in, cm, mm). Unlabeled numbers are pixels. format, if set, wins over width and height. preferCSSPageSize honors @page size in CSS. scale is 0.1-2. pageRanges is a string such as 1-5, 8, 11-13.
Around that one call you still own process lifetime, concurrency, disk, and retries. In-process Chromium also means memory, container shared memory, and a queue if more than one print runs at once. That is general worker cost, not a RelayPDF benchmark.
Wait settings are not a print-call option
In Playwright, waitUntil lives on navigation helpers. It is not a field on the print call. If you print too early, you get empty charts, missing webfonts, or half-loaded images.
Documented waitUntil values: load (default), domcontentloaded, networkidle, and commit. The library marks networkidle as discouraged for tests: it waits until there are no network connections for at least 500 ms. commit means the navigation response started loading. None of those names are networkidle0 or networkidle2.
RelayPDF puts waitUntil on the PDF job itself. Documented values on the HTML product page and the options page: load, domcontentloaded, networkidle0, networkidle2. Default is networkidle0. That is the other-library pair, not the single networkidle name. If you paste a script into an API body, rename the wait. options.timeout is the Chromium render budget, max 60000 ms. It is not an SDK HTTP timeout. Jobs that fail to render return render_failed and are not billed. Optional extras: waitForSelector and waitForTimeout (extra wait, max 30000 ms).
Headers and footers
Headers and footers are off until you set displayHeaderFooter to true and pass headerTemplate and footerTemplate HTML. Official limits: script tags inside templates are not evaluated, and page styles are not visible inside templates. Dynamic values are injected via CSS classes: date, title, url, pageNumber, totalPages. Templates that use Handlebars-style tokens will print the token, not the page index. You must reserve margin. With default zero margins, Chromium clips the header and footer and they never show. Inline font-size; the template default is effectively unreadable if you leave it unset.
RelayPDF uses the same Chromium header and footer mechanism. options.headerTemplate and options.footerTemplate are HTML. Setting them enables Chromium headers and footers. Documented placeholders: pageNumber, totalPages, date, title. The options page does not list a url placeholder. Docs say to use extra margin so templates are not clipped. Field names are identical in JSON, Node, and Python (printBackground, not print_background). printBackground defaults to true on POST /v1/pdf, which is the opposite of the Playwright page.pdf default.
If a footer is missing in either stack, check three things before rewriting layout: the header/footer flag or templates are actually set, top and bottom margins leave space, and the template HTML uses the injector names the engine documents. Then check the wait setting so images in the body finished before print.
Side-by-side
Facts from official print docs and live RelayPDF pages. Nothing here is a rating.
| Capability | Playwright page.pdf | RelayPDF POST /v1/pdf |
|---|---|---|
| Process | You start, isolate, and close it | Managed |
| Input | Load HTML or a URL, then print | Exactly one of html, url, markdown, templateId |
| waitUntil | On navigation: load, domcontentloaded, networkidle, commit | On the job: load, domcontentloaded, networkidle0, networkidle2; default networkidle0 |
| Headers / footers | displayHeaderFooter plus templates; classes date, title, url, pageNumber, totalPages | headerTemplate / footerTemplate HTML; placeholders pageNumber, totalPages, date, title; extra margin required |
| printBackground | Default false | Default true |
| Timeout | Your library timeout on navigation and actions | Chromium budget, max 60s; render_failed not billed |
| Response | Buffer or path on disk | binary (default), url (24h expiry), or async plus callbackUrl |
| Auth and billing | Yours | Bearer key; prepaid wallet; $0.015 per successful HTML/URL/Markdown PDF |
| Office / merge | Not this print call | POST /v1/convert; POST /v1/pdf/merge |
| URL fetch | Your network and cookies | Public http(s) only; private, loopback, and metadata hosts rejected |
Playwright page.pdf sample
Minimal Chromium path. The wait setting is on the HTML load call. Headers require displayHeaderFooter, class injectors, and non-zero margins. printBackground is explicit because the default is false.
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle" });
await page.pdf({
path: "invoice.pdf",
format: "Letter",
printBackground: true,
displayHeaderFooter: true,
headerTemplate: "<div style=\"font-size:10px;width:100%;padding:0 12mm\"><span class=\"title\"></span></div>",
footerTemplate: "<div style=\"font-size:10px;width:100%;text-align:center\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>",
margin: { top: "20mm", bottom: "16mm", left: "12mm", right: "12mm" },
});
await browser.close();
RelayPDF POST /v1/pdf sample
Same invoice intent. HTML is a UTF-8 JSON string, not Base64. waitUntil is on the job. headerTemplate / footerTemplate enable Chromium headers. Default response is application/pdf. Live curl shape from https://relaypdf.com/html-to-pdf; options from https://relaypdf.com/docs/options.
curl https://api.relaypdf.com/v1/pdf
-H "Authorization: Bearer pdf_live_..."
-H "Content-Type: application/json"
-d '{"html":"<h1>Invoice 1042</h1>","filename":"invoice.pdf"}'
--output invoice.pdf
Add options on that same JSON body: format letter, printBackground true, waitUntil networkidle0, margin with extra top and bottom, headerTemplate and footerTemplate as HTML. Other sources on the same route: a public url, GitHub-flavoured markdown, or a published Handlebars templateId plus templateData. response may be binary, url, or async. url responses include a public download that expires in 24 hours. Optional callbackUrl is an HTTPS POST when the job finishes. Node helpers on the same endpoint: fromHtml, fromUrl, fromMarkdown, fromTemplate.
What you still own
A working print script is not a production service. You still write process flags, a pool or a queue, crash restart, storage, auth in front of the worker, and a bill if you run it on rented CPUs. Tagged and outline PDF flags exist on the Playwright page.pdf as of v1.42. They are library options. Do not assume a hosted API exposes every Chromium flag you see in that library.
page.pdf is the right tool when print is downstream of a real session. An HTML to PDF API is the right tool when you already have the HTML string or a public URL and you want Chromium print without that session.
Limits
RelayPDF URL fetch is public http(s) only. Private, loopback, and metadata hosts are rejected. Timeout max is 60 seconds. Send exactly one source field. Office files go to POST /v1/convert. Merging existing PDFs is POST /v1/pdf/merge. This article does not claim OCR, e-sign, PDF/A, or HIPAA. Pricing is a prepaid wallet, not monthly document tiers: HTML, URL, or Markdown to PDF is $0.015 per successful operation; Handlebars template PDFs are $0.015; LibreOffice convert is $0.04; wkhtmltopdf convert is $0.025; PDF tools, barcodes, and zip are $0.005. Failed, invalid, unauthorized, and rate-limited requests are not billed. Credit does not expire. Signup includes $5 credit with no card. Source: https://relaypdf.com/pricing.
Playwright limits are the inverse: you can hit private hosts and hold a document as long as your process lives. You also own the outage when Chromium runs out of memory. Neither tool is a full document platform by itself. One is a driver. RelayPDF is a document HTTP API with Chromium print as one route.
If you already run it in CI
Do not rip it out of CI to print invoices. Keep the test driver. Move production print when the request path should not start Chromium: a webhook, an agent, a JVM service, or a worker that only has an HTTP client.
- Keep the HTML you already render. Send it as html, not an image of the page.
- Map format, margin, printBackground, pageRanges, and templates onto options. Rename networkidle to networkidle0 unless you mean networkidle2.
- Set headerTemplate / footerTemplate and widen top and bottom margin. Do not expect page CSS to leak into the template.
- Choose response: binary for a sync download, url for a 24-hour link, async plus callbackUrl for a queue.
Sibling compare: https://relaypdf.com/compare/puppeteer