Building a PDF feature?
Save this to your Work Desktop.

All Tutorials
PythonTutorial

HTML to PDF in Django

Generate PDFs from your Django views in 10 lines.

01. Install

pip install requests # Set your pdfmyhtml API key in settings.py or .env # PDFMYHTML_API_KEY=pmh_live_xxxxxxxxxxxxxxxxxxxx

02. Basic call

# views.py import os import requests from django.http import HttpResponse def invoice_pdf(request, invoice_id): html = render_to_string("invoice.html", {"invoice": invoice}) res = requests.post( "https://api.pdfmyhtml.com/v1/html-to-pdf", headers={"X-API-Key": os.environ["PDFMYHTML_API_KEY"]}, json={"html": html, "wait": True}, timeout=30, ) data = res.json() pdf = requests.get(data["download_url"]).content return HttpResponse(pdf, content_type="application/pdf")

03. Error handling

# views.py — robust version import requests, os, logging from django.http import HttpResponse, HttpResponseServerError logger = logging.getLogger(__name__) def invoice_pdf(request, invoice_id): try: res = requests.post( "https://api.pdfmyhtml.com/v1/html-to-pdf", headers={"X-API-Key": os.environ["PDFMYHTML_API_KEY"]}, json={"html": "<h1>Invoice</h1>", "wait": True}, timeout=30, ) res.raise_for_status() data = res.json() if data.get("status") != "COMPLETED": return HttpResponseServerError("PDF still processing — try /v1/jobs polling") pdf = requests.get(data["download_url"], timeout=30).content return HttpResponse(pdf, content_type="application/pdf") except requests.HTTPError as e: logger.error("pdfmyhtml HTTP %s: %s", e.response.status_code, e.response.text) return HttpResponseServerError("Could not generate PDF") except requests.Timeout: logger.error("pdfmyhtml timeout") return HttpResponseServerError("PDF service timeout")

04. Async pattern (high volume)

# Async pattern: enqueue + poll (use this for high-volume / long-running jobs) # Avoids holding a Django request open for 25s. import requests, os, time def enqueue_pdf(html: str) -> str: """Returns job_id immediately.""" res = requests.post( "https://api.pdfmyhtml.com/v1/html-to-pdf", headers={"X-API-Key": os.environ["PDFMYHTML_API_KEY"]}, json={"html": html, "wait": False}, timeout=10, ) return res.json()["job_id"] def get_pdf_url(job_id: str) -> str | None: """Returns download_url when ready, None if still processing.""" res = requests.get( f"https://api.pdfmyhtml.com/v1/jobs/{job_id}", headers={"X-API-Key": os.environ["PDFMYHTML_API_KEY"]}, ) data = res.json() if data["status"] == "COMPLETED": return data["download_url"] return None # Use Celery / Django-Q to poll without blocking your request workers.

05. Real-world example

# Stripe webhook → invoice PDF → email # Pair pdfmyhtml with django.core.mail for full automation. from django.core.mail import EmailMessage import requests, os from django.template.loader import render_to_string def send_invoice_email(stripe_invoice): html = render_to_string("invoices/template.html", { "number": stripe_invoice.number, "total": stripe_invoice.total / 100, "customer_email": stripe_invoice.customer_email, }) res = requests.post( "https://api.pdfmyhtml.com/v1/html-to-pdf", headers={"X-API-Key": os.environ["PDFMYHTML_API_KEY"]}, json={"html": html, "wait": True}, ) pdf_bytes = requests.get(res.json()["download_url"]).content email = EmailMessage( subject=f"Invoice #{stripe_invoice.number}", body="Your invoice is attached.", to=[stripe_invoice.customer_email], ) email.attach(f"invoice-{stripe_invoice.number}.pdf", pdf_bytes, "application/pdf") email.send()

Common gotchas

  • 01.Django's `render_to_string()` will not include external CSS unless the URLs in your template are absolute. Use `request.build_absolute_uri()` to build full URLs to your stylesheets if you reference them in the HTML.
  • 02.If you use Tailwind in your invoice template, build the CSS to a static file and inline it via `<style>` in the template — Tailwind's CDN script will not run inside Playwright headless Chromium reliably.
  • 03.Don't hold a request open for the full 25s sync wait under normal load — use the async enqueue+poll pattern with Celery for any production traffic above ~5 PDFs/sec.
  • 04.When deploying to managed hosts (Heroku, Render, Fly), set `PDFMYHTML_API_KEY` as an env var, not in `settings.py`.

Going deeper? Read the full guide on cost trade-offs vs self-hosted Headless Chrome.