Renderpaper
Log in Start free

Django HTML to PDF

The Django answers are django-weasyprint, xhtml2pdf and django-wkhtmltopdf, and each asks for something at the system level. WeasyPrint needs Pango, cairo and GDK-PixBuf present in every environment — which is fine locally and an afternoon on a slim container image. xhtml2pdf is pure Python and supports a subset of CSS 2.1, so anything built this decade does not survive it. django-wkhtmltopdf shells out to a binary whose engine predates flexbox.

This needs nothing installed beyond requests.

Render a Django template

render_to_string gives you the HTML; the API gives you the PDF.

import os

import requests
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string

from .models import Invoice


def invoice_pdf(request, invoice_id):
    invoice = get_object_or_404(Invoice, pk=invoice_id)

    html = render_to_string(
        "invoices/show.html",
        {"invoice": invoice},
        request=request,
    )

    response = requests.post(
        "https://renderpaper.com/v1/render",
        headers={"X-API-Key": os.environ["RENDER_API_KEY"]},
        json={"template": html},
        timeout=60,
    )

    if response.status_code != 200:
        return HttpResponse("Could not render the document", status=502)

    pdf = HttpResponse(response.content, content_type="application/pdf")
    pdf["Content-Disposition"] = f'attachment; filename="invoice-{invoice_id}.pdf"'
    return pdf

Wire it up as usual:

from django.urls import path

from . import views

urlpatterns = [
    path("invoices/<int:invoice_id>/pdf", views.invoice_pdf, name="invoice-pdf"),
]

Passing request= to render_to_string matters if your template uses {% url %} or context processors — without it those resolve differently or fail.

Static files and images

Django serves CSS and images at URLs the renderer cannot reach if they are behind your authentication, or on localhost during development. Two options that both work:

Inline the stylesheet into the template used for PDFs:

{% load static %}
<style>{% include "invoices/pdf.css" %}</style>

Or embed images as data URIs so the document carries everything it needs:

import base64
from pathlib import Path


def data_uri(path: Path) -> str:
    encoded = base64.b64encode(path.read_bytes()).decode("ascii")
    return f"data:image/png;base64,{encoded}"

A self-contained document is also faster to render, because nothing has to be fetched while the page is being laid out.

Stored templates: send data, not markup

Rendering a Django template keeps the document in your repository, so a design change is a deploy. Store it once and post data.

import os

import requests

TEMPLATE_ID = "your-template-id"

response = requests.post(
    f"https://renderpaper.com/v1/templates/{TEMPLATE_ID}/render",
    headers={"X-API-Key": os.environ["RENDER_API_KEY"]},
    json={
        "data": {
            "InvoiceNumber": "2026-0142",
            "Customer": {"Name": "Northwind Trading AB"},
            "LineItems": [
                {"Description": "Consulting", "Quantity": 12, "Amount": 14400},
                {"Description": "Hosting", "Quantity": 1, "Amount": 4350},
            ],
            "Total": 18750,
        }
    },
    timeout=60,
)
response.raise_for_status()

with open("invoice.pdf", "wb") as f:
    f.write(response.content)

The stored template is a Go html/template. The loop reads much like Django's:

<h1>Invoice {{.InvoiceNumber}}</h1>
<p>{{.Customer.Name}}</p>
<table>
  {{range .LineItems}}
  <tr><td>{{.Description}}</td><td>{{.Quantity}}</td><td>{{.Amount}}</td></tr>
  {{end}}
</table>

Do it in Celery for anything slow

A render takes a second or two, which is too long to hold a worker for a batch:

import os

import requests
from celery import shared_task
from django.core.files.base import ContentFile
from django.template.loader import render_to_string

from .models import Invoice


@shared_task
def render_invoice(invoice_id: int) -> None:
    invoice = Invoice.objects.get(pk=invoice_id)
    html = render_to_string("invoices/show.html", {"invoice": invoice})

    response = requests.post(
        "https://renderpaper.com/v1/render",
        headers={"X-API-Key": os.environ["RENDER_API_KEY"]},
        json={"template": html},
        timeout=120,
    )
    response.raise_for_status()

    invoice.pdf.save(f"invoice-{invoice_id}.pdf", ContentFile(response.content))

Why not django-weasyprint

WeasyPrint's CSS Paged Media support is better than Chromium's — named pages, margin boxes, running elements — and for print-first typography that matters.

The trade is the rest of CSS and the install. Grid is unsupported and flexbox arrived late, so a layout built with modern tools does not survive. And Pango and cairo have to be present in every environment you deploy to, which is a class of deployment problem a pure-Python dependency does not have.

Why not xhtml2pdf

Pure Python and no system libraries, which is genuinely appealing. It targets a subset of CSS 2.1, so it suits a fixed, simple document generated entirely by your code and nothing that a designer will open.

Try it without a key

https://renderpaper.com/sample.pdf is a real render, no account. The free tier is 50 documents a month with no card.

Last updated 2026-08-08.