Generate Invoice PDFs
Create professional invoice PDFs with line items, calculations, and your company branding.
Overview​
This recipe shows you how to:
- Design an invoice template
- Generate invoices with dynamic data
- Handle multiple line items
- Include calculated totals
The Invoice Template​
Create a template with these sections:
Header​
- Company logo
- Company name and address
- Invoice number and date
Customer Info​
- Customer name
- Billing address
- Contact information
Line Items Table​
- Description
- Quantity
- Unit price
- Line total
Summary​
- Subtotal
- Tax
- Total due
- Payment terms
Template Variables​
Define these variables in your template:
{
"variables": [
{ "name": "invoiceNumber", "type": "string", "required": true },
{ "name": "invoiceDate", "type": "string", "required": true },
{ "name": "dueDate", "type": "string", "required": true },
{ "name": "customerName", "type": "string", "required": true },
{ "name": "customerAddress", "type": "string" },
{ "name": "customerEmail", "type": "string" },
{ "name": "lineItems", "type": "array", "required": true },
{ "name": "subtotal", "type": "number", "required": true },
{ "name": "taxRate", "type": "number" },
{ "name": "taxAmount", "type": "number" },
{ "name": "total", "type": "number", "required": true },
{ "name": "notes", "type": "string" }
]
}
Generate Invoice​
Node.js​
import { Rynko } from '@rynko/sdk';
const client = new Rynko({ apiKey: process.env.RYNKO_API_KEY });
async function generateInvoice(order) {
// Calculate line items
const lineItems = order.items.map(item => ({
description: item.name,
quantity: item.quantity,
unitPrice: item.price,
total: item.quantity * item.price
}));
// Calculate totals
const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0);
const taxRate = 0.10; // 10% tax
const taxAmount = subtotal * taxRate;
const total = subtotal + taxAmount;
// Generate invoice
const result = await client.documents.generate({
templateId: 'invoice-template',
format: 'pdf',
variables: {
invoiceNumber: `INV-${order.id}`,
invoiceDate: new Date().toLocaleDateString(),
dueDate: getDueDate(30), // Net 30
customerName: order.customer.name,
customerAddress: formatAddress(order.customer.address),
customerEmail: order.customer.email,
lineItems,
subtotal: subtotal.toFixed(2),
taxRate: (taxRate * 100).toFixed(0) + '%',
taxAmount: taxAmount.toFixed(2),
total: total.toFixed(2),
notes: 'Payment due within 30 days. Thank you for your business!'
},
filename: `invoice-${order.id}`
});
return result.downloadUrl;
}
function getDueDate(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toLocaleDateString();
}
function formatAddress(addr) {
return `${addr.street}\n${addr.city}, ${addr.state} ${addr.zip}`;
}
Python​
from rynko import Rynko
from datetime import datetime, timedelta
client = Rynko(api_key=os.environ['RYNKO_API_KEY'])
def generate_invoice(order):
# Calculate line items
line_items = [{
'description': item['name'],
'quantity': item['quantity'],
'unitPrice': item['price'],
'total': item['quantity'] * item['price']
} for item in order['items']]
# Calculate totals
subtotal = sum(item['total'] for item in line_items)
tax_rate = 0.10
tax_amount = subtotal * tax_rate
total = subtotal + tax_amount
# Generate invoice
result = client.documents.generate(
template_id='invoice-template',
format='pdf',
variables={
'invoiceNumber': f"INV-{order['id']}",
'invoiceDate': datetime.now().strftime('%B %d, %Y'),
'dueDate': (datetime.now() + timedelta(days=30)).strftime('%B %d, %Y'),
'customerName': order['customer']['name'],
'customerAddress': format_address(order['customer']['address']),
'customerEmail': order['customer']['email'],
'lineItems': line_items,
'subtotal': f"{subtotal:.2f}",
'taxRate': f"{tax_rate * 100:.0f}%",
'taxAmount': f"{tax_amount:.2f}",
'total': f"{total:.2f}",
'notes': 'Payment due within 30 days. Thank you for your business!'
},
filename=f"invoice-{order['id']}"
)
return result['downloadUrl']
Batch Invoice Generation​
Generate all monthly invoices at once:
async function generateMonthlyInvoices(month, year) {
const orders = await getOrdersForMonth(month, year);
const documents = orders.map(order => {
const lineItems = calculateLineItems(order);
const { subtotal, taxAmount, total } = calculateTotals(lineItems);
return {
variables: {
invoiceNumber: `INV-${month}${year}-${order.id}`,
invoiceDate: new Date().toLocaleDateString(),
dueDate: getDueDate(30),
customerName: order.customer.name,
customerAddress: formatAddress(order.customer.address),
lineItems,
subtotal: subtotal.toFixed(2),
taxAmount: taxAmount.toFixed(2),
total: total.toFixed(2)
},
filename: `invoice-${order.customer.name.toLowerCase().replace(/\s+/g, '-')}-${month}${year}`,
metadata: {
orderId: order.id,
customerId: order.customer.id
}
};
});
const batch = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents
});
console.log(`Submitted ${documents.length} invoices. Batch ID: ${batch.batchId}`);
return batch;
}
Template Design Tips​
1. Use a Clean Layout​
+------------------------------------------+
| [LOGO] INVOICE |
| Invoice #: {{invoiceNumber}} |
| Date: {{invoiceDate}} |
| Due: {{dueDate}} |
+------------------------------------------+
| BILL TO: |
| {{customerName}} |
| {{customerAddress}} |
+------------------------------------------+
| Description Qty Price Total |
| {{#each lineItems}} |
| {{description}} {{qty}} {{price}} {{total}}|
| {{/each}} |
+------------------------------------------+
| Subtotal: {{subtotal}}|
| Tax: {{taxAmount}}|
| TOTAL: {{total}} |
+------------------------------------------+
| {{notes}} |
+------------------------------------------+
2. Format Currency Consistently​
Always format currency in your code before sending:
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
variables: {
subtotal: formatCurrency(subtotal),
total: formatCurrency(total)
}
3. Handle Long Line Item Descriptions​
Use text wrapping in your template for long descriptions:
lineItems: items.map(item => ({
description: item.name.substring(0, 50) + (item.name.length > 50 ? '...' : ''),
// ... other fields
}))
Integration Examples​
After Payment (Stripe)​
// Stripe webhook handler
app.post('/webhooks/stripe', async (req, res) => {
const event = req.body;
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object;
const order = await getOrderByPaymentIntent(paymentIntent.id);
// Generate invoice
const invoiceUrl = await generateInvoice(order);
// Save invoice URL
await saveInvoiceUrl(order.id, invoiceUrl);
// Optionally send via email service
await sendInvoiceEmail(order.customer.email, invoiceUrl);
}
res.status(200).send('OK');
});
Scheduled Generation (Cron)​
// Run on the 1st of each month
cron.schedule('0 0 1 * *', async () => {
const lastMonth = getLastMonth();
await generateMonthlyInvoices(lastMonth.month, lastMonth.year);
});
Next Steps​
- Batch generation for high-volume invoicing
- Excel reports for invoice summaries
- Webhooks for async processing