# 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": "<h1>Hello</h1>"}, 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 = """
<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.

```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": (
        '<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.

```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": "<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.

```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/<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](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.

