Invoicing is one of those tasks that looks simple until you actually have to do it at scale. Generating one invoice for a one-off client is easy — open a template in Word, fill in the fields, save as PDF, send. But when you have dozens of clients, recurring engagements, and varying line items, the manual approach becomes unsustainable quickly.

Dedicated invoicing software is one option, but it comes with subscription costs, vendor lock-in, and yet another dashboard to manage. A better approach for developers: generate invoices programmatically from your own billing data, using the ReportForge invoice template. Your data stays in your systems, and invoices are generated on demand with a single API call.

What the Invoice Template Produces

The invoice template generates a complete, print-ready HTML invoice with:

The output is self-contained HTML with all styles inline, so it renders correctly in email clients and can be saved as a standalone file.

CSV Schema for the Invoice Template

The invoice template expects your CSV to have one row per line item. Client information (name, email, invoice number) is read from the first row and applied to the invoice header.

Column Required Description
description Required Line item description (e.g., "Web Development")
amount Required Line item total in decimal (e.g., 6000.00)
quantity Optional Units, hours, or quantity for the line item
unit_price Optional Price per unit (displayed alongside quantity)
invoice_number Optional Invoice ID — displayed in the header (read from row 1)
client_name Optional Client or company name — displayed in the header
client_email Optional Client email address — displayed in the header
Sample Input CSV
description,quantity,unit_price,amount,invoice_number,client_name,client_email
Web Development,40,150.00,6000.00,INV-2026-042,Acme Corp,billing@acme.com
UI Design,20,125.00,2500.00,INV-2026-042,Acme Corp,billing@acme.com
Project Management,10,100.00,1000.00,INV-2026-042,Acme Corp,billing@acme.com
Code Review,5,150.00,750.00,INV-2026-042,Acme Corp,billing@acme.com

Note: The invoice_number, client_name, and client_email values are read from the first data row and applied to the entire invoice. You do not need to repeat them on every line item — the API reads them once.

Node.js Integration Example

Here is a complete invoice generation function that takes a billing record from your database and produces a ready-to-send HTML invoice. This pattern fits naturally into a webhook handler or a billing event listener.

JavaScript — generate-invoice.js
const API_URL = 'https://reportforge-api.vercel.app/api/csv-to-report';

/**
 * Generate an HTML invoice from a billing record.
 * @param {Object} billing - Your internal billing object
 * @returns {Promise<string>} Complete HTML invoice
 */
export async function generateInvoice(billing) {
  // Build CSV rows from line items
  const headers = ['description', 'quantity', 'unit_price', 'amount', 'invoice_number', 'client_name', 'client_email'];
  const rows = billing.lineItems.map((item, index) => {
    return [
      `"${item.description}"`,
      item.quantity,
      item.unitPrice.toFixed(2),
      item.amount.toFixed(2),
      index === 0 ? billing.invoiceNumber : billing.invoiceNumber, // same on every row
      index === 0 ? `"${billing.clientName}"` : `"${billing.clientName}"`,
      index === 0 ? billing.clientEmail : billing.clientEmail,
    ].join(',');
  });

  const csv = [headers.join(','), ...rows].join('\n');

  const response = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.REPORTFORGE_API_KEY,
    },
    body: JSON.stringify({
      csv,
      template: 'invoice',
      title: `Invoice ${billing.invoiceNumber}`,
    }),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`Invoice generation failed: ${err.error}`);
  }

  const { html } = await response.json();
  return html;
}

Wiring Into a Stripe Webhook

A common pattern is to generate and email an invoice automatically when a payment succeeds in Stripe. Here is how to connect the two:

JavaScript — stripe-webhook.js (Express)
import express from 'express';
import Stripe from 'stripe';
import { generateInvoice } from './generate-invoice.js';
import { sendInvoiceEmail } from './mailer.js';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payment_intent.succeeded') {
    const pi = event.data.object;

    // Build a billing record from your database using pi.metadata
    const billing = await getBillingRecord(pi.metadata.orderId);

    // Generate the invoice HTML
    const invoiceHtml = await generateInvoice(billing);

    // Email it to the client
    await sendInvoiceEmail({
      to: billing.clientEmail,
      subject: `Invoice ${billing.invoiceNumber} — Payment Confirmed`,
      html: invoiceHtml,
    });

    console.log(`Invoice ${billing.invoiceNumber} sent to ${billing.clientEmail}`);
  }

  res.json({ received: true });
});

Python Integration Example

Here is the same invoice generation logic in Python, using dataclasses to model your billing structure and requests for the API call.

Python — invoice_generator.py
from dataclasses import dataclass
from typing import List
import csv
import io
import os
import requests

@dataclass
class LineItem:
    description: str
    quantity: float
    unit_price: float
    amount: float

@dataclass
class BillingRecord:
    invoice_number: str
    client_name: str
    client_email: str
    line_items: List[LineItem]

def generate_invoice(billing: BillingRecord) -> str:
    """
    Generate a professional HTML invoice from a BillingRecord.
    Returns the complete HTML string.
    """
    fieldnames = ["description", "quantity", "unit_price", "amount",
                  "invoice_number", "client_name", "client_email"]

    buf = io.StringIO()
    writer = csv.DictWriter(buf, fieldnames=fieldnames)
    writer.writeheader()

    for item in billing.line_items:
        writer.writerow({
            "description": item.description,
            "quantity": item.quantity,
            "unit_price": f"{item.unit_price:.2f}",
            "amount": f"{item.amount:.2f}",
            "invoice_number": billing.invoice_number,
            "client_name": billing.client_name,
            "client_email": billing.client_email,
        })

    resp = requests.post(
        "https://reportforge-api.vercel.app/api/csv-to-report",
        json={
            "csv": buf.getvalue(),
            "template": "invoice",
            "title": f"Invoice {billing.invoice_number}",
        },
        headers={"X-API-Key": os.environ["REPORTFORGE_API_KEY"]},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["html"]


# Example usage
billing = BillingRecord(
    invoice_number="INV-2026-042",
    client_name="Acme Corp",
    client_email="billing@acme.com",
    line_items=[
        LineItem("Web Development", 40, 150.00, 6000.00),
        LineItem("UI Design", 20, 125.00, 2500.00),
        LineItem("Project Management", 10, 100.00, 1000.00),
    ],
)

html = generate_invoice(billing)
with open("invoice-INV-2026-042.html", "w") as f:
    f.write(html)
print("Invoice saved.")

Numbering Invoices Automatically

Professional invoice numbering typically follows a pattern like INV-YYYY-NNN. Here is a simple sequence generator using a database counter that guarantees unique, incrementing invoice numbers even under concurrent load.

JavaScript — invoice-number.js
import { pool } from './db.js';

export async function nextInvoiceNumber() {
  const year = new Date().getFullYear();

  // Atomic increment — safe under concurrent requests
  const { rows } = await pool.query(
    `INSERT INTO invoice_sequences (year, last_seq)
     VALUES ($1, 1)
     ON CONFLICT (year) DO UPDATE
       SET last_seq = invoice_sequences.last_seq + 1
     RETURNING last_seq`,
    [year]
  );

  const seq = String(rows[0].last_seq).padStart(3, '0');
  return `INV-${year}-${seq}`;
}
// Returns: INV-2026-001, INV-2026-002, INV-2026-003...

Storing and Retrieving Generated Invoices

For compliance and audit purposes, you should persist generated invoices alongside your billing records. The HTML string can be stored in your database as a text field, or you can save it to object storage (S3, Vercel Blob, Cloudflare R2) and store the URL.

JavaScript — store-invoice.js
import { put } from '@vercel/blob';
import { pool } from './db.js';

export async function storeInvoice(invoiceNumber, html) {
  // Upload HTML to Vercel Blob storage
  const { url } = await put(
    `invoices/${invoiceNumber}.html`,
    html,
    { access: 'private', contentType: 'text/html' }
  );

  // Save URL to database for retrieval
  await pool.query(
    'UPDATE invoices SET html_url = $1, generated_at = NOW() WHERE invoice_number = $2',
    [url, invoiceNumber]
  );

  return url;
}

Rate Limits and Scaling

The free tier allows 5 reports per day, which is enough to build and test your integration. For production invoice systems:

For burst scenarios (e.g., month-end billing runs), consider batching your invoice generation requests with a small delay between calls to stay within rate limits on the Starter plan.

Summary

The ReportForge invoice template gives you a complete, professional invoice generation system without dedicated invoicing software. Your billing data lives in your own database, invoices are generated on demand via API, and the output is print-ready HTML that works in any browser and email client.

The Node.js and Python examples above cover the most common integration patterns — connect to your billing system, call the API, and deliver the invoice wherever it needs to go.

Start Generating Invoices Free

Try the invoice template right now — no API key or sign-up required.

Try It Live →