How invoice generation works
Every invoice you send has the same bones: your details, the customer’s details, a list of what they bought, the tax and the amount due. Only the values change. So the layout is designed once, as a template, and your billing code sends the values each time.
Order data in, PDF out. No PDF library sits in your app, and no browser runs on your servers. The invoice appears in about a second, ready to email, attach or store.
- Start from the invoice starter. In the dashboard, create a template from the invoice starter. It already has a logo area, billing details, line items, tax and payment terms.
- Make it yours. Change the colours, fonts and wording in the HTML and CSS with a live preview. Use the Simple mode form to try different sample data without touching code.
- Copy the template ID. Each template has an ID. Your code sends it with every invoice, so a design change never needs a deploy.
- Send each invoice’s data. When an invoice is due, your server posts its data to
POST /v1/pdfwith a test key first, then a live key. - Store and send the PDF. Save the file in your own storage and email it to the customer from your own system.
The invoice starter’s data and one request
The starter comes with sample data for a design studio billing a coffee company. This is a shortened copy, with two of its five line items. The full set, with every field explained, is on the invoice template page.
{
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"data": {
"currency": "USD",
"locale": "en-US",
"company": { "name": "Halden & Moss", "initials": "HM", "city": "Brooklyn, NY 11249", "tax_id": "84-2917365" },
"customer": { "name": "Brightwater Coffee Co.", "contact": "Priya Raman", "city": "Seattle, WA 98101" },
"invoice": {
"number": "INV-2026-0142",
"issued": "2026-09-30",
"due": "2026-10-30",
"tax_rate": 8.875,
"terms": "Payment is due within 30 days of the invoice date."
},
"items": [
{ "description": "Packaging design", "detail": "Three bag designs", "quantity": 3, "unit": "", "unit_price": 1850 },
{ "description": "Print production management", "detail": "Proof checks", "quantity": 6, "unit": "hrs", "unit_price": 125 }
]
},
"filename": "invoice-INV-2026-0142",
"response": "url"
}curl https://api.castpdf.com/v1/pdf \
-H "Authorization: Bearer $CASTPDF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-INV-2026-0142" \
-d @invoice-0142.jsonThe answer is JSON with a signed url to the PDF, its page count and the date the link expires. Anyone with the link can download the file until then, without your API key. Leave out response and the body of the answer is the PDF itself.
Notice what the request does not contain: no subtotal, no tax amount, no total. The starter adds those up from the line items, which brings us to the part of invoicing that has to be right.
Totals and tax done right
The starter multiplies each quantity by its price, adds the lines and applies the tax rate. It then formats every amount with the money filter, which rounds to the right number of decimals for the currency. For the sample above, that prints a subtotal of $6,300.00, tax of $559.13 and a total of $6,859.13.
That is fine for quotes and simple invoices. For invoices that go into your books, we suggest one change: work out the money in your own system and send the results. Your billing code already knows the discounts, the tax rules and how your accountant wants half cents rounded. The template should only format what it receives.
from decimal import Decimal, ROUND_HALF_UP
CENT = Decimal("0.01")
def to_cents(value):
return value.quantize(CENT, rounding=ROUND_HALF_UP)
items = [
{"description": "Packaging design", "quantity": 3, "unit_price": Decimal("1850.00")},
{"description": "Print production management", "quantity": 6, "unit_price": Decimal("125.00")},
]
for item in items:
item["amount"] = to_cents(item["unit_price"] * item["quantity"])
subtotal = sum((item["amount"] for item in items), Decimal("0"))
tax = to_cents(subtotal * Decimal("8.875") / 100)
total = subtotal + tax
data = {
"currency": "USD",
"locale": "en-US",
"items": [{**item, "unit_price": str(item["unit_price"]), "amount": str(item["amount"])} for item in items],
"totals": {"subtotal": str(subtotal), "tax": str(tax), "total": str(total)},
}The amounts travel as strings, so no rounding happens on the way. The money filter accepts a number or a numeric string. In the template, the totals block then prints the values you sent instead of summing them:
<table class="totals keep-together">
<tr><th>Subtotal</th><td>{{ totals.subtotal | money: currency, locale }}</td></tr>
<tr><th>Tax ({{ invoice.tax_rate | number: 3, locale }}%)</th><td>{{ totals.tax | money: currency, locale }}</td></tr>
<tr class="grand"><th>Total due</th><td>{{ totals.total | money: currency, locale }}</td></tr>
</table>The number filter prints the rate with a fixed count of decimals, here 8.875. A missing value prints nothing rather than failing. A value that is not a number fails the request with template_render_error, and the error points to the line and column, so a broken invoice never goes out quietly. The Liquid reference lists every filter.
Currencies, languages and local formats
Send a currency code and a locale with each invoice and the same template writes amounts the way your customer expects. The symbol, its position, the separators and the decimals all follow the locale.
| Template | Output |
|---|---|
{{ 1234.5 | money: "USD" }} | $1,234.50 |
{{ 1234.5 | money: "EUR", "de-DE" }} | 1.234,50 € |
{{ 19.9 | money: "GBP", "en-GB" }} | £19.90 |
{{ 1234.5 | money: "JPY", "ja-JP" }} | ¥1,235 (yen has no decimals) |
Watch one default: without a currency, money assumes euros. Always pass the currency when you bill in anything else.
Dates work the same way. format_date takes a style, a time zone and a locale, so the same date prints as "September 30, 2026" for a US client and "30. September 2026" for a German one. The time zone matters for timestamps near midnight, when the date can differ from one country to the next.
Labels such as "Invoice" and "Total due" are plain text in the HTML. For a few languages, keep one template per language. For many, send the labels in your data alongside the numbers. Noto fonts are installed for many scripts, including Arabic, Chinese, Japanese and Korean, so accented names and other alphabets print without loading fonts.
What a valid invoice includes
Rules depend on the country and on whether you charge VAT or sales tax. Most rules share a core, and the invoice starter has a place for each item below. This is general information, not tax or legal advice, so confirm the details with your accountant.
- A unique invoice number that you never reuse.
- The issue date, and usually a due date or payment terms.
- Your legal name and address, and the customer’s.
- Your tax or registration number where your tax office asks for one.
- Each item or service, with quantity, unit price and line amount.
- The tax rate and tax amount shown apart from the net amounts.
- The total due and how to pay it.
Businesses registered for VAT often have extra duties, such as showing the customer’s VAT number on some sales between countries. In the US, sales tax rules vary by state. Whatever applies to you, it is a field in your data and a line in your template.
Receipts, quotes and credit notes
An invoice asks for money. The documents around it follow the same pattern, so the setup you build for invoices carries over.
- Receipts confirm a payment. The receipt starter is sized like a till slip and works out VAT included in prices. See the receipt PDF generator.
- Quotes and estimates are invoices with a different title, a validity date and no payment details. Copy the invoice template and change those parts.
- Credit notes refer to the original invoice number and show the amounts being returned. Send the reference and the amounts from your system.
- Statements list several invoices for one customer. A long statement benefits from the repeating table header described below.
Connect your billing today, from your own code
Most billing systems can tell your server when an invoice is finalised or a payment succeeds. That moment is when you call CastPDF. Your handler gathers the invoice data, sends it with your template ID and saves the PDF it gets back.
const COMPANY = { name: 'Halden & Moss', initials: 'HM', city: 'Brooklyn, NY 11249', tax_id: '84-2917365' };
export async function renderInvoicePdf(invoice) {
const res = await fetch('https://api.castpdf.com/v1/pdf', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CASTPDF_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `invoice-${invoice.number}`,
},
body: JSON.stringify({
template_id: process.env.INVOICE_TEMPLATE_ID,
data: {
currency: invoice.currency,
locale: invoice.locale,
company: COMPANY,
customer: { name: invoice.customerName, contact: invoice.contactName, city: invoice.customerCity },
invoice: { number: invoice.number, issued: invoice.issuedOn, due: invoice.dueOn, tax_rate: invoice.taxRate },
items: invoice.lines.map((line) => ({
description: line.name,
detail: line.note ?? '',
quantity: line.quantity,
unit: line.unit ?? '',
unit_price: line.unitPriceCents / 100,
})),
},
filename: `invoice-${invoice.number}`,
response: 'url',
}),
signal: AbortSignal.timeout(60_000),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.url;
}Run this after you have stored the invoice in your own database, so a failed render can simply be retried. The custom invoice PDFs from your billing events guide walks through one billing provider end to end. The invoice PDF from JSON guide covers the data side for any system.
Want the layout in your own codebase instead? The HTML invoice template guide builds one you can send as raw HTML. Ready-made apps for billing and shop platforms are not available today, so the call always comes from your code. There is nothing to install: the Node.js and Python pages show the patterns.
Long invoices with a repeating header
A monthly invoice for a busy customer can run to dozens of lines. In many tools, that is where things go wrong: a row split across two pages, column names missing on page two, a total stranded alone at the top of the last page.
Templates render in print mode, which handles all three. The invoice starter puts its column names in a <thead>, so they repeat on every continued page. Rows are never split where it can be avoided. The totals and payment block carries the keep-together class, so it moves to the next page as one piece rather than breaking in half.
You can also add "Page 2 of 3" to the footer with a few lines of print CSS. The repeating table header guide shows the markup, and the page numbers guide shows the footer. If an invoice would grow past 50 pages, the request fails with a clear error instead of returning half a document.
Numbering, duplicates and keeping a copy
Many tax offices expect each invoice to carry a sequential number that identifies it once. Keep that counter in your own database, in the same transaction that creates the invoice. CastPDF prints the number it receives and never invents one, so your records and your PDFs always match.
Then reuse the number as the Idempotency-Key. If a network error leaves you unsure whether a request worked, send it again with the same key. You get the first invoice back, and it is never rendered or billed twice.
- The same key with a different body is refused with
idempotency_mismatch, which catches an edited invoice reusing an old number. - A repeat that arrives while the first request is still running gets
idempotency_conflict. Wait a moment and retry. - A key is remembered for at least 24 hours. Every new invoice needs its own key.
Stored PDFs are kept for your plan’s storage time, from 1 day on Free to 90 days on Scale. Invoices usually have to be kept for years, so download each PDF into your own storage as soon as it is made. The signed link is for delivery, not for your archive.
Pricing per invoice
One invoice is one PDF, whether it has one page or 50. Test invoices made with a test key are free and unlimited, and failed requests never count.
| Plan | Invoices a month | Monthly price | Cost per invoice when fully used | Extra invoices per 1,000 |
|---|---|---|---|---|
| Free | 100 | Free | Free | None (hard limit) |
| Starter | 2,500 | $19 | $0.0076 | $9.00 |
| Growth | 10,000 | $49 | $0.0049 | $6.00 |
| Pro | 50,000 | $129 | $0.0026 | $3.50 |
| Scale | 200,000 | $299 | $0.0015 | $1.75 |
A business sending 10,000 invoices a month pays $49 on Growth, about $0.0049 an invoice. At 50,000 a month, Pro brings that to $0.0026. Starter is $15 a month when paid yearly.
Month-end runs are spiky, and the plans allow for it. On paid plans, unused invoices roll over for one month. Past your allowance, invoices continue at the overage price up to a spending cap you set. We email you at 80% and 100% of your allowance, when overage starts and if you reach the cap. The pricing page has the details.