HTML to PDF in Python
Convert HTML or a URL to PDF in Python with one HTTP call. Includes requests, httpx, Django and Flask examples, plus WeasyPrint, pdfkit and Playwright.
html-pdf.com renders your HTML in headless Chromium and returns a PDF. Flexbox, grid, web fonts and JavaScript work the way they do in Chrome. You don't need a browser or system packages on your server, just requests or httpx.
Quick answer. POST your HTML as JSON and write the response bytes to a file.
import os, requests
r = requests.post("https://html-pdf.com/v1/pdf",
headers={"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"},
json={"html": "<h1>Hello</h1>"}, timeout=60)
open("out.pdf", "wb").write(r.content)
Get an API key
One call gives you a key.
curl -X POST https://html-pdf.com/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
export HTML_PDF_KEY=hp_live_...
Open the link we email you to confirm your address. That adds 100 free credits, and 1 credit = 1 PDF page. You can also get a key at https://html-pdf.com/login.
Convert HTML to PDF with requests
This is the full example. It sends an HTML string, saves the PDF and prints your usage.
import os
import requests
API = "https://html-pdf.com/v1/pdf"
HEADERS = {"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"}
html = """
<html>
<head><style>body { font-family: sans-serif } h1 { color: #333 }</style></head>
<body><h1>Invoice #042</h1><p>Total: $120.00</p></body>
</html>
"""
resp = requests.post(API, headers=HEADERS, json={"html": html}, timeout=60)
resp.raise_for_status()
with open("invoice.pdf", "wb") as f:
f.write(resp.content)
print("pages:", resp.headers["X-Pages"], "credits left:", resp.headers["X-Credits-Remaining"])
Page size, margins and page numbers
You control the paper, margins and footer with extra fields in the same JSON payload.
payload = {
"html": html,
"format": "A4", # A3, A4, A5, A6, Letter, Legal, Tabloid
"landscape": False,
"margin": {"top": "15mm", "right": "15mm", "bottom": "20mm", "left": "15mm"},
"footer_html": (
'<div style="font-size:9px;width:100%;text-align:center">'
'Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>'
),
"max_pages": 20, # fail (without charge) if the PDF would be longer
"filename": "invoice.pdf",
}
resp = requests.post(API, headers=HEADERS, json=payload, timeout=60)
The header and footer need a margin to have room. For page breaks, use the CSS rules break-before: page and break-inside: avoid.
Convert a URL to PDF
Send url instead of html to print a live web page.
resp = requests.post(
API,
headers=HEADERS,
json={"url": "https://example.com", "media": "screen", "wait_delay": 2},
timeout=60,
)
This works for public pages only. For pages behind a login, render the HTML yourself (for example with your template engine) and send it as html.
Get a download link instead of bytes
Set response to get JSON back instead of the raw PDF.
resp = requests.post(API, headers=HEADERS, json={"html": html, "response": "url"}, timeout=60)
data = resp.json()
print(data["url"], data["expires_at"]) # link valid for 24 hours
Use "response": "base64" to get pdf_base64 in JSON instead.
Handle errors (including 402)
This helper turns API errors into clear Python exceptions. Errors are JSON shaped like {"error": {"code", "message", "hint"}}, and failed renders cost no credits.
def html_to_pdf(html: str, **options) -> bytes:
resp = requests.post(API, headers=HEADERS, json={"html": html, **options}, timeout=60)
if resp.ok:
return resp.content
err = resp.json().get("error", {})
if resp.status_code == 402: # insufficient_credits
raise RuntimeError(f"Out of credits: {err.get('hint')}")
if resp.status_code in (429, 503):
raise RuntimeError(f"Retry later: {err.get('code')}")
raise RuntimeError(f"{resp.status_code} {err.get('code')}: {err.get('message')}")
Async with httpx
With httpx you can render several PDFs at the same time.
import asyncio
import os
import httpx
async def main():
headers = {"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"}
async with httpx.AsyncClient(timeout=60) as client:
docs = {"a.pdf": "<h1>A</h1>", "b.pdf": "<h1>B</h1>"}
responses = await asyncio.gather(*[
client.post("https://html-pdf.com/v1/pdf", headers=headers, json={"html": h})
for h in docs.values()
])
for name, r in zip(docs, responses):
r.raise_for_status()
with open(name, "wb") as f:
f.write(r.content)
asyncio.run(main())
The rate limit is 120 requests per minute per account. For large batches, use an asyncio.Semaphore to limit concurrency.
Return a PDF from Django
This view renders a Django template and returns it as a PDF download.
# views.py
import os
import requests
from django.http import HttpResponse
from django.template.loader import render_to_string
def invoice_pdf(request, pk):
html = render_to_string("invoice.html", {"invoice_id": pk})
r = requests.post(
"https://html-pdf.com/v1/pdf",
headers={"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"},
json={"html": html, "format": "A4", "margin": "15mm"},
timeout=60,
)
r.raise_for_status()
response = HttpResponse(r.content, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="invoice-{pk}.pdf"'
return response
Return a PDF from Flask
This Flask route does the same thing and shows the PDF inline in the browser.
import os
import requests
from flask import Flask, Response, render_template
app = Flask(__name__)
@app.get("/invoice/<int:pk>.pdf")
def invoice_pdf(pk):
html = render_template("invoice.html", invoice_id=pk)
r = requests.post(
"https://html-pdf.com/v1/pdf",
headers={"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"},
json={"html": html, "format": "A4", "margin": "15mm"},
timeout=60,
)
r.raise_for_status()
return Response(r.content, mimetype="application/pdf",
headers={"Content-Disposition": f'inline; filename="invoice-{pk}.pdf"'})
Use absolute https:// URLs (or data: URIs) for images and stylesheets in your templates, because relative paths like /static/logo.png can't be resolved from the renderer.
Python alternatives: WeasyPrint, pdfkit, Playwright
This table compares the common Python libraries with the API, so you can pick the right tool.
| Library | How it works | Good for | Watch out for |
|---|---|---|---|
| WeasyPrint | Its own layout engine, written in Python | Print-focused documents, good @page and paged media support, no browser |
No JavaScript. Some CSS differs from browsers. Needs Pango and other system libraries. |
| pdfkit | Wrapper around the wkhtmltopdf binary | Legacy projects | wkhtmltopdf is archived (2023) and uses old QtWebKit, with no grid and weak flexbox. |
| Playwright / pyppeteer | Drives a real Chromium | Full browser fidelity, self-hosted | You install, run, patch and scale Chromium yourself. Memory per render, cold starts. |
| html-pdf.com API | Hosted headless Chromium | Chrome-accurate output without running a browser | Network call. Public URLs only for url mode. Billed per page. |
If you can't send documents to a third party, or need offline rendering, WeasyPrint or Playwright are good choices. If you want Chrome-accurate PDFs without running browsers, use the API.
Full reference: API docs · MCP server
FAQ
How do I convert HTML to PDF in Python?
POST your HTML to https://html-pdf.com/v1/pdf with requests, using json={"html": html} and your API key as a Bearer token. Then write resp.content to a .pdf file.
Is there a free HTML to PDF API for Python?
html-pdf.com gives you 100 free credits once, with no card needed. 1 credit = 1 PDF page, and failed renders cost nothing.
Does it support CSS page breaks?
Yes. Use break-before: page and break-inside: avoid in your CSS. Chromium also supports CSS @page margin boxes (Chrome 131+).
Do credits expire?
No. Purchased credits never expire. Packs go from $10 for 1,000 pages to $250 for 50,000 pages.
