You do not need an HttpResponse to email a PDF from Django. Generate the PDF as bytes with Python-PDFKit, then attach those bytes to a Django EmailMessage using the filename invoice.pdf and MIME type application/pdf. An HTTP response is only needed when returning a file to a browser or another HTTP client.
Generate PDF bytes and attach them to a Django email
Python-PDFKit can return the rendered PDF content when you omit an output filename and pass False as the output path. Django’s EmailMessage.attach(filename, content, mimetype) accepts that content directly, so the basic flow stays in memory: render, attach, send.
import pdfkit
from django.core.mail import EmailMessage
html = "<h1>Invoice</h1><p>Example content</p>"
pdf_bytes = pdfkit.from_string(html, False)
email = EmailMessage(
subject="Your invoice",
body="Please find your invoice attached.",
from_email="billing@example.com",
to=["customer@example.com"],
)
email.attach("invoice.pdf", pdf_bytes, "application/pdf")
email.send()
This uses the documented PDFKit and Django APIs; it is not a claim that this particular snippet was executed. Install and configure the Python package and the wkhtmltopdf executable in the environment that runs the application. PDFKit is a wrapper: installing the Python package alone does not install the executable.
Use a real invoice template and data
In an application, build the HTML from a template rather than concatenating untrusted values into a string. For example, a Django view or background task can render a template with invoice data, then pass the resulting HTML to PDFKit:
Recommended Free Tools
#1 Best Overall
from django.template.loader import render_to_string
import pdfkit
html = render_to_string("billing/invoice.html", {"invoice": invoice})
pdf_bytes = pdfkit.from_string(html, False)
Here invoice represents an application object loaded and authorized by your own code. Keep template autoescaping enabled for user-provided text. If the document needs stylesheets, images, or fonts, make sure the renderer can resolve their URLs or provide assets in a form supported by your configured PDFKit/wkhtmltopdf setup. Missing assets can produce output that looks different from the browser preview.
What the arguments mean
pdfkit.from_string(html, False)asks PDFKit to return the output instead of writing it to a named output file. Retain the returned PDF data aspdf_bytes.EmailMessage(...)constructs a message suitable for attachments. Django describessend_mail()as a thin wrapper; useEmailMessagewhen you need advanced features such as file attachments.attach("invoice.pdf", pdf_bytes, "application/pdf")sets the attachment’s visible filename, content, and MIME type. Naming the file with a.pdfextension and specifyingapplication/pdfmakes its type explicit.email.send()hands the message to the configured Django email backend. The backend, its credentials, and delivery behavior are separate from PDF rendering.
Choose in-memory bytes or a file-backed attachment
Use the in-memory pattern when the generated document is a transient attachment and fits comfortably in the application’s memory. It avoids creating and cleaning up a temporary file. If the PDF already exists on disk, or your workflow needs to retain a generated artifact, attach the file by path instead.
| Approach | How to attach | Trade-off |
|---|---|---|
| In-memory PDF bytes | email.attach("invoice.pdf", pdf_bytes, "application/pdf") |
No temporary-file cleanup; the generated content occupies memory while it is processed. |
| Existing PDF file | email.attach_file(path, "application/pdf") |
Useful when a file already exists or is retained; the application owns file storage and cleanup. |
Attach an existing PDF file
Django provides EmailMessage.attach_file(path, mimetype) for a file-backed attachment. The MIME type can be provided explicitly; Django can also infer a type from a filename when it is omitted.
Rank #2
from django.core.mail import EmailMessage
email = EmailMessage(
subject="Your invoice",
body="Please find your invoice attached.",
from_email="billing@example.com",
to=["customer@example.com"],
)
email.attach_file("/srv/app/generated/invoice.pdf", "application/pdf")
email.send()
Choose a storage location with appropriate access controls, and define when the application deletes temporary files. Do not assume a file written to a worker’s local disk is durable or available to a different worker.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsKeep the mail attachment separate from an HTTP download
An HttpResponse is a Django response to an HTTP request. Its content-disposition header can tell a browser to download a PDF, but that does not make the response object the input required by Django’s mail attachment API. For an email, give EmailMessage.attach() the PDF content and MIME type. Use an HTTP response only in a separate endpoint whose job is to serve the PDF to a client.
Install and configure wkhtmltopdf in the runtime
PDFKit requires a working wkhtmltopdf executable. Its project documentation describes configuring the executable path when it is not available through the normal system path. Deployment must include a compatible binary and its operating-system dependencies; a developer workstation installation does not configure a production container, server, or task worker.
- Install the Python PDFKit package and a
wkhtmltopdfbinary compatible with the target operating system and architecture. - Check that the process running Django can locate and execute the binary. If it is outside the system path, configure PDFKit with the executable’s actual path according to the installed version’s usage documentation.
- Run a small render in the same runtime context as the mail-sending code, including the same container or worker configuration.
- Verify the generated bytes are a valid PDF and that required styles, images, and fonts are present before sending real messages.
Version compatibility matters. The PDFKit project page marks the project deprecated in line with wkhtmltopdf’s project status. The wkhtmltopdf downloads page lists 0.12.6 as its stable release, dated June 11, 2020. That release date is not evidence that a binary is suitable for every current operating system; verify architecture, dependencies, and behavior in the actual deployment.
Match Django attachment code to your installed release
The example uses the long-established EmailMessage.attach() and attach_file() interfaces. Django 5.2 documents these APIs and adds EmailAttachment support. Django 6.0 documents MIMEPart attachments and deprecates legacy MIMEBase attachments. Do not copy a release-specific attachment example without checking the version installed in your project; follow that release’s Django documentation and deprecation guidance.
Protect the renderer and the email workflow
wkhtmltopdf’s project explicitly warns against processing untrusted HTML or JavaScript without sanitizing user input, noting that doing so can lead to server takeover. Sanitization alone should not be treated as proof that arbitrary content is safe. Prefer templates you control, constrain user-supplied content, and assess the renderer’s isolation and network access. A renderer that can fetch remote resources may also reach locations your application did not intend it to access.
Rank #4
- Do not pass arbitrary user HTML or scripts directly to the renderer.
- Limit which network resources the renderer can access, especially when input can influence URLs.
- Run rendering with only the filesystem and process permissions it needs.
- Keep rendering failures distinct from email delivery failures in logs and error handling.
- For larger or slower jobs, consider generating and sending outside the web request path so a render does not hold an interactive request open.
The wkhtmltopdf project describes its old WebKit base as a concern and suggests considering other renderers for some use cases. It names WeasyPrint or Prince for controlled reports and Puppeteer for pages that depend on dynamic JavaScript. Those are alternatives to evaluate, not a claim that one renderer is universally faster, safer, or more compatible; no head-to-head performance test is established here.
Troubleshoot common failures
| Symptom | Likely cause | What to check |
|---|---|---|
| PDFKit cannot find or start wkhtmltopdf | The executable is absent, not on the runtime’s path, or incompatible with its environment. | Install a compatible binary in the deployment environment; check its path, permissions, architecture, and required dependencies from the same worker or container. |
| The email has no attachment or the attachment is not a PDF | The wrong value was passed as content, or the MIME type/filename is missing or incorrect. | Pass the return value from the PDFKit call to attach(); use a filename ending in .pdf and application/pdf. |
| The attachment is blank or missing images and styles | The HTML rendered, but linked assets were inaccessible or unavailable to wkhtmltopdf. | Check asset URLs, permissions, network access, and the renderer’s ability to load each required resource from the deployment environment. |
| The mail sends but the recipient cannot open the PDF | The content may not be valid PDF output, or the wrong content may have been attached. | Validate the generated file independently before sending; verify the exact bytes passed into the attachment call. |
| Rendering fails only for some customer data | Input may produce malformed markup, trigger unsafe behavior, or refer to unavailable resources. | Use controlled templates, validate data, inspect renderer errors, and avoid executing untrusted HTML or JavaScript. |
| The request times out while creating an email | Rendering and delivery are both occurring synchronously in a request path. | Separate rendering from interactive response handling where appropriate; measure the application’s own rendering and backend behavior rather than assuming a fixed duration. |
Or skip the browser setup
If the document is already available as a web page, ScreenshotNeo can capture a URL as an image or PDF; it is a screenshot API and MCP server, not a drop-in renderer for arbitrary Django template HTML or a substitute for the wkhtmltopdf byte-generation code above. One GET request can capture a URL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. ScreenshotNeo removes cookie/consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, and cache hits are not billed. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots. These details make it useful when the task is capturing a hosted page, rather than rendering a private invoice template directly.
Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month with no card.
FAQ
Does sending an email attachment require a public PDF URL?
No. Django’s attachment method accepts the content itself, so the in-memory approach does not require uploading the PDF to a public location.
Can I use this for a PDF generated from a private invoice template?
Yes. The PDFKit method renders HTML supplied by your application. Keep the template and its data handling under your control, and ensure any assets needed by the renderer are reachable in its runtime.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →

