Generating your own branded PDF from a Stripe invoice
Stripe gives every invoice an invoice_pdf URL, and for a lot of businesses
that is enough. It stops being enough when you need your own layout, a second
language, a tax field Stripe does not model, or a document that matches the rest
of your paperwork.
The invoice_pdf file is not customisable beyond the branding settings in the
dashboard. If you need more, you generate your own — which means mapping the
Stripe invoice object onto your own template.
The amounts are integers in minor units
This is the first thing that catches people:
{
"amount_due": 120000,
"currency": "usd",
"lines": { "data": [{ "amount": 120000, "quantity": 1 }] }
}
120000 is $1,200.00, not $120,000. Stripe stores minor units — cents for
USD, and the same integer with no decimal shift for zero-decimal currencies
like JPY, where 1200 means ¥1,200.
So the conversion is currency-dependent. Dividing everything by 100 is wrong for about a dozen currencies, and it is wrong in the direction that inflates a yen invoice by 100×.
Mapping the object
from decimal import Decimal
ZERO_DECIMAL = {"JPY", "KRW", "VND", "CLP"} # abridged
def to_major(amount: int, currency: str) -> str:
if currency.upper() in ZERO_DECIMAL:
return str(amount)
return str(Decimal(amount) / 100)
payload = {
"document_type": "invoice",
"data": {
"invoice_number": inv["number"],
"issue_date": date.fromtimestamp(inv["created"]).isoformat(),
"currency": inv["currency"].upper(),
"seller": {"name": "Acme Inc."},
"buyer": {
"name": inv["customer_name"],
"email": inv["customer_email"],
},
"line_items": [
{
"name": line["description"],
"quantity": str(line["quantity"] or 1),
"unit_price": to_major(
line["amount"] // (line["quantity"] or 1), inv["currency"]
),
}
for line in inv["lines"]["data"]
],
},
}
Note Decimal(amount) / 100 rather than amount / 100. The moment a monetary
value passes through a float it can be a cent out, and on an invoice that is a
number someone reconciles against a bank statement.
Render it
curl -X POST https://invoicepdfs.com/api/v1/documents/render \
-H "Authorization: Bearer inv_..." \
-H "Content-Type: application/json" \
-d '{"document_type":"invoice","template":{"id":"tpl_modern"},
"data": { ... },
"output":{"format":"pdf","delivery":"url"}}'
You get a download URL back once the PDF exists.
Do it on the webhook, not on request
Generate when invoice.finalized fires, store the result, and serve the stored
file afterwards. Two reasons: an invoice must not change after it is issued, and
regenerating on every download means a template edit silently alters documents
your customers already have.
What Stripe still owns
Keep Stripe as the source of truth for what was charged. The PDF is a presentation of that record, not a second copy of it — if the two ever disagree, the payment processor is right.
Same pattern applies to any billing system that exposes an invoice object. The Invoice PDF API takes the JSON and returns the document; the mapping above is the only part specific to Stripe.