# 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.
```python
import os, requests
r = requests.post("https://html-pdf.com/v1/pdf",
headers={"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"},
json={"html": "
Hello
"}, timeout=60)
open("out.pdf", "wb").write(r.content)
```
## Get an API key
One call gives you a key.
```bash
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.
```python
import os
import requests
API = "https://html-pdf.com/v1/pdf"
HEADERS = {"Authorization": f"Bearer {os.environ['HTML_PDF_KEY']}"}
html = """
Invoice #042
Total: $120.00
"""
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.
```python
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": (
''
'Page of
'
),
"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.
```python
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.
```python
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.
```python
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.
```python
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": "A
", "b.pdf": "B
"}
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.
```python
# 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.
```python
import os
import requests
from flask import Flask, Response, render_template
app = Flask(__name__)
@app.get("/invoice/.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](https://html-pdf.com/docs) ยท [MCP server](https://html-pdf.com/mcp)
## 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.