Tutorials

Python Create PDF: Production API Guide for Developers

Learn how to create PDF files with Python using APIs, Chromium rendering, templates, and production fixes for fonts, SSRF, and page breaks.

M Mikel Rougstone · 27 September 2026 · 7 min read
Python Create PDF: Production API Guide for Developers

TL;DR

  • Python create PDF workflows can use a hosted API that accepts HTML, a URL, or one of 40 ready document templates and returns a finished PDF from a single request.
  • PDFGeny uses headless Chromium as the default renderer, with WeasyPrint and Ghostscript for PDF/A-2b output. The API endpoint is POST https://pdfgeny.com/api/v1/render, with a median render time of 0.6 seconds.
  • A local PDF library is often the better choice for a handful of documents per day. The trade-off changes when teams must maintain browsers, fonts, containers, and security controls in production.
  • PDFGeny provides a free plan with 50 documents a month and no card required. Overage is $0.009 per document.

How to create a PDF in Python: the direct answer

Python create PDF tasks usually fall into two paths: generate files locally with a library, or send document data to a rendering service. For production applications that need invoices, receipts, certificates, contracts, reports, or labels, PDFGeny creates PDFs through a single API request to POST https://pdfgeny.com/api/v1/render and returns the generated document. The service reports a median render time of 0.6 seconds.

The correct approach depends on volume and control requirements. A script producing a few internal reports each day can be better served by a local library such as ReportLab. A SaaS product generating customer-facing invoices needs to consider browser dependencies, font loading, security boundaries, and deployment maintenance.

A PDF generator is not just a file writer. It is a document rendering system with browser behavior, CSS rules, fonts, network access, and storage decisions.

Python API example with a real request

PDFGeny accepts document input and returns a finished PDF. The following Python example uses the standard requests package.

import requests

api_key = "YOUR_API_KEY"

payload = {
    "html": """
    <html>
      <body>
        <h1>Invoice #1001</h1>
        <p>Amount due: $250</p>
      </body>
    </html>
    """
}

response = requests.post(
    "https://pdfgeny.com/api/v1/render",
    json=payload,
    headers={
        "Authorization": f"Bearer {api_key}"
    }
)

response.raise_for_status()

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

Choosing between a PDF library and a PDF API

The common assumption is that developers should always keep PDF generation inside their own infrastructure. That assumption fails when the rendering stack becomes more complicated than the document itself.

Failure mode: browser maintenance becomes the hidden project

Headless Chromium is powerful because it understands modern HTML and CSS. The operational cost appears when teams must package Chromium versions, install fonts, manage sandbox settings, and handle memory behavior inside containers.

PDFGeny uses headless Chromium as its default engine and provides WeasyPrint as a second rendering engine. Ghostscript handles PDF/A-2b output for archival requirements.

ApproachBest fitTrade-off
Local librarySmall scripts and low document volumeApplication owns rendering behavior and dependencies
Headless browser setupTeams needing full browser rendering controlRequires browser operations and security work
Hosted PDF APIApplications generating customer documentsRequires an external service dependency

The non-obvious answer is that local generation can be the professional choice. For a developer creating five PDFs a day from fixed data, an API adds unnecessary network dependency. The engineering calculation changes when PDF creation becomes part of a product workflow.

For more details on production API patterns, see Python Generate PDF: Production API Guide for Developers and PDF Generator API: Real Costs & Performance in Production.

Production failures that break PDF generation

PDF generation failures are rarely caused by the final PDF file format. They usually happen earlier, during rendering, networking, or resource loading.

Failure mode: cold Chromium startup creates slow requests

A cold headless Chromium process can take about 7.8 seconds per request. Keeping the browser warm reduces that startup path to about 0.65 seconds. This difference explains why production systems often need process reuse instead of launching a new browser for every document.

Applications using local Chromium should treat browser lifecycle management as part of the architecture. The rendering code may be short, but the runtime behavior is not.

Failure mode: Django async execution breaks synchronous Playwright

Running sync_playwright directly inside Django async code raises SynchronousOnlyOperation. The failure happens because synchronous browser work runs inside an async execution context.

from concurrent.futures import ThreadPoolExecutor
from playwright.sync_api import sync_playwright

executor = ThreadPoolExecutor(max_workers=1)

def render_pdf():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.set_content("<h1>Invoice</h1>")
        page.pdf(path="invoice.pdf")
        browser.close()

future = executor.submit(render_pdf)
future.result()

Failure mode: web fonts silently fall back

PDF output can look correct in development and fail in production when fonts are not ready before rendering completes. The visible symptom is a PDF using a fallback font instead of the intended typeface.

The fix is not only installing fonts. The renderer must wait for font loading before creating the PDF.

await page.goto("https://example.com/invoice")

await page.evaluate("""
    async () => {
        await document.fonts.ready;
    }
""")

await page.pdf(path="invoice.pdf")

Security and scaling details developers miss

PDF generation often accepts HTML or URLs, which creates security responsibilities beyond normal file creation.

Failure mode: URL rendering creates an SSRF vulnerability

A URL-to-PDF endpoint becomes an SSRF hole until the application validates destinations. A renderer that can access internal addresses may expose private services, loopback systems, or cloud metadata endpoints.

The required controls include resolving the hostname first and rejecting private, loopback, and metadata addresses before fetching content.

import ipaddress
import socket

def is_private_target(host):
    addresses = socket.getaddrinfo(host, None)

    for item in addresses:
        ip = ipaddress.ip_address(item[4][0])
        if ip.is_private or ip.is_loopback:
            return True

    return False

print(is_private_target("example.com"))

Failure mode: batch jobs overwhelm synchronous workflows

Large document runs need a different design from single invoices. PDFGeny supports batch requests with up to 100 documents in one call, along with sync and async jobs, stored documents, and HMAC-signed webhooks.

These features change how applications handle document pipelines. A checkout page may need one immediate invoice, while a reporting system may queue hundreds of generated files.

Code examples across backend stacks

The rendering pattern is similar across languages: send document input, authenticate, receive PDF bytes, and store or deliver the result.

Node.js

const response = await fetch(
  "https://pdfgeny.com/api/v1/render",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer YOUR_API_KEY",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      html: "<h1>Receipt</h1>"
    })
  }
);

const buffer = await response.arrayBuffer();

PHP

<?php

$ch = curl_init("https://pdfgeny.com/api/v1/render");

curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer YOUR_API_KEY",
    "Content-Type: application/json"
]);

curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "html" => "<h1>Certificate</h1>"
]));

curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$pdf = curl_exec($ch);

file_put_contents("certificate.pdf", $pdf);

Go

package main

import (
    "bytes"
    "fmt"
    "net/http"
)

func main() {
    body:= []byte(`{"html":"<h1>Report</h1>"}`)

    req, _:= http.NewRequest(
        "POST",
        "https://pdfgeny.com/api/v1/render",
        bytes.NewBuffer(body),
    )

    req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
    req.Header.Set("Content-Type", "application/json")

    client:= &http.Client{}
    response, _:= client.Do(req)

    fmt.Println(response.StatusCode)
}

Ruby and cURL

curl -X POST https://pdfgeny.com/api/v1/render \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Invoice</h1>"}'

What We Got Wrong / What Surprised Us

The strongest surprise is that PDF generation problems are usually not about converting HTML into a PDF file. The difficult parts are lifecycle management, security, and predictable rendering.

The idea that every team should own a browser rendering stack is also often wrong. A local library beats an API when requirements are small and documents are simple. The opposite becomes true when teams need templates, async processing, webhooks, stored documents, or PDF/A-2b output.

wkhtmltopdf is another example of a common misconception. It is not dead; the practical issue is that it is unmaintained. The real cost is not installation. The real cost is supporting CSS behavior that newer browser engines already understand.

For teams evaluating browser-based generation, the related guide Chrome HTML Document to PDF: Real Costs and Production Issues covers the operational side of running browser rendering.

Practical Takeaways

  • Choose the rendering model first. Expected outcome: fewer architecture changes later. Time estimate: 30 minutes. Difficulty: Easy.
  • Test fonts and page breaks with real documents. Expected outcome: fewer production layout bugs. Time estimate: 2-4 hours. Difficulty: Medium.
  • Protect URL rendering endpoints. Expected outcome: reduced SSRF exposure. Time estimate: 1-3 hours. Difficulty: Medium.
  • Measure browser startup separately from rendering. Expected outcome: clearer performance decisions. Time estimate: 1 hour. Difficulty: Medium.
  • Use templates for repeated documents. Expected outcome: faster generation of standardized files. Time estimate: 1 day. Difficulty: Easy.

Try PDFGeny for production PDF generation

PDFGeny is designed for applications that need HTML, URL, or template input converted into finished PDFs without maintaining a Chromium deployment. The service includes 40 ready document templates, supports Python, Node.js, PHP, Go, Ruby, and cURL workflows, and provides a free plan with 50 documents a month.

Start with a free API key and test your own invoices, receipts, reports, or certificates using the same API endpoint your application can call in production.

Get a free API key

FAQ

How do I create a PDF in Python?

Python applications can create PDFs with local libraries such as ReportLab or by sending HTML to a rendering API. PDFGeny accepts HTML, URLs, and templates through POST https://pdfgeny.com/api/v1/render.

Is Python better than an API for PDF generation?

Not always. A local Python library is often better for a small number of simple documents. An API becomes useful when the application needs hosted rendering, templates, async jobs, webhooks, stored documents, or archival PDF/A-2b output.

Why do PDFs have missing fonts?

PDFs can use fallback fonts when rendering finishes before web fonts load. Waiting for font readiness and verifying font availability prevents many layout differences between browsers and generated files.

What is the cost of PDFGeny?

PDFGeny provides 50 documents a month on its free plan with no card required. Additional documents cost $0.009 each.

:::

M

Mikel Rougstone

Founder, PDFGeny

I build and run PDFGeny — the API, the rendering fleet and the template catalog. Most of what I write here comes from something that broke in production first.