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:

  1. Import at startup, not per-request. Keep the WeasyPrint module warm.
  2. Bundle fonts in the Docker image instead of loading from Google Fonts at render time.
  3. Cache template compilation — Jinja2 does this automatically with auto_reload=False in 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

  1. WeasyPrint > headless Chrome for this use case. Lighter, faster, no browser dependency.
  2. Start with fewer templates. Three is enough. Users can always request more.
  3. Free tier is essential. Developers need to test before they buy. 10 free invoices/month costs almost nothing to serve but builds trust.
  4. 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 →