HTML to PDF in Python

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.

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.

This page as markdown · text