Skip to content
CastPDF

Generate a PDF in Django and return it from a view

To generate a PDF in Django, post your data (or HTML from render_to_string) to CastPDF from a view, then return the bytes in an HttpResponse with content_type="application/pdf". There is no canvas code to write and no browser to host. It is the same PDF generation API behind every framework, called with requests.

  • Updated October 2026
  • 100 free PDFs a month

How Django projects usually make PDFs

Django has no PDF engine of its own, so every project picks a library. Django’s official how to guide on creating PDF files uses ReportLab, which is why so many codebases start there. Others prefer to keep using Django templates and convert the resulting HTML. Each route is valid; they differ in who can change the layout and what your deployment has to carry.

PDF options for a Django project
LibraryYou design withWorth knowing
ReportLabPython code: a canvas, or Platypus flowablesThe approach shown in Django’s own docs; precise and quick, but a designer cannot edit the layout as HTML
WeasyPrintDjango templates rendered to HTML and CSSExcellent paged media support; needs Pango and related system libraries in each image and runs no JavaScript
xhtml2pdfDjango templates, converted through ReportLabPure Python and easy to install; understands a limited subset of CSS
CastPDFDjango templates or a saved template filled with JSONChromium with a print engine on our side; one HTTPS request per document and a plan above the free allowance

If your PDFs are a few fixed forms and ReportLab already draws them, there is little reason to change. The pressure usually starts elsewhere: marketing wants the certificate to match the website, a report needs CSS grid, or the Docker image keeps breaking when the base layer updates its font and graphics libraries. Sending HTML to an API removes the native dependencies entirely, and the layout stays in the template language your team already writes.

Store the key in settings

Treat the API key like your SECRET_KEY: read it from the environment in settings.py and never commit it. Copy a test key from the dashboard (it starts with cpdf_test_); test documents are free, carry a watermark and never count towards your plan. Failing fast on a missing variable is kinder than discovering it on the first download.

settings.py (excerpt)
import os

# Raises KeyError at startup if the variable is missing, which is what you want.
CASTPDF_API_KEY = os.environ["CASTPDF_API_KEY"]
CASTPDF_CERTIFICATE_TEMPLATE_ID = os.environ.get("CASTPDF_CERTIFICATE_TEMPLATE_ID", "")

A view that returns a course certificate

Picture a training platform where students download a certificate after finishing a course. The view below only finds enrolments that belong to the signed in user and are completed, sends the details to the certificate starter template and returns the PDF as an attachment. Using the enrolment ID in the idempotency key means a student who clicks twice gets the same document, billed once.

courses/views.py
import logging

import requests
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import get_object_or_404

from .models import Enrollment

logger = logging.getLogger(__name__)
CASTPDF_URL = "https://api.castpdf.com/v1/pdf"


@login_required
def certificate_pdf(request, enrollment_id):
    enrollment = get_object_or_404(
        Enrollment.objects.select_related("course"),
        pk=enrollment_id,
        student=request.user,
        completed_on__isnull=False,
    )
    payload = {
        "template_id": settings.CASTPDF_CERTIFICATE_TEMPLATE_ID,
        "data": {
            "issuer": {"name": "Fernhill Academy", "short": "FA", "verify_url": "fernhill.example/verify"},
            "recipient": {"name": request.user.get_full_name()},
            "course": {
                "title": enrollment.course.title,
                "hours": enrollment.course.hours,
                "format": "Online, self paced",
                "grade": enrollment.grade,
            },
            "completed_on": enrollment.completed_on.isoformat(),
            "signatory": {"name": "Dr Amara Osei", "title": "Head of Learning"},
            "certificate_id": f"FA-{enrollment.pk:06d}",
        },
        "filename": f"certificate-{enrollment.pk}",
    }
    res = requests.post(
        CASTPDF_URL,
        headers={
            "Authorization": f"Bearer {settings.CASTPDF_API_KEY}",
            "Idempotency-Key": f"certificate-{enrollment.pk}",
        },
        json=payload,
        timeout=(10, 75),
    )
    if not res.ok:
        error = res.json().get("error", {})
        logger.error("CastPDF %s: %s", error.get("code"), error.get("message"))
        return HttpResponse("The certificate could not be created.", status=502, content_type="text/plain")

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

Wire it up with path("enrollments/<int:enrollment_id>/certificate.pdf", views.certificate_pdf, name="certificate-pdf") in your urls.py, and link to it with {% url "certificate-pdf" enrollment.pk %}. Change attachment to inline if you would rather the certificate open in a browser tab. HttpResponse is the right class here because the whole PDF is already in memory; FileResponse is meant for file objects you stream from disk or storage.

Render a Django template, then send the HTML

Saved templates suit documents that marketing or operations want to adjust without a deploy. For internal paperwork, you may prefer to keep everything in your repository and use the Django template language you already know. In that case, build the HTML with render_to_string and send it as html with no data key, so CastPDF prints it exactly as Django produced it. The example creates a class register for a tutor, with one row per attendee.

courses/pdf.py
import requests
from django.conf import settings
from django.template.loader import render_to_string


def class_register_pdf(session):
    html = render_to_string(
        "courses/pdf/class_register.html",
        {"session": session, "attendees": session.attendees.order_by("last_name")},
    )
    res = requests.post(
        "https://api.castpdf.com/v1/pdf",
        headers={"Authorization": f"Bearer {settings.CASTPDF_API_KEY}"},
        json={
            "html": html,
            "mode": "print",
            "format": "A4",
            "orientation": "landscape",
            "margins": "15mm",
            "filename": f"register-{session.pk}",
        },
        timeout=(10, 75),
    )
    res.raise_for_status()
    return res.content

"mode": "print" matters for registers and other long tables: the print engine repeats the <thead> at the top of every page and keeps rows whole. Put a <style> block in the template itself, or link a stylesheet by its full public https URL. Paths like /static/css/print.css are relative to your site, which the renderer cannot see, and addresses such as 127.0.0.1 used by runserver are refused on purpose. Because Django templates and Liquid share the {{ }} syntax, never add a data key to a request whose HTML Django already rendered.

Long documents belong in a task queue

A synchronous view is fine for a one page certificate. For a 30 page course report, move the call into a Celery or RQ task, save the bytes with default_storage.save() and let the page poll or show a link once the file exists. Alternatively, ask for "response": "url" and store the signed link CastPDF returns on the model; it works without a key until its expires_at, which follows your plan’s retention period. Keep the same idempotency key across task retries so a crashed worker never produces a duplicate.

The certificate request as JSON
{
  "template_id": "2c9d7e1f-3a4b-4c5d-8e6f-9a0b1c2d3e4f",
  "data": {
    "issuer": { "name": "Fernhill Academy", "short": "FA", "verify_url": "fernhill.example/verify" },
    "recipient": { "name": "Tomasz Nowak" },
    "course": { "title": "Applied Data Protection", "hours": 24, "format": "Online, self paced", "grade": "Distinction" },
    "completed_on": "2026-09-28",
    "signatory": { "name": "Dr Amara Osei", "title": "Head of Learning" },
    "certificate_id": "FA-004417"
  },
  "filename": "certificate-4417",
  "response": "url"
}

Production checklist for Django

  • Always set a timeout on requests.post. Without one a stuck connection holds a Gunicorn worker indefinitely. A short connect limit and a read limit around 75 seconds works well.
  • Check your Gunicorn --timeout (30 seconds by default) and any proxy timeout in front of it. A render may take up to 30 seconds, so long documents should run in a task, not in the request.
  • Keep CASTPDF_API_KEY out of settings.py literals, fixtures and Git history. Load it from the environment or a secrets manager, and switch from the test key to a cpdf_live_ key at launch.
  • Use select_related and prefetch_related when building the data, so a 200 row report does not run 200 queries before the PDF request even starts.
  • In tests, patch the HTTP call (for example with the responses library or unittest.mock) and return a small fixture PDF, so CI never needs a key.
  • Respect the per document limits of 50 pages and 40 MB. Photos uploaded to an ImageField are often full resolution, so serve a resized rendition in the PDF.

Troubleshooting Django PDF views

Problems Django developers meet and their fixes
SymptomLikely causeFix
The PDF has no stylingThe stylesheet was linked with a relative /static/ path or a localhost addressInline the CSS or link a public https URL; check blocked_resources in a url response
template_render_error (422)Django template tags such as {% url %} were sent to Liquid inside a saved templateRender Django templates locally and send html without data
WORKER TIMEOUT in the Gunicorn logThe worker timeout is shorter than the render plus network timeRaise the timeout or move the document to a Celery task
invalid_api_key (401)The environment variable is set in your shell but not for the serviceSet it in the process manager, container or platform settings
content_lost (422)Print mode found a wide table or image that would be cut offLet cells wrap, reduce widths or switch to landscape
The browser saves the file as certificate_pdfNo Content-Disposition filename was setAdd attachment; filename="..." to the response

The quickstart shows how to create keys and pick a starter, and the API reference documents every field used above. For Python outside Django, including httpx and retry helpers, read the Python page. The Laravel page shows the same controller and queue patterns in PHP.

FAQ

Common questions

Why does the Django documentation use ReportLab for PDFs?

ReportLab is a mature Python library that needs no external programs, so it makes a dependable example. It suits layouts you are happy to describe in code. For HTML designs, rendering a template and sending it to an API is usually less work.

Can I use my existing Django templates for PDFs?

Yes. Call render_to_string with your template and context, then send the resulting HTML without a data field. The template language stays the same; only the final step changes.

Should a Django view return HttpResponse or FileResponse for a PDF?

Use HttpResponse when the PDF bytes are already in memory, as they are after the API call. FileResponse is designed for streaming file objects from disk or storage. Both need the application/pdf content type.

Does this work with Django REST Framework?

Yes. Return a plain Django HttpResponse from the API view for the PDF itself, or return the signed url and expires_at in a normal serializer response. Keep the CastPDF key on the server either way.

Can I generate PDFs from the Django admin?

Yes. Add an admin action that calls the same helper for the selected objects and returns the file, or queue a task when many objects are selected. Admin users never see the API key.

Do I still need WeasyPrint system packages in my Docker image?

No. Once PDFs come from the API, you can remove Pango and the related text and font libraries from the image. That usually makes builds faster and the image smaller.

Make your first PDF in 5 minutes

Pick a template, add your details and download your PDF. You get 100 free PDFs every month, and you don’t need a card.