HTML To PDF Converter
4 March 2026

Generate PDF Invoices with Python: A Simple API Approach

Skip ReportLab, WeasyPrint, and self-hosted Puppeteer. Here's how to generate professional PDF invoices from HTML templates in Python with a single HTTP request.

Python developers generating PDFs typically reach for one of three options: ReportLab (low-level, code-driven layout), WeasyPrint (CSS-based but requires system dependencies), or Puppeteer via subprocess (heavy, fragile). All three work, but all three add complexity you probably don't need.

If your goal is to convert an HTML invoice template to a PDF, the simplest approach is an API call. Design your template in HTML/CSS, render it with Jinja2, and POST it to a service that handles the Chromium rendering.

The approach

  1. Design your invoice as an HTML template with Jinja2 placeholders
  2. Render it with real invoice data
  3. POST the HTML to the API
  4. Get a PDF URL back

No system dependencies. No Chromium binary. No C library compilation. Just pip install requests jinja2.

Step 1: Create the invoice template

<!-- templates/invoice.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; margin: 40px; color: #333; }
    .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    .company { font-size: 24px; font-weight: bold; }
    .invoice-number { color: #666; }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th { background: #f5f5f5; text-align: left; padding: 12px; border-bottom: 2px solid #ddd; }
    td { padding: 12px; border-bottom: 1px solid #eee; }
    .total-row td { font-weight: bold; border-top: 2px solid #333; }
    .amount { text-align: right; }
    @media print { body { margin: 0; } }
  </style>
</head>
<body>
  <div class="header">
    <div class="company">{{ company_name }}</div>
    <div>
      <div class="invoice-number">Invoice #{{ invoice_number }}</div>
      <div>{{ invoice_date }}</div>
    </div>
  </div>

  <p><strong>Bill to:</strong> {{ customer_name }}</p>

  <table>
    <thead>
      <tr><th>Description</th><th class="amount">Qty</th><th class="amount">Price</th><th class="amount">Total</th></tr>
    </thead>
    <tbody>
      {% for item in line_items %}
      <tr>
        <td>{{ item.description }}</td>
        <td class="amount">{{ item.quantity }}</td>
        <td class="amount">${{ "%.2f"|format(item.price) }}</td>
        <td class="amount">${{ "%.2f"|format(item.quantity * item.price) }}</td>
      </tr>
      {% endfor %}
      <tr class="total-row">
        <td colspan="3">Total</td>
        <td class="amount">${{ "%.2f"|format(total) }}</td>
      </tr>
    </tbody>
  </table>
</body>
</html>

Step 2: Render and convert

import requests
from jinja2 import Environment, FileSystemLoader

# Load and render the template
env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("invoice.html")

html = template.render(
    company_name="Acme Pty Ltd",
    invoice_number="INV-2026-0042",
    invoice_date="8 March 2026",
    customer_name="Jane Smith",
    line_items=[
        {"description": "Web development", "quantity": 40, "price": 150.00},
        {"description": "UI design",       "quantity": 12, "price": 120.00},
        {"description": "Hosting (annual)", "quantity": 1,  "price": 240.00},
    ],
    total=40 * 150 + 12 * 120 + 240,
)

# Convert to PDF
response = requests.post(
    "https://htmltopdfconverter.com.au/api/programmatic/pdf/generate",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "text/html",
    },
    data=html.encode("utf-8"),
)

data = response.json()
pdf_url = data["pdfs"][0]["url"]
print(f"PDF ready: {pdf_url}")

# Download if you need a local copy
pdf_bytes = requests.get(pdf_url).content
with open("invoice.pdf", "wb") as f:
    f.write(pdf_bytes)

Why this beats the alternatives

OptionProsCons
API (this approach) No dependencies, full CSS, 10 lines of code Requires network call, $5/year
ReportLab No network, fine-grained control Proprietary layout API, no CSS, painful for complex templates
WeasyPrint CSS-based, no network Requires cairo, Pango, GDK-PixBuf system deps — breaks in Docker/CI regularly
Puppeteer via subprocess Full Chromium rendering 300+ MB binary, Node.js dependency in a Python project, memory leaks

Integration with Flask / Django / FastAPI

# Flask example
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/api/invoices/<invoice_id>/pdf")
def generate_invoice_pdf(invoice_id):
    invoice = get_invoice(invoice_id)  # your DB lookup
    html = render_invoice_html(invoice)  # Jinja2 render

    resp = requests.post(
        "https://htmltopdfconverter.com.au/api/programmatic/pdf/generate",
        headers={
            "Authorization": f"Bearer {os.environ['PDF_API_KEY']}",
            "Content-Type": "text/html",
        },
        data=html.encode("utf-8"),
    )

    return jsonify(resp.json())

The same pattern works in Django views, FastAPI endpoints, or plain scripts. It's just an HTTP POST — any framework that can make HTTP requests can use it.

Get started

  1. Create an account
  2. Subscribe ($5 AUD/year) from your dashboard
  3. Generate an API key
  4. pip install requests jinja2
  5. Copy the code above

Full API docs — request formats, error codes, rate limits, and examples in Node.js, Python, and PHP.

FAQ

Do I need to install Chromium or any system packages?

No. The API handles all rendering infrastructure. You just need requests (and optionally jinja2 for templating). Works in Docker, CI, serverless — anywhere Python runs.

Can I send multiple files in one request?

Yes — up to 10 HTML files via multipart form upload. See the API docs for the multipart request format.

What about WeasyPrint?

WeasyPrint is a solid option if you can manage the system dependencies (cairo, Pango, GDK-PixBuf). The API approach avoids all of that and gives you full Chromium-level CSS support without any native library installation.