# 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.