Build an Automated Invoice Generator using HTML & Python
If you are building an e-commerce platform, a SaaS app, or just managing your own freelance work, you eventually hit the "Invoice Problem." You have data in a database, but your customers need a clean, professional PDF invoice.
Trying to build PDFs from scratch using low-level Python libraries like ReportLab is painful. It involves calculating x/y coordinates for every line of text.
A much smarter approach is to design your invoice using standard HTML/CSS, and then use an API to convert that HTML into a PDF.
In this tutorial, we will build a Python script that takes raw invoice data, merges it into an HTML template, and generates a professional PDF using the free Apdf API.
The Workflow
- Data Source: A simple Python dictionary (mimicking a database).
- Template Engine: We use Jinja2 to inject data into HTML.
- PDF Engine: We send the rendered HTML to Apdf.
- Result: We get a download link for the PDF.
Step 1: The Setup
- Go to Apdf.
- Sign up (it's free).
- Copy your API Token from the dashboard.
Next, install the requests and jinja2 library if you haven't already:
pip install requests jinja2
Step 2: Create the HTML Invoice Template
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Helvetica', sans-serif; color: #333; }
.box { max-width: 800px; margin: auto; padding: 30px; border: 1px solid #eee; }
.header { display: flex; justify-content: space-between; margin-bottom: 20px; }
.title { font-size: 24px; font-weight: bold; color: #2c3e50; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.total { text-align: right; margin-top: 20px; font-size: 18px; font-weight: bold; }
</style>
</head>
<body>
<div class="box">
<div class="header">
<div>
<div class="title">INVOICE</div>
<p>Invoice #: {{ invoice_id }}<br>Date: {{ date }}</p>
</div>
<div style="text-align: right;">
<strong>{{ company_name }}</strong><br>
{{ company_email }}
</div>
</div>
<p><strong>Bill To:</strong><br>
{{ client_name }}<br>
{{ client_address }}</p>
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.qty }}</td>
<td>USD {{ item.price }}</td>
<td>USD {{ item.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="total">
Grand Total: USD {{ grand_total }}
</div>
</div>
</body>
</html>
Step 3: The Python Generator Script
import time
import requests
from jinja2 import Template
# 1. Your API Token
API_TOKEN = "YOUR_API_TOKEN_HERE"
API_URL = "https://apdf.io/api/pdf/file/create"
STATUS_URL = "https://apdf.io/api/job/status/check"
# Helper: poll until the async job finishes, then return its result.
def wait_for_job(job_id, max_attempts=1200):
for _ in range(max_attempts):
check = requests.post(
STATUS_URL,
headers={'Authorization': f'Bearer {API_TOKEN}'},
data={'id': job_id},
)
check.raise_for_status()
body = check.json()
if body['status'] == 'successful':
return body['result']
if body['status'] == 'failed':
raise RuntimeError(body.get('error') or 'Job failed')
time.sleep(2)
raise TimeoutError('Job did not finish in time')
# 2. Mock Data (In a real app, this comes from your DB)
invoice_data = {
"invoice_id": "INV-2025-001",
"date": "November 21, 2025",
"company_name": "Tech Solutions Inc.",
"company_email": "billing@techsolutions.com",
"client_name": "John Doe Enterprises",
"client_address": "123 Market St, San Francisco, CA",
"items": [
{"name": "Web Development Service", "qty": 1, "price": 500.00, "total": 500.00},
{"name": "Server Maintenance", "qty": 2, "price": 100.00, "total": 200.00},
{"name": "SSL Certificate", "qty": 1, "price": 50.00, "total": 50.00},
],
"grand_total": 750.00
}
# 3. Load and Render the HTML Template
with open("invoice_template.html", "r") as file:
template_content = file.read()
jinja_template = Template(template_content)
rendered_html = jinja_template.render(invoice_data)
# 4. Send to API (returns a job_id; the PDF is built in the background)
print("Generating PDF...")
try:
response = requests.post(
API_URL,
headers={
'Authorization': f'Bearer {API_TOKEN}'
},
data={
'html': rendered_html,
# Optional Layout Parameters
'format': 'a4',
'margin_top': '20',
'margin_bottom': '20',
'margin_left': '20',
'margin_right': '20'
}
)
response.raise_for_status()
job_id = response.json()['job_id']
# 5. Wait for the job to finish, then read the result
result = wait_for_job(job_id)
print("Success!")
print(f"Download your invoice here: {result['file']}")
# Optional: Print file expiration or size
print(f"Pages: {result['pages']}")
print(f"Size: {result['size']} bytes")
except Exception as e:
print(f"An error occurred: {e}")
Run the script
python generate_invoice.py
Output
Generating PDF...
Success!
Download your invoice here: https://apdf-files.s3.eu-central-1.amazonaws.com/6f1674e2703953aa.pdf
Pages: 1
Size: 12405 bytes
Next Steps
- Secure the Invoice: Use the Password Protection endpoint to lock the PDF with the client's ID.
- Store Efficiently: If your invoices have high-resolution logos, use the Compress endpoint to shrink the file size before archiving.
Related tutorials
How to Generate PDFs from Claude
Describe the document, get the PDF: Claude drafts the HTML layout and renders it through the Apdf MCP tools — invoices, reports and one-pagers straight from the conversation, with page format, margins and headers steered in plain English. Revisions are just more chat.
Generate PDF Event Tickets with QR Codes in A6 Format using Go
Learn how to generate A6-sized event tickets with embedded QR codes using Go. This tutorial focuses on small-format PDFs (tickets, badges, labels) using CSS print media and the Apdf API. Perfect for event management systems, concerts, and conferences where scanning and compact printing are essential.
Dynamic Certificate Generation for Online Courses (LMS)
Learn how to dynamically generate professional PDF certificates for online course platforms using PHP. Design certificates with HTML/CSS, use landscape orientation, and automatically generate them when students complete courses.
Your code made the PDF.
Then it went dark.
Opened, read, re-read, dropped on page 4 — you never see any of it. Share the PDFs you generate through Apdf recipient links, and every signal becomes something you can act on: ping Slack, update the CRM, let an agent follow up. Same account, same API token, one more call.
API · Webhook · MCP