HTML and CSS are the easiest way to design an invoice: style it once, fill it in with a template, and let the Ink & Order API turn it into a clean PDF using headless Chromium. Paid, approved, in order.
Get a free key (50 files/month), then export it:
export INKORDER_API_KEY=io_live_...
pip install requests
"""Generate an invoice PDF from an HTML template with the Ink & Order API.
Usage: INKORDER_API_KEY=io_live_... python guide_invoice_pdf.py
"""
import html
import os
from string import Template
import requests
BASE = os.environ.get("INKORDER_API_URL", "https://api.inkandordertools.com")
INVOICE = Template("""<!doctype html><html><head><meta charset="utf-8"><style>
body { font-family: Helvetica, Arial, sans-serif; margin: 48px; color: #111; }
.top { display: flex; justify-content: space-between; }
table { width: 100%; border-collapse: collapse; margin-top: 32px; }
th, td { padding: 8px; border-bottom: 1px solid #ddd; text-align: left; }
td.n, th.n { text-align: right; }
.total { font-size: 20px; font-weight: bold; text-align: right; margin-top: 16px; }
</style></head><body>
<div class="top"><div><h1>Invoice $number</h1><p>Billed to: $customer</p></div>
<div><p>Date: $date</p><p>Due: $due</p></div></div>
<table><tr><th>Item</th><th class="n">Qty</th><th class="n">Unit</th><th class="n">Amount</th></tr>
$rows</table>
<p class="total">Total: $$$total</p>
</body></html>""")
def render_invoice(number, customer, date, due, items):
rows, total = [], 0.0
for desc, qty, unit in items:
amount = qty * unit
total += amount
rows.append(f"<tr><td>{html.escape(desc)}</td><td class='n'>{qty}</td>"
f"<td class='n'>${unit:,.2f}</td><td class='n'>${amount:,.2f}</td></tr>")
return INVOICE.substitute(number=number, customer=html.escape(customer), date=date,
due=due, rows="".join(rows), total=f"{total:,.2f}")
def html_to_pdf(page: str) -> bytes:
r = requests.post(f"{BASE}/v1/convert",
headers={"Authorization": f"Bearer {os.environ['INKORDER_API_KEY']}"},
files={"file": ("invoice.html", page.encode(), "text/html")}, timeout=120)
r.raise_for_status()
return r.content
page = render_invoice("INV-1001", "Acme Corp", "2026-09-29", "2026-10-29",
[("Website redesign", 1, 2400.00), ("Hosting (months)", 12, 25.00)])
with open("invoice-INV-1001.pdf", "wb") as f:
f.write(html_to_pdf(page))
print("saved invoice-INV-1001.pdf")
python guide_invoice_pdf.py
# saved invoice-INV-1001.pdf
html.escape so it can't break your layout.string.Template for Jinja2 if you need loops and conditionals in the template.