How to Generate PDF Invoices from HTML with PDFCrowd in a Django App
Render invoice HTML with Django, convert it to PDF using PDFCrowd’s Python client, and return, store, or email the PDF bytes safely.
For an invoice whose data and HTML template live in your Django app, render the template to an HTML string with Django, pass that string to PDFCrowd’s official Python client, and return the resulting PDF bytes in an application/pdf response. The same conversion helper can also feed Django storage, email attachments, or a background batch job.
This guide follows PDFCrowd’s official Django integration guide and Python client documentation. The examples use settings for credentials, preserve invoice authorization in the view, and keep PDF conversion on the server. API options and endpoint versions can change, so check the current documentation before changing your integration.
1. Install the client and configure credentials
Install the package in the environment used by your Django application:
pip install pdfcrowd
Keep the PDFCrowd account username and API key in deployment configuration or a secret manager. Do not put production credentials in a template, JavaScript bundle, URL, or source repository. For example, read environment variables in your Django settings:
# settings.py
import os
PDFCROWD_USERNAME = os.environ["PDFCROWD_USERNAME"]
PDFCROWD_API_KEY = os.environ["PDFCROWD_API_KEY"]
Set those environment variables in the application’s deployment environment. PDFCrowd’s sample credentials are for trying examples; use the credentials for your own account in production.
2. Render the invoice template and convert it
Render with render_to_string when the app owns the invoice data and layout. A reusable helper keeps conversion separate from how the resulting bytes are delivered:
# invoices/pdf.py
import pdfcrowd
from django.conf import settings
from django.template.loader import render_to_string
def render_invoice_pdf(invoice, *, request=None):
context = {"invoice": invoice}
if request is not None:
context["request"] = request
html = render_to_string("invoices/invoice.html", context)
client = pdfcrowd.HtmlToPdfClient(
settings.PDFCROWD_USERNAME,
settings.PDFCROWD_API_KEY,
)
# Optional: choose a viewport mapping that suits the invoice CSS.
# The exact accepted values and behavior are documented by PDFCrowd.
client.setContentViewportWidth("balanced")
return client.convertString(html)
Then use the helper in a view. Fetch the invoice through your application’s normal access-control path before conversion:
# invoices/views.py
import pdfcrowd
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from .models import Invoice
from .pdf import render_invoice_pdf
@require_POST
def download_invoice_pdf(request, invoice_id):
# Replace this lookup with the app's normal tenant and permission checks.
invoice = get_object_or_404(
Invoice.objects.filter(account=request.user.account),
pk=invoice_id,
)
try:
pdf_bytes = render_invoice_pdf(invoice, request=request)
except pdfcrowd.Error:
# Log the exception with a request/correlation ID in production.
# Do not return provider credentials or internal exception details.
return HttpResponse(
"The invoice PDF could not be generated. Please try again later.",
status=502,
content_type="text/plain; charset=utf-8",
)
response = HttpResponse(pdf_bytes, content_type="application/pdf")
response["Content-Disposition"] = (
f'attachment; filename="invoice-{invoice.pk}.pdf"'
)
response["Cache-Control"] = "private, no-store"
response["Accept-Ranges"] = "none"
return response
Adapt the account lookup to your actual model and authorization rules. If the download is initiated by a form, use the app’s intended HTTP method and CSRF protection. PDFCrowd’s Django example posts from a form with a CSRF token and reuses the view’s invoice lookup and permission checks.
render_to_string does not automatically get request context processors in the way a request-based template render does. Add every required value explicitly to the context. If the template depends on request context processors, pass the request using the supported request argument for your Django version, or add the required values yourself. Check the installed Django documentation for the exact render_to_string signature.
3. Make invoice assets and print layout work
PDFCrowd must be able to load stylesheets, fonts, and images referenced by the HTML. For remotely hosted assets, use absolute URLs or an HTML <base> element so relative paths resolve. A URL hosted on your app’s localhost is not reachable from PDFCrowd’s servers. For local assets, package the HTML and its resources together in a supported archive and submit the archive contents; putting a local filename in a text field does not upload the file.
For invoice layouts, begin with print CSS, page size, margins, and page-break rules. For example:
@media print {
.screen-only { display: none !important; }
.invoice-items { break-inside: auto; }
.invoice-item { break-inside: avoid; }
.invoice-footer { break-inside: avoid; }
}
PDFCrowd documents controls for page size, orientation, margins, page breaks, headers and footers, print CSS, custom CSS, JavaScript readiness, viewport behavior, remote fonts and images, watermarks, password protection, PDF/A, and tagged PDF output. Set options through the Python client methods documented for your installed client version. Avoid copying option names from another SDK without checking the Python documentation.
The optional setContentViewportWidth('balanced') call chooses a viewport mapping for the HTML-to-PDF conversion. Use an exact viewport width if the invoice’s responsive CSS must map from a known web width. Review representative output after changing this setting, because responsive breakpoints affect layout.
4. Choose the right input route
| Input | Use it when | Important constraint |
|---|---|---|
| Rendered HTML string | Django has invoice data and a custom template | Referenced assets must be reachable or packaged with the HTML. |
| URL | The page is already hosted and reachable by PDFCrowd | A private development URL or localhost cannot be fetched by the service. |
| Uploaded HTML file or archive | HTML and local assets should travel together | Submit file contents using the supported upload flow; a local path string is not an upload. |
| Invoice PDF API | Your input is structured billing data for the provider’s predefined invoice model | It is a different input model from a custom Django HTML/CSS template. |
For the title’s use case, the HTML string is usually the direct route: Django has already applied application logic and rendered the invoice. PDFCrowd also describes WebSave as PDF for a visitor saving a page they are viewing; that is a different workflow from generating a server-side invoice.
5. Return, store, email, or batch the PDF bytes
The client’s convertString call returns PDF bytes. The view above sends them directly to the browser. To store them through Django’s configured storage backend, use the returned storage name because the backend can change the requested name to avoid a collision:
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
pdf_bytes = render_invoice_pdf(invoice)
stored_name = default_storage.save(
f"invoices/invoice-{invoice.pk}.pdf",
ContentFile(pdf_bytes),
)
To attach the same bytes to an email:
from django.core.mail import EmailMessage
pdf_bytes = render_invoice_pdf(invoice)
message = EmailMessage(
subject=f"Invoice {invoice.pk}",
body="Your invoice is attached.",
to=[invoice.billing_email],
)
message.attach(
f"invoice-{invoice.pk}.pdf",
pdf_bytes,
"application/pdf",
)
message.send()
For a batch, call the conversion helper from a background task rather than holding a browser request open for many invoices. Record which invoice failed, apply the job system’s retry policy, and make storage or email delivery idempotent so a retry does not create duplicate side effects.
6. HTTP API equivalent
The HTTP API uses a versioned endpoint, HTTP Basic authentication with the PDFCrowd username and API key, and form fields rather than a JSON request body. Keep the API version explicit and consult the current versioning documentation before changing it. A successful conversion returns 200 OK with PDF bytes. The ?errfmt=json query option changes error formatting; it does not change successful PDF responses. Use the official client for the Django path unless you have a reason to manage the HTTP request yourself.
For complete request fields and the current endpoint, refer to PDFCrowd’s HTTP API guide and HTTP API reference. Do not substitute a JSON body or a local filename for the documented form/upload inputs.
7. Error handling, reliability, and cost
PDFCrowd’s examples catch pdfcrowd.Error. Decide how errors should surface before shipping: a user-facing download can return a controlled error, while a background task should log enough context to diagnose the invoice and then follow the queue’s retry policy. Do not expose credentials, raw provider responses, or sensitive invoice data in browser error messages or logs.
Conversion depends on a network call to the service and on external assets loading. Keep authorization checks before conversion so unauthorized requests cannot cause work on another user’s invoice. For high-volume work, move conversion off the request path and define timeouts, retry limits, and duplicate handling in the job system. No performance, availability, or price figures are asserted here; check PDFCrowd’s current documentation and account terms for operational and cost details.
For invoices spanning multiple pages, test representative cases in your own application: long item descriptions, many rows, non-ASCII names and addresses, missing optional fields, and page breaks near totals or footers. These are validation cases to run for your templates, not results claimed by this guide.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Authentication error | Wrong account username or API key, or unset deployment configuration | Check the server-side settings and account credentials; never send them to the browser. |
| Images, styles, or fonts are missing | Relative asset paths resolve incorrectly, or the service cannot reach a private/local asset URL | Use absolute reachable URLs, an appropriate <base>, or package local resources with the HTML in a supported archive. |
| Page looks different from the browser | Responsive CSS uses a different viewport, print rules differ, or assets/JavaScript are not ready | Set the documented viewport behavior, use print CSS, and configure documented readiness options where needed. |
| Download returns an HTML error instead of a PDF | The view caught a conversion exception and returned its controlled error response | Inspect server-side logs and the provider exception; return or display the appropriate retry message. |
| Invoice is generated for the wrong user or account | The view did not use the application’s normal authorization path | Scope the invoice query by tenant/user and check permissions before calling PDFCrowd. |
| Repeated jobs create duplicate files or email | A background retry repeated a side effect | Make job operations idempotent and track completion against the invoice/job identifier. |
| Request times out during a large batch | Many conversions are running synchronously in the web request | Queue batch conversion as background work and expose job status through the app’s existing workflow. |
9. Skip browser setup for a screenshot of an invoice page
If the requirement is a screenshot of a rendered invoice page rather than a paginated PDF document, ScreenshotNeo provides a website screenshot API and MCP server. A screenshot is an image capture; it does not replace HTML-to-PDF pagination for a multi-page invoice.
Or skip the browser setup:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Replace the example URL with a page the service can access, and keep the access key server-side. See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for free.
10. FAQ
Should I render the invoice in Django or pass PDFCrowd a URL?
Render it in Django when the app owns the invoice data and template. Use a URL only when the page is reachable by PDFCrowd and URL-based rendering fits the workflow.
Can the conversion function be reused outside a download view?
Yes. Return the bytes from a shared helper, then pass them to an HTTP response, Django storage, an email attachment, or a background task.
Is this the same as PDFCrowd’s Invoice PDF API?
No. This guide uses HTML-to-PDF for a custom HTML/CSS template. The separate Invoice PDF API is aimed at structured billing data and a predefined invoice model.
Does PDFCrowd provide Django invoice authorization?
No. Enforce access to each invoice in your application before starting conversion.


