Generate PDFs in Django

Add a PDF download to an existing Django view, or generate a PDF from a template. The examples use PDFCrowd's HTML to PDF API and work for invoices, reports, receipts, and other documents built from application data.

Set up the client

Install the Python client:

pip install pdfcrowd

You can run the examples below with the included demo credentials, or replace them with your PDFCrowd username and API key.

Add a PDF download to an existing view

An existing view can display its usual HTML page and return a PDF when the user clicks a download button. Reuse the view's template, data preparation, and access checks.

Choose what to send to PDFCrowd:

  • Rendered HTML uses content Django has already prepared, such as an invoice for the logged-in user.
  • The page URL loads the page through its normal address.

To include changes made in the browser, such as filled-in form fields, consider WebSave as PDF in content mode.

Convert rendered HTML

This example extends an invoice view. get_invoice_for_user(user, pk) represents your application's existing invoice lookup and permission checks; substitute your own logic here.

import pdfcrowd
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render
from django.template.loader import render_to_string
from django.views.decorators.http import require_http_methods


@require_http_methods(["GET", "POST"])
def invoice_detail_view(request, pk):
    invoice = get_invoice_for_user(request.user, pk)
    context = {"invoice": invoice}

    if request.method == "POST" and "download_pdf" in request.POST:
        context["pdf_base_url"] = getattr(settings, "PDF_BASE_URL", None)
        html = render_to_string("invoices/detail.html", context, request=request)

        try:
            client = pdfcrowd.HtmlToPdfClient("demo", "demo")  # Username, API key
            client.setContentViewportWidth('balanced')
            pdf = client.convertString(html)
        except pdfcrowd.Error:
            # Add your application's error handling here.
            raise

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

    return render(request, "invoices/detail.html", context)

Add this form to the template, outside any existing form. Clicking its download button sends a POST request to the current view. The pdfcrowd-remove class keeps the button visible on the webpage but excludes it from the PDF.

<form method="post">
    {% csrf_token %}
    <button type="submit" name="download_pdf" value="1"
            class="pdfcrowd-remove">Download PDF</button>
</form>

GET requests continue to display the HTML page. A POST containing download_pdf renders the same template to an HTML string, converts it, and returns a PDF download. The pdf_base_url value supports the CSS and image setup below.

To display the PDF in the browser, change attachment to inline. Customize page size, margins, or other PDF options on the client before convertString().

The except block provides a place for your application's error handling. As written, raise propagates the original exception. See SDK error handling for details.

Make CSS and images available

When you send rendered HTML to PDFCrowd, relative paths to CSS and images need a base URL so PDFCrowd can locate the files and include them in the PDF.

Check STATIC_URL and the asset URLs in the rendered HTML. If all asset URLs are complete, such as https://cdn.your-site.example/static/style.css, or the template already provides a correctly configured <base> element, no additional setup is needed.

For relative paths, define the site or directory URL in your Django settings:

PDF_BASE_URL = "https://your-site.example/"

Replace https://your-site.example/ with your website's public URL, where PDFCrowd can access the assets. Then add this to the template's <head>, before stylesheet links:

{% if pdf_base_url %}
<base href="{{ pdf_base_url }}">
{% endif %}

The view passes this setting to the template as pdf_base_url when generating a PDF. The <base> element tells PDFCrowd where to resolve relative URLs. For example, /static/style.css becomes https://your-site.example/static/style.css. This applies to assets and hyperlinks.

You can also bundle the rendered HTML with local CSS, images, or fonts in a ZIP archive and pass its path to convertFile(). Keep paths consistent with the archive's folders and omit the website <base> so those paths resolve within the archive.

Convert the page URL instead

With URL conversion, PDFCrowd opens the page at the supplied address.

Keep the POST check, client initialization, error handling, and PDF response from the view above. Replace its HTML rendering and convertString() call with convertUrl():

pdf = client.convertUrl(request.build_absolute_uri())

This uses the current URL, including query parameters such as report filters. PDFCrowd makes a separate GET request, so the view runs its normal access checks and returns HTML. Only the original POST triggers PDF generation.

The server must be able to handle this GET while the original POST waits for conversion. Django's development server supports concurrent requests by default; running it with --nothreading can cause the conversion to time out.

PDFCrowd must be able to reach the URL. A localhost or 127.0.0.1 URL cannot be used to reach your development machine from PDFCrowd's servers.

PDFCrowd's request does not inherit the user's Django session. For protected pages, configure cookies or HTTP authentication as appropriate, or use the rendered-HTML approach above.

Generate a PDF from a template

For scheduled reports or invoice emails, you can generate a PDF directly from application data without a browser request. Use an existing Django template or a separate template designed for the document.

The function below renders a template with the supplied data and returns the PDF as bytes. It uses the same asset configuration as the view example:

import pdfcrowd
from django.conf import settings
from django.template.loader import render_to_string


def render_pdf(template_name, context):
    pdf_context = {
        **context,
        "pdf_base_url": getattr(settings, "PDF_BASE_URL", None),
    }
    html = render_to_string(template_name, pdf_context)
    client = pdfcrowd.HtmlToPdfClient("demo", "demo")  # Username, API key
    client.setContentViewportWidth('balanced')
    return client.convertString(html)

Pass all data the template needs in context; this function runs without request context processors. Conversion errors propagate to the calling job, which can use the same try/except pattern as the view.

Save or email the PDF

For example, given an invoice object, render it with invoices/detail.html and save the result through Django's storage API:

from django.core.files.base import ContentFile
from django.core.files.storage import default_storage

pdf = render_pdf("invoices/detail.html", {"invoice": invoice})
name = default_storage.save(
    f"invoices/{invoice.pk}.pdf", ContentFile(pdf)
)

Django storage returns the actual saved name, which can differ if the requested name already exists. For email delivery, attach the same bytes to an existing EmailMessage:

message.attach("invoice.pdf", pdf, "application/pdf")

Generate a batch of invoices

In a background job or management command, loop over the invoices selected by your application:

from django.core.files.base import ContentFile
from django.core.files.storage import default_storage

for invoice in invoices:
    pdf = render_pdf("invoices/detail.html", {"invoice": invoice})
    name = default_storage.save(
        f"invoices/{invoice.pk}.pdf", ContentFile(pdf)
    )

The same function can generate a single PDF when an invoice is issued.

WebSave alternative

For a download button without Django conversion code, see WebSave as PDF.