Tutorial: Invoice Generator
Build the classic flow: a Download PDF button on an invoice page that renders a real, branded invoice. This mirrors the open-source demo at github.com/Reforgio/reforgio-demo , which you can run to see the finished result.
Create the invoice template
In the dashboard , create a template (format: PDF) with:
- an HTML section for the header — company and customer details, using variables like
{{invoice_number}},{{date}},{{company_name}},{{customer_name}}; - a table section with
dataKey: itemsand columnsdescription(string),quantity(number),unit_price(currency),total(currency); - an HTML section for the totals —
{{subtotal}},{{tax_rate}},{{tax_amount}},{{total_amount}}.
Publish it and note the template ID (visible in the template’s URL).
Write the server action
The action maps your domain objects (invoice, customer) to the template’s flat variable payload:
'use server'
import { ReforgioServer, ReforgioError } from '@reforgio/sdk-core/server'
const reforgio = new ReforgioServer(process.env.REFORGIO_SECRET_KEY!)
export type RenderResult =
| { success: true; downloadUrl: string; renderTimeMs: number }
| { success: false; error: string }
export interface Invoice {
number: string
date: string
dueDate: string
status: string
items: Array<{ description: string; quantity: number; unitPrice: number }>
}
export interface Customer {
name: string
email: string
address: string
}
export async function exportInvoice(invoice: Invoice, customer: Customer): Promise<RenderResult> {
const subtotal = invoice.items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0)
const taxRate = 0.23
const taxAmount = subtotal * taxRate
try {
const doc = await reforgio.render(process.env.REFORGIO_TEMPLATE_INVOICE_ID!, {
invoice_number: invoice.number,
date: invoice.date,
due_date: invoice.dueDate,
status: invoice.status,
company_name: 'ACME Corp',
customer_name: customer.name,
customer_email: customer.email,
customer_address: customer.address,
items: invoice.items.map((item) => ({
description: item.description,
quantity: item.quantity,
unit_price: item.unitPrice,
total: item.quantity * item.unitPrice,
})),
subtotal,
tax_rate: taxRate * 100,
tax_amount: taxAmount,
total_amount: subtotal + taxAmount,
})
return { success: true, downloadUrl: doc.downloadUrl, renderTimeMs: doc.renderTimeMs }
} catch (err) {
if (err instanceof ReforgioError) {
return { success: false, error: err.message }
}
throw err
}
}Note the mapping pattern: computed values (total per row, subtotal, tax_amount) are calculated in code and passed as plain variables — templates receive ready-to-print data.
Add the download button
'use client'
import { useState, useTransition } from 'react'
import { exportInvoice, type Invoice, type Customer, type RenderResult } from '../../actions'
export function InvoiceExportButton({ invoice, customer }: { invoice: Invoice; customer: Customer }) {
const [isPending, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)
function handleClick() {
setError(null)
startTransition(async () => {
const result: RenderResult = await exportInvoice(invoice, customer)
if (result.success) {
window.open(result.downloadUrl)
} else {
setError(result.error)
}
})
}
return (
<div>
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Generating…' : 'Download PDF'}
</button>
{error && <p role="alert">Export failed: {error}</p>}
</div>
)
}Run it
Set the env vars and click the button:
REFORGIO_SECRET_KEY=sk_live_...
REFORGIO_TEMPLATE_INVOICE_ID=<your template id>The PDF opens in a new tab from the presigned downloadUrl.
Variations
- XLSX export — pass
'xlsx'as the third argument torender(template needs the XLSX format and a table section). The demo’s overdue invoices export does exactly this. - Receipts, certificates, reports — same pattern, different template + payload; the demo repo implements all of them in
app/actions.ts. - Client-side preview — render the invoice in an
<iframe>as the user edits, using frontend rendering withuseRender.
Rendering from a server action keeps your secret key on the server and lets you derive totals from trusted data. Use frontend rendering when you want live previews without a round-trip through your own backend.