Skip to main content

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:

  • 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​