Why Build Your Own Invoice API?
Every SaaS product eventually needs invoices. You can use Stripe's built-in invoicing (limited customization), pay $49+/mo for a third-party service, or build a focused API that does exactly what you need. We chose the third option and built DocuMint — here's how.
The core idea: accept a JSON payload describing an invoice, render it through an HTML template, convert to PDF using WeasyPrint, and return the file. Under 200 lines of Python.
The Stack
- FastAPI — async Python framework with automatic OpenAPI docs and request validation
- WeasyPrint — CSS-based PDF rendering engine (no headless Chrome needed)
- Jinja2 — HTML templating for invoice layouts
- SQLite — user accounts and usage tracking
This stack runs on a single $8/mo VPS and can generate hundreds of invoices per minute. No Kubernetes, no microservices, no overengineering.
Step 1: Define the Invoice Schema
Start with what data an invoice needs. At minimum:
{
"from": {
"name": "Acme Corp",
"address": "123 Main St, San Francisco, CA 94102",
"email": "[email protected]"
},
"to": {
"name": "Client Inc",
"address": "456 Oak Ave, New York, NY 10001"
},
"items": [
{"description": "Web Development", "quantity": 40, "unit_price": 150.00},
{"description": "Design Review", "quantity": 8, "unit_price": 125.00}
],
"invoice_number": "INV-001",
"date": "2026-04-15",
"due_date": "2026-05-15",
"tax_rate": 0.08
}
Use Pydantic models for validation. This catches malformed input before it reaches your rendering engine:
from pydantic import BaseModel, EmailStr
from typing import Optional
class InvoiceItem(BaseModel):
description: str
quantity: float
unit_price: float
class Party(BaseModel):
name: str
address: Optional[str] = None
email: Optional[str] = None
class InvoiceRequest(BaseModel):
sender: Party # "from" is reserved in Python
recipient: Party
items: list[InvoiceItem]
invoice_number: Optional[str] = None
date: Optional[str] = None
due_date: Optional[str] = None
tax_rate: float = 0.0
currency: str = "USD"
template: str = "classic"
Step 2: Build HTML Templates
The secret to professional-looking PDF invoices is CSS. WeasyPrint supports modern CSS including flexbox, custom fonts, and @page rules for controlling PDF output.
Design your invoice template as standard HTML/CSS. Use @page {{ size: A4; margin: 2cm; }} to set PDF dimensions. Use CSS variables for colors so you can support multiple templates with the same base HTML.
We ship three templates — classic (traditional corporate), modern (gradient accents, rounded cards), and minimal (clean monospace). Users choose via the template parameter.
Step 3: The Generation Endpoint
@app.post("/api/v1/invoices")
async def generate_invoice(
req: InvoiceRequest,
api_key: str = Depends(verify_api_key)
):
# Check usage limits
if not check_usage(api_key):
raise HTTPException(429, "Monthly limit reached. Upgrade your plan.")
# Render HTML with Jinja2
template = env.get_template(f"{{req.template}}.html")
html = template.render(invoice=req.dict(), totals=calculate_totals(req))
# Convert to PDF with WeasyPrint
pdf_bytes = HTML(string=html).write_pdf()
# Track usage
increment_usage(api_key)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename=invoice.pdf"}
)
That's the core. Everything else — authentication, rate limiting, Stripe integration — is standard web application infrastructure.
Step 4: Performance Optimization
WeasyPrint's initial import is slow (~2 seconds) because it loads system fonts. Solutions:
- Import at startup, not per-request. Keep the WeasyPrint module warm.
- Bundle fonts in the Docker image instead of loading from Google Fonts at render time.
- Cache template compilation — Jinja2 does this automatically with
auto_reload=Falsein production.
With these optimizations, invoice generation takes ~300ms for a typical invoice, even on a modest VPS.
Step 5: Monetization
The pricing model is straightforward:
- Free tier: 10 invoices/month — enough to validate the API before committing
- Starter ($9/mo): 500 invoices, all 3 templates
- Pro ($19/mo): 5,000 invoices, priority support
- Business ($39/mo): Unlimited invoices, custom templates, webhook notifications
Stripe Checkout handles the payment flow. When a user upgrades, a webhook updates their plan tier in the database. No custom payment UI needed.
Key Lessons
- WeasyPrint > headless Chrome for this use case. Lighter, faster, no browser dependency.
- Start with fewer templates. Three is enough. Users can always request more.
- Free tier is essential. Developers need to test before they buy. 10 free invoices/month costs almost nothing to serve but builds trust.
- SQLite is fine for this workload. We serve thousands of requests with a single SQLite database. Don't reach for PostgreSQL until you need concurrent writes.
Try DocuMint free
A step-by-step guide to building a production-ready PDF invoice generation API using Python, FastAPI, and WeasyPrint. Get started with our free tier — no credit card required.
Get started free →