Blog/SDKs

HTML to PDF in Python

SDKs··6 min read

The documented html to pdf python api path is pip install relaypdf, then client.pdf.from_html. The official client is stdlib-only (urllib), Python 3.10+, package 0.1.2 on PyPI as of 22 Aug 2026. You send UTF-8 HTML in JSON to POST /v1/pdf. Chromium runs on RelayPDF, not in your process. Method names are snake_case. JSON option keys stay camelCase (printBackground, sourceFilename). Full method table: https://relaypdf.com/docs/sdks/python.

WeasyPrint and pdfkit stay in-process. WeasyPrint paints with Pango and Cairo and has no JavaScript engine. pdfkit shells out to a wkhtmltopdf binary. Those are real tools. They are not the same contract as an HTTP client that posts HTML and writes bytes.

Install the client

Install from PyPI. uv add relaypdf is also documented. Do not invent extra native packages for this SDK. The README states the client uses urllib only. api_key is required. Missing it raises TypeError. Load the key from the environment written by npx @relaypdf/cli setup, or from Dashboard API keys (prefix pdf_live_).

pip install relaypdf
import os
from relaypdf import RelayPDF
client = RelayPDF(api_key=os.environ["RELAYPDF_API_KEY"])
# client = RelayPDF(api_key=..., base_url="http://localhost:8787")

Default base URL is https://api.relaypdf.com. Override base_url for a local server. opener injects urlopen in tests. The SDK does not retry. User-Agent is relaypdf-python/0.1.2 (+https://relaypdf.com).

from_html

client.pdf.from_html(html, **extra) is the helper for POST /v1/pdf with an html field. html is a UTF-8 string, not Base64. filename, if you send it, must end in .pdf. Default response is binary. BinaryResult has id, filename, size_bytes, content_type, bytes, and save(path).

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")
print(pdf.id, pdf.size_bytes, pdf.content_type)

printBackground defaults to true on the API. format defaults to letter; A4 is a documented option. options.timeout is the Chromium budget, max 60000 ms. waitUntil defaults to networkidle0. waitForSelector and waitForTimeout (max 30000 ms) exist if a late chart is still empty. headerTemplate and footerTemplate are Chromium margin fragments, not a copy of a body header. Product page: https://relaypdf.com/html-to-pdf. Options: https://relaypdf.com/docs/options.

client.pdf.create(**input) is the same endpoint with a raw body. Use it when you already have a dict. from_url, from_markdown, and from_template are the other exclusive sources. Mixing html with url or templateId in one request is invalid_request.

URL, Markdown, templates

from_url fetches a public HTTPS page. Private, loopback, and metadata hosts return url_not_allowed. If the page sits behind login, render HTML in your app and call from_html. from_markdown sends GitHub-flavoured Markdown; the server turns it into HTML and prints it. from_template takes a published template id or slug plus a dict. strict=True fails the job if a Handlebars path is missing.

pdf = client.pdf.from_url(
    "https://example.com",
    filename="page.pdf",
    options={"format": "A4", "printBackground": True},
)
md = client.pdf.from_markdown("# Hello\n\nFrom **Markdown**.")
invoice = client.pdf.from_template(
    "invoice",
    {"number": "INV-1042", "total": 1458},
    filename="invoice.pdf",
    strict=True,
)

binary, url, async

response is binary (default), url, or async, same as REST. UrlResult has url and expires_at (24 hours). GET /v1/files/:id does not send the API key. response="async" returns AsyncResult; poll with client.jobs.wait(id) or client.jobs.get(id). jobs.wait defaults interval_ms=1000 and timeout_ms=120000. Then client.files.download(id) and save.

url_result = client.pdf.from_html("<h1>Hi</h1>", response="url")
print(url_result.url, url_result.expires_at)
job = client.pdf.from_html("<h1>Batch</h1>", response="async")
done = client.jobs.wait(job.id)
file = client.files.download(done["id"])
file.save("batch.pdf")

The PyPI README async sample uses client.convert.from_path for an Office file. The same response="async" plus jobs.wait plus files.download sequence applies to from_html. callbackUrl must be public HTTPS if you want a webhook instead of a poll.

WeasyPrint and pdfkit

WeasyPrint is a Python library that implements CSS Paged Media. Official install docs require system cairo, Pango, and GDK-PixBuf in addition to pip. There is no browser. JavaScript does not run. Flex and modern CSS coverage is the WeasyPrint layout engine, not Chrome. That is why invoices that are static HTML and CSS often look tight, and why a React chart will not appear unless you already baked it into the HTML.

pdfkit (often installed as python-pdfkit) is a wrapper. It expects a wkhtmltopdf binary on the machine. wkhtmltopdf is a Qt WebKit print path. The upstream GitHub org archived the project in January 2023. Flexbox and grid are weak or absent on that engine. New work that still needs that binary is a maintenance choice, not a feature upgrade.

RelayPDF does not replace those libraries inside your venv. It moves the print to a hosted Chromium (or, if you ask for it, a hosted wkhtml path). The Python process holds an HTTP client. You do not install Pango, you do not ship a wkhtmltopdf layer, and you do not launch Chrome in the web worker.

relaypdf SDKWeasyPrintpdfkit
Installpip install relaypdfpip + cairo/Pango/GDK-PixBufpip + wkhtmltopdf binary
Where it printsPOST /v1/pdf (hosted Chromium)In-processLocal wkhtmltopdf
JavaScriptChromiumNoneOld WebKit JS
AuthBearer api_keyNone (local)None (local)
BilledSuccessful HTML PDF $0.015Your CPUYour CPU

If you need the old engine on purpose, the SDK exposes client.convert.wkhtml(**input) as POST /v1/convert. That is a billed convert ($0.025), not from_html. client.convert.from_html(html, to="docx") is LibreOffice, also a different endpoint ($0.04). Do not confuse those with client.pdf.from_html.

Keep WeasyPrint when the document is CSS-only, you already have the native stack on every host, and you cannot send HTML off-box. Keep pdfkit only if an existing template is locked to wkhtmltopdf flags you do not want to rewrite. Use the SDK when the page needs a current browser, when you do not want a browser or a GTK stack in the deploy, or when the same app also needs from_url, merge, or templates.

Errors and account

HTTP failures raise RelayPDFError with status, code, message, and optional retry_after. The SDK does not retry 429 for you; honor retry_after. Failed operations are not billed. Trial rate is 20/min; funded or auto-reload is 60/min; burst is 5 / 10s.

from relaypdf import RelayPDF, RelayPDFError
try:
    client.pdf.from_url("https://example.com")
except RelayPDFError as err:
    print(err.status, err.code, err.message, err.retry_after)

Documented codes you will actually see on a bad HTML job: invalid_request, payload_too_large, unauthorized, payment_required, rate_limited, render_failed. client.health() is unauthenticated. client.account() returns plan, rate tier, and wallet.balanceMillicents and is not billed. 1 millicent = $0.001. New accounts get a $5.00 trial (5000 millicents).

What this client also covers

The same package is not HTML-only. client.images.from_html and from_url hit POST /v1/images. client.pdf.merge, stamp, extract, protect, and the rest of the /v1/pdf/* tools are on the client. client.convert.from_path reads a local Office file and sets sourceFilename. client.barcodes.qr, zip.create, templates.list/gallery/get/create/update/delete/publish, webhooks.list/create/delete, and verify_webhook are in 0.1.2. Node still has extra template helpers (preview, generate, versions) that Python has not shipped; those REST paths remain available over raw HTTP.

Do not put the API key in a query string. Do not Base64 the HTML for from_html. Do not send html and templateId together. filename must end with .pdf. Private URLs are rejected. verify_webhook needs the raw body bytes or str and the RelayPDF-Signature header, plus RELAYPDF_WEBHOOK_SECRET, not the API key.

Start with from_html, save the BinaryResult, then add options only as the layout needs them. Method names and error fields: https://relaypdf.com/docs/sdks/python. REST twin: https://relaypdf.com/docs. Endpoint: POST https://api.relaypdf.com/v1/pdf.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.