Why Manual Invoice Creation Does Not Scale
Creating invoices manually works when you have five clients. It stops working at fifty. By five hundred, it's a full-time job that nobody wants.
The manual process looks something like this: open a template in Word or Google Docs, replace the client name, update line items, adjust the totals, export to PDF, rename the file, email it. Repeat for every client, every month.
This process is error-prone, time-consuming and completely unnecessary in 2026. Any invoice that follows a consistent format can be generated programmatically from a template.
How Template-Based Invoice Generation Works
The concept is straightforward:
- Design a template - Create your invoice layout once, with placeholders where dynamic data goes
- Define variables - Map each placeholder to a data field (client name, invoice number, line items, total)
- Call an API - Send your variable data to a generation endpoint, receive a formatted PDF
- Deliver - Email the PDF to your client or store it in your system
The template stays the same. Only the data changes.
Setting Up an Invoice Template
A well structured invoice template needs the following variable placeholders:
Header Variables
{{company_name}}- Your business name{{company_address}}- Your address block{{company_logo}}- Your logo (HTML content type for image embedding){{invoice_number}}- Unique identifier{{invoice_date}}- Issue date{{due_date}}- Payment deadline
Client Variables
{{client_name}}- Recipient name{{client_address}}- Recipient address{{client_email}}- For reference
Line Item Variables
lines- The billable items, as a list. One row in the template, as many as you send on the page{{subtotal}}- Pre-tax amount{{tax_rate}}- Applicable tax percentage{{tax_amount}}- Calculated tax{{total}}- Final amount due
Footer Variables
{{payment_instructions}}- Bank details or payment link{{notes}}- Optional terms or notes
Variable Content Types
Not all variables are plain text. Quaterio supports multiple content types for different rendering needs:
Text - Simple string replacement. HTML is escaped for safety. Use for names, numbers, dates.
HTML - Rendered as formatted HTML. Use for line item tables, styled addresses or any content that needs structure.
Markdown - Parsed to HTML, then rendered. Useful for notes or descriptions that benefit from basic formatting.
QR Code - Renders as an SVG QR code. Pass a payment URL or reference string, get a scannable code on the invoice.
Barcode - Renders as an SVG barcode (CODE128, EAN13 and other formats). Useful for invoice reference numbers that need machine-readable encoding.
Line Items Without Guessing How Many
Most invoices have a variable number of lines. Some months a client gets three, some months thirty. You don't want a template that only fits ten.
Mark the row that should repeat, and address each field by the collection name:
<tr data-repeat="lines">
<td>lines.description</td>
<td>lines.qty</td>
<td>lines.amount</td>
</tr>
Then send a list:
{
"variables": {
"client_name": "Acme Inc",
"lines": [
{ "description": "Consulting", "qty": "10", "amount": "1500.00" },
{ "description": "Design work", "qty": "3", "amount": "2750.00" }
]
}
}
The row renders once per entry. Three lines or thirty, the template doesn't change. Send an empty list and the row disappears rather than leaving an empty box on the page.
A placeholder inside the repeating row that isn't part of the collection still works normally, so you can show the currency or a tax rate on every line without repeating it in your data.
Worth being clear about one thing, because it trips people up: this is different from batch generation below. A list inside one request adds rows to one invoice. The batch endpoint's rows array produces a separate PDF for each entry.
Generating a Single Invoice via API
Once your template is designed in the Quaterio editor, generating a PDF is a single API call:
const response = await fetch('https://quaterio.com/api/v1/generate/template', {
method: 'POST',
headers: {
'Authorization': 'Bearer q_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'inv-template-2026',
variables: {
company_name: 'Triple Down AB',
invoice_number: 'INV-2026-0547',
invoice_date: '2026-05-16',
due_date: '2026-06-16',
client_name: 'Acme Corporation',
client_address: '123 Business Avenue\nStockholm, Sweden',
line_items: `
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Amount</th></tr>
</thead>
<tbody>
<tr><td>Consulting - May 2026</td><td>40</td><td>$150</td><td>$6,000</td></tr>
<tr><td>Development Sprint 12</td><td>1</td><td>$8,500</td><td>$8,500</td></tr>
</tbody>
</table>
`,
subtotal: '$14,500',
tax_rate: '25%',
tax_amount: '$3,625',
total: '$18,125',
payment_instructions: 'Bank: SEB\nAccount: 1234-5678-9012\nReference: INV-2026-0547',
},
}),
});
const { url } = await response.json();
console.log('Invoice PDF:', url);
The API returns a download URL for your generated PDF. The invoice inherits all formatting, pagination, headers and footers from your template design.
Batch Generation for Monthly Billing
Generating invoices one at a time works for ad-hoc needs. For monthly billing cycles where you need to generate dozens or hundreds of invoices, use the batch endpoint:
const invoiceData = [
{
client_name: 'Acme Corp',
invoice_number: 'INV-2026-0547',
total: '$18,125',
// ... other variables
},
{
client_name: 'Globex Inc',
invoice_number: 'INV-2026-0548',
total: '$4,200',
// ... other variables
},
// ... up to 1000 rows per batch
];
const response = await fetch('https://quaterio.com/api/v1/generate/batch', {
method: 'POST',
headers: {
'Authorization': 'Bearer q_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'inv-template-2026',
rows: invoiceData,
concurrency: 10, // parallel generation (max 20)
}),
});
const results = await response.json();
// results: [{ success: true, url: "..." }, { success: true, url: "..." }, ...]
Each row in the array produces a separate PDF. The batch endpoint processes them with configurable concurrency, so you're not waiting sequentially for each generation to complete.
Integrating with Your Billing System
Here's a practical example of connecting invoice generation to a billing workflow:
import { createClient } from '@supabase/supabase-js';
async function generateMonthlyInvoices() {
// 1. Fetch all active subscriptions with billing due
const { data: subscriptions } = await supabase
.from('subscriptions')
.select('*, customers(*)')
.eq('status', 'active')
.lte('next_billing_date', new Date().toISOString());
// 2. Transform subscription data into invoice variables
const rows = subscriptions.map((sub) => ({
client_name: sub.customers.name,
client_address: sub.customers.address,
invoice_number: generateInvoiceNumber(),
invoice_date: new Date().toISOString().split('T')[0],
due_date: addDays(new Date(), 30).toISOString().split('T')[0],
line_items: buildLineItemsHtml(sub.line_items),
total: formatCurrency(sub.amount),
// ... remaining variables
}));
// 3. Generate all invoices in one batch call
const response = await fetch('https://quaterio.com/api/v1/generate/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.QUARTERIO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: process.env.INVOICE_TEMPLATE_ID,
rows,
concurrency: 15,
}),
});
const results = await response.json();
// 4. Store PDF URLs and send to customers
for (let i = 0; i < results.length; i++) {
if (results[i].success) {
await sendInvoiceEmail(subscriptions[i].customers.email, results[i].url);
await recordInvoice(subscriptions[i].id, results[i].url);
}
}
}
Tips for Invoice Template Design
Keep it simple - Invoices should be scannable. Use clear hierarchy: company header, client details, line items table, totals, payment info.
Let the template own the table - Send line items as a list and mark the row with data-repeat. You keep control of columns, alignment and styling in the template, and your code just sends data. Building an HTML table string in your own code still works, but it means your billing logic owns your invoice styling, which is a bad place for it to live.
Include a QR code - Add a {{payment_qr}} variable with content type qrcode. Pass a payment URL (like a Stripe payment link) and your invoice becomes interactive.
Add a barcode for reference - A {{reference_barcode}} variable with content type barcode makes your invoice number machine-readable for automated payment matching.
Test with edge cases - Generate test invoices with very long client names, many line items (to test pagination) and large amounts to ensure your layout handles everything gracefully.
Pricing Considerations
Quaterio's API is available on Business plans and above. The batch endpoint supports up to 100 rows per call on Business tier and up to 1,000 on Enterprise. For most small-to-medium billing operations, the Business tier is sufficient.
If you're generating thousands of invoices monthly, the Enterprise tier removes rate limits and adds priority processing.
If You Invoice in France or Germany
From 2026 German businesses must be able to receive e-invoices, and from 2027 French businesses must issue them. Both mandates point at the same thing: an invoice that carries structured data a machine can read, not just a page a person can read.
Factur-X is the format that answers it. One PDF, two halves: the page your customer looks at, and an XML file embedded inside it that their accounting system reads. Get the XML wrong and the invoice bounces back.
Quaterio generates them from the same template and the same data you're already using:
curl -X POST https://quaterio.com/api/v1/generate/facturx \
-H "Authorization: Bearer q_..." \
-H "Content-Type: application/json" \
-d '{
"templateId": "monthly_invoice",
"invoice": {
"number": "INV-2026-0547",
"issueDate": "2026-08-06",
"typeCode": "380",
"currency": "EUR",
"seller": { "name": "Your Company", "vatId": "FR12345678901",
"address": { "country": "FR" } },
"buyer": { "name": "Acme Inc", "address": { "country": "DE" } },
"lines": [
{ "id": "1", "name": "Consulting", "quantity": "10",
"unitCode": "HUR", "netPrice": "150.00",
"vat": { "code": "S", "rate": "20" } }
]
}
}'
Two things worth knowing about how this works.
You don't send totals. Not the VAT, not the grand total, nothing. Send the lines and the rates, and the arithmetic is done for you, rounded the way the standard requires. Most rejected e-invoices fail on a total that doesn't reconcile with its own lines, and the easiest way to avoid that is to not have a place to type it in.
Every invoice is checked before you get it. The same rules our free Factur-X validator runs, applied to our own output. If the invoice doesn't pass, you get a 422 naming the rule and what to change, not a PDF that fails at your customer's end a week later.
The page and the XML come from one source, so the total your customer reads is the total their software reads. Factur-X requires that, and nothing else enforces it.
Available on Business and Enterprise plans, generating the EN 16931 profile.
Get Started
If you just need to send a single invoice without writing any code, the free invoice template gets you to a sent PDF in under two minutes. For automated, programmatic invoicing:
- Sign up for Quaterio and create a Business account
- Design your invoice template in the visual editor
- Add template variables for all dynamic fields
- Generate your API key in the organization settings
- Integrate the generation endpoint into your billing workflow
The entire setup - from template design to first generated invoice - typically takes less than an hour.