WeasyPrint is a Python library that lays out HTML and CSS with its own engine and writes PDF. RelayPDF is a hosted Chromium print API. You send HTML, a public URL, Markdown, or a published template and get a PDF back. If you need a WeasyPrint alternative because the page runs JavaScript, Chart.js, or the Flex and Grid you already debug in Chrome, use a browser engine. If the document is static HTML plus CSS Paged Media and must stay in-process, keep WeasyPrint. Product: https://relaypdf.com/html-to-pdf. Chromium sibling: https://relaypdf.com/compare/puppeteer. PDF docs: https://relaypdf.com/docs/pdf.
What each one is
WeasyPrint is a visual rendering engine for HTML and CSS that exports to PDF. It is not WebKit, Gecko, or Chromium. Layout is Python, built for pagination, BSD licensed. Docs target Python 3.10+ and need Pango on the host. Install via pip, a distro package, Homebrew, or the Windows executable. Then HTML write_pdf or the CLI. CourtBouillon sells consulting; the library is free software. Site: https://weasyprint.org/ Manual: https://doc.courtbouillon.org/weasyprint/
RelayPDF runs Chromium for you. POST /v1/pdf with exactly one of html, url, markdown, or templateId. html is a UTF-8 JSON string, not Base64. Default response is application/pdf. response url returns a public download that expires in 24 hours. response async plus optional callbackUrl is the job path. Office files go to POST /v1/convert. Merge is POST /v1/pdf/merge. Official Python client: pip install relaypdf. Methods are snake_case. JSON fields stay REST camelCase such as printBackground and templateId. Python SDK: https://relaypdf.com/docs/sdks/python.
Paged CSS versus a browser
WeasyPrint exists because browser engines were thin on CSS Paged Media and they still skip a lot of print-only CSS. Page size, margins, running headers, page counters, and print typography are why people pick it for invoices, tickets, and books. Geometry lives in CSS, not CLI flags. The First Steps and Common Use Cases manuals say command-line size flags are the wrong tool; put at-page rules in the document or a user stylesheet.
Those same manuals can emit archive, accessibility, graphics-exchange, and hybrid e-invoice PDF variants when you pass pdf_variant and supply the metadata files. That is WeasyPrint work, not RelayPDF. This landing does not claim those variants for the API. Going Further in the WeasyPrint docs states the scope: no user interaction, no JavaScript, no live rendering after parse. Maintainers have said there is no plan to add a JS engine. Chart libraries, syntax highlighters, SPA shells, and any layout that only exists after a script runs will print empty unless you pre-render the DOM and hand WeasyPrint a static tree.
RelayPDF waits on Chromium lifecycle. waitUntil defaults to networkidle0 and also accepts load, domcontentloaded, and networkidle2. Optional waitForSelector. Render timeout max 60 seconds. printBackground defaults to true. Header and footer templates are Chromium HTML with pageNumber, totalPages, date, and title placeholders. Options live on the rendering options doc. See https://relaypdf.com/docs/options.
Flex and Grid are not a slogan fight. Chromium prints the Flex and Grid you already debug in DevTools. WeasyPrint implements its own box model. Unsupported declarations log warnings and are ignored. Some modern CSS works there. Fragmentation across pages is a different codebase than Blink. If the template was designed in Chrome and depends on that paint, a WeasyPrint alternative means a browser engine, not a closer paged-CSS implementation.
Side by side
| WeasyPrint | RelayPDF | |
|---|---|---|
| Shape | In-process Python library + CLI | Hosted REST API, SDKs, CLI, MCP |
| Engine | Own HTML/CSS layout (not Chromium) | Chromium print |
| JavaScript | None; preprocess if you need a live DOM | Runs in Chromium; waitUntil / waitForSelector |
| Page control | @page, user stylesheets, pdf_variant | format, margin, headerTemplate, preferCSSPageSize |
| Inputs | File, URL, string, custom URL fetcher | html | url | markdown | templateId (one only) |
| URL fetch | Default HTTP fetcher; file:// possible; no cookies in default client | Public http(s) only; private/loopback/metadata hosts rejected |
| Python | from weasyprint import HTML | from relaypdf import RelayPDF |
| You operate | Pango, fonts, memory, untrusted-HTML sandbox | Wallet, API key, 60s render budget |
| Also | PDF forms flag, attachments, image cache | Screenshots, LibreOffice, wkhtmltopdf, merge, barcodes, zip |
The Python you actually write
WeasyPrint happy path is one import. HTML accepts a path, a URL, or string equals for in-memory markup. write_pdf with no path returns bytes. A filename overwrites silently. You can pass extra CSS objects, a FontConfiguration for font-face, a shared image cache, and a custom URLFetcher when the default client has no cookies and no auth. Flask-WeasyPrint and Django-WeasyPrint exist because the library does not automatically see the user browser cookies.
from weasyprint import HTML, CSS
HTML(string="<h1>Invoice 1042</h1>").write_pdf(
"invoice.pdf",
stylesheets=[CSS(string="@page { size: A4; margin: 18mm }")],
)
The RelayPDF client is a thin urllib wrapper around the same JSON the curl examples use. You do not install Pango. You send a key and you pay per successful job. from_html, from_url, from_markdown, and from_template map to POST /v1/pdf. BinaryResult.save writes the file. Errors raise RelayPDFError with status, code, message, and optional retry_after.
import os
from relaypdf import RelayPDF
client = RelayPDF(api_key=os.environ["RELAYPDF_API_KEY"])
pdf = client.pdf.from_html(
"<h1>Invoice #1042</h1><p>Total: $1,200.00</p>",
filename="invoice.pdf",
options={"format": "A4", "printBackground": True},
)
pdf.save("invoice.pdf")
Equivalent curl, placeholder key, from the product page:
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
Ops WeasyPrint leaves on your machine
pip install weasyprint is the start, not the inventory. First Steps walks Linux packages for Pango, HarfBuzz, and fontconfig; macOS via Homebrew; Windows via an exe or MSYS2 plus a DLL directory env var. Missing libraries fail at import. Missing fonts draw squares. The docs recommend long-lived processes so you do not pay startup on every request. Performance notes are blunt: WeasyPrint is often slower than other web engines; large CSS frameworks and multi-page tables are expensive; speed is not the main goal.
Security is documented, not implied. Untrusted HTML or CSS can mean long renders, high memory, infinite loops, huge CSS values, local file reads including device sinks, and attachments that embed whatever the process can reach. Default HTTP timeout is 10 seconds for web protocols and does not apply to local files. The project tells you to drop privileges, sandbox, cap CPU and memory, use a restrictive fetcher, and sanitize input. That is normal for an in-process renderer. It is also work you skip when the HTML never touches your filesystem. RelayPDF url sources reject private, loopback, and metadata hosts. That is a different threat model, not a claim that the API is a compliance product.
Limits
WeasyPrint will not execute JavaScript. It will not magically match Chrome on every Flex or Grid edge case. You own Pango, fonts, workers, and the untrusted-HTML checklist. Archive and accessibility variants are available there when you configure them; validity still depends on your HTML and CSS. Default fetch has no cookies.
RelayPDF will not run on your laptop offline. html must be complete enough to print. A public url cannot see localhost. Chromium timeout max is 60 seconds. render_failed is not billed. preferCSSPageSize is off unless you set it, so at-page size is not the default the way it is in WeasyPrint. Header and footer HTML needs extra margin or Chromium clips it. This API does not advertise archival PDF profiles, OCR, e-sign, or HIPAA. Do not treat it as a drop-in for WeasyPrint pdf_variant work.
Pricing (verified)
WeasyPrint the library is free under a BSD license. CourtBouillon consulting packages are a separate commercial offering on the project site. Those pages list topic counts and response times and note the packages are not sold in Canada and the USA. This article is not a price list for their support.
RelayPDF is a prepaid wallet. Credit does not expire. Failed, invalid, unauthorized, and rate-limited requests do not debit. HTML, URL, Markdown, or template PDF is 0.015 USD. Screenshots 0.015. LibreOffice convert 0.04. wkhtmltopdf convert 0.025. Tools such as merge, stamp, raster, barcode, and zip are 0.005. AI template generate 0.05. Signup credit is 5 USD, no card. Top-up bonuses: 20 USD at 5 percent, 50 at 6, 100 at 10, 500 at 15. Rate card checked 23 Aug 2026. Source: https://relaypdf.com/pricing.
When to use which
Stay on WeasyPrint when the document is server-rendered HTML, the CSS is written for print, you need in-process generation or air-gapped hosts, or you already invested in at-page rules, running elements, and pdf_variant. Use RelayPDF when the same HTML is a real web page: scripts, webfonts, Flex, charts, wait-for-network, and you do not want a browser or Pango on the box. Mixed shops keep WeasyPrint for the static invoice and send the dashboard export to POST /v1/pdf. A useful check: print the same template in Chrome. If the PDF already looks right there, Chromium is the closer engine. If Chrome pagination is sloppy and your CSS is written around at-page margin boxes, WeasyPrint is doing a different job and a hosted browser API will not magically grow those print features.
Inputs differ in the boring ways that matter. WeasyPrint will read a local file and, unless you lock the fetcher down, local URIs. RelayPDF wants the HTML in the JSON body or a public URL. Markdown and published Handlebars templates are first-class on the same PDF route. WeasyPrint does not ship a template store; you render Jinja or Django first, then write_pdf. That is often the better design for a Python app. It is extra glue if the content already lives as a public page.