Expense reporting is one of the most tedious tasks in any organization. Employees log expenses in an app or spreadsheet, someone has to aggregate them into a formatted report, finance reviews it, and eventually the numbers flow into accounting. At every step, someone is manually touching a spreadsheet.
If your application already stores expense data as JSON (from a REST API, a database query, or a mobile app), you are one API call away from generating a professional, categorized expense report — no spreadsheet software required. This guide shows you exactly how, using the ReportForge expense-report template.
What the Expense-Report Template Does
The expense-report template takes tabular expense data and generates a formatted report with:
- A summary header showing total expenses and total line-item count
- Expenses grouped and subtotaled by category
- Per-category subtotals with a grand total footer
- Date and vendor columns rendered cleanly when provided
- Print-optimized CSS — Ctrl+P produces a submission-ready PDF
The only required columns are description and amount. Adding category unlocks the grouped subtotals. Adding date and vendor enriches the line items with additional context.
From JSON Array to Expense Report
Most expense data starts as a JSON array from a database query or an API endpoint. The ReportForge API accepts CSV, so the first step is converting your JSON to CSV. Here is the full pipeline in JavaScript:
/** * Convert an array of expense objects to a formatted HTML expense report. * expenses: Array of { description, amount, date, category, vendor } */ async function generateExpenseReport(expenses, reportTitle) { // Step 1: Convert JSON to CSV const headers = ['description', 'amount', 'date', 'category', 'vendor']; const rows = expenses.map(e => [ `"${(e.description || '').replace(/"/g, '""')}"`, e.amount.toFixed(2), e.date || '', `"${(e.category || 'Uncategorized').replace(/"/g, '""')}"`, `"${(e.vendor || '').replace(/"/g, '""')}"`, ].join(',')); const csv = [headers.join(','), ...rows].join('\n'); // Step 2: Call the ReportForge API const response = await fetch('https://reportforge-api.vercel.app/api/csv-to-report', { method: 'POST', headers: { 'Content-Type': 'application/json', // 'X-API-Key': process.env.REPORTFORGE_API_KEY, // paid tiers }, body: JSON.stringify({ csv, template: 'expense-report', title: reportTitle, }), }); if (!response.ok) { throw new Error(`Report generation failed: ${response.status}`); } const { html, meta } = await response.json(); console.log(`Generated expense report: ${meta.rowCount} line items`); return html; } // Example: expenses from your database or API const expenses = [ { description: 'Office supplies', amount: 234.50, date: '2026-01-15', category: 'Office', vendor: 'Staples' }, { description: 'Software license', amount: 599.00, date: '2026-01-18', category: 'Software', vendor: 'Adobe' }, { description: 'Client lunch', amount: 87.25, date: '2026-01-20', category: 'Meals', vendor: 'Restaurant' }, { description: 'Uber rides', amount: 45.60, date: '2026-01-22', category: 'Travel', vendor: 'Uber' }, { description: 'Cloud hosting', amount: 149.00, date: '2026-01-25', category: 'Software', vendor: 'AWS' }, ]; const html = await generateExpenseReport(expenses, 'January 2026 Expenses');
Python: Converting from a Django or Flask API Response
If your backend returns expenses from a database ORM or REST endpoint, this Python function handles the JSON-to-CSV conversion and API call in one step.
import csv import io import os import requests from typing import List, Dict, Any def generate_expense_report(expenses: List[Dict[str, Any]], title: str) -> str: """ Generate an HTML expense report from a list of expense dicts. Each dict should have: description, amount, date (optional), category (optional), vendor (optional). """ fieldnames = ["description", "amount", "date", "category", "vendor"] buf = io.StringIO() writer = csv.DictWriter(buf, fieldnames=fieldnames, extrasaction="ignore") writer.writeheader() for expense in expenses: writer.writerow({ "description": expense.get("description", ""), "amount": f"{float(expense.get('amount', 0)):.2f}", "date": str(expense.get("date", "")), "category": expense.get("category", "Uncategorized"), "vendor": expense.get("vendor", ""), }) resp = requests.post( "https://reportforge-api.vercel.app/api/csv-to-report", json={ "csv": buf.getvalue(), "template": "expense-report", "title": title, }, headers={"X-API-Key": os.environ.get("REPORTFORGE_API_KEY", "")}, timeout=10, ) resp.raise_for_status() return resp.json()["html"] # Example: data from a Django queryset # expenses = list(Expense.objects.filter(user=request.user, month='2026-01').values()) # html = generate_expense_report(expenses, "January 2026 Expenses")
Building an Employee Expense Submission Workflow
A complete employee expense workflow has three stages: submission, approval, and reporting. Here is how to wire up the ReportForge API at the reporting stage using a Next.js API route.
import { getServerSession } from 'next-auth'; import { prisma } from '@/lib/prisma'; export default async function handler(req, res) { if (req.method !== 'GET') return res.status(405).end(); const session = await getServerSession(req, res); if (!session) return res.status(401).json({ error: 'Unauthorized' }); const { month } = req.query; // e.g., "2026-01" // 1. Fetch this user's approved expenses for the given month const expenses = await prisma.expense.findMany({ where: { userId: session.user.id, status: 'approved', date: { gte: new Date(`${month}-01`), lt: new Date(`${month}-31`) }, }, orderBy: [{ category: 'asc' }, { date: 'asc' }], }); if (expenses.length === 0) { return res.status(404).json({ error: 'No approved expenses found for this period' }); } // 2. Convert to CSV const headers = 'description,amount,date,category,vendor\n'; const rows = expenses .map(e => `"${e.description}",${e.amount.toFixed(2)},${e.date.toISOString().slice(0,10)},"${e.category}","${e.vendor || ''}"`) .join('\n'); const csv = headers + rows; // 3. Generate the report const reportRes = await fetch('https://reportforge-api.vercel.app/api/csv-to-report', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.REPORTFORGE_API_KEY, }, body: JSON.stringify({ csv, template: 'expense-report', title: `${session.user.name} — Expense Report ${month}`, }), }); const { html } = await reportRes.json(); // 4. Return the HTML — the client can display it in an iframe or trigger a print res.setHeader('Content-Type', 'text/html'); res.send(html); }
Common Expense Tracking Use Cases
Monthly Employee Reimbursements
Employees log expenses in a mobile app. At month-end, each person's approved expenses are compiled into a formatted report and emailed to finance.
Project Budget Tracking
Expenses tagged to a project are aggregated on demand. Project managers can generate a current budget report at any time without waiting for finance.
Client Billing Reimbursements
Consultants log client-facing expenses (travel, meals, supplies). The report is attached to the monthly invoice to support reimbursement claims.
Department Spending Reviews
Finance teams pull all departmental expenses for a quarter, generate a categorized breakdown, and include it in board-level spending reviews.
Handling Multicurrency Expense Data
If your team operates across multiple currencies, normalize amounts to a single currency before calling the API. The expense-report template renders amount values as plain numbers — currency formatting is applied by the template. Here is a normalization pattern using exchange rates:
const RATES = { USD: 1, EUR: 1.08, GBP: 1.27, CAD: 0.73, }; function normalizeToUsd(expenses) { return expenses.map(e => ({ ...e, amount: parseFloat((e.amount * (RATES[e.currency] || 1)).toFixed(2)), description: e.currency !== 'USD' ? `${e.description} (${e.currency} ${e.amount})` : e.description, })); } // Usage const normalizedExpenses = normalizeToUsd(rawExpenses); const html = await generateExpenseReport(normalizedExpenses, 'Q1 Expenses (USD)');
Integrating with Expense Tracking Tools
Many teams already use tools like Expensify, Ramp, or Brex for expense capture. These tools provide export APIs or CSV downloads that you can pipe directly into ReportForge. Here is the pattern for a Ramp CSV export:
import pandas as pd import requests import io import os def ramp_csv_to_report(ramp_export_path: str, title: str) -> str: """ Convert a Ramp expense export CSV to a ReportForge expense report. Ramp columns: Transaction Date, Merchant, Amount, Category, Employee """ df = pd.read_csv(ramp_export_path) # Normalize Ramp column names to ReportForge expected names df = df.rename(columns={ "Merchant": "vendor", "Transaction Date": "date", "Amount": "amount", "Category": "category", "Employee": "description", }) # Keep only the columns we need df = df[["description", "amount", "date", "category", "vendor"]] df["amount"] = df["amount"].abs() # Ramp exports as negative buf = io.StringIO() df.to_csv(buf, index=False) resp = requests.post( "https://reportforge-api.vercel.app/api/csv-to-report", json={"csv": buf.getvalue(), "template": "expense-report", "title": title}, headers={"X-API-Key": os.environ["REPORTFORGE_API_KEY"]}, timeout=10, ) resp.raise_for_status() return resp.json()["html"]
Sending Reports to Finance Automatically
Once you have the HTML report, you can deliver it in several ways depending on your workflow:
- Email — Send the HTML as the email body. Most email clients render it correctly. Alternatively, save to PDF with Puppeteer and attach.
- Slack — Save to PDF, upload to a Slack channel using the Files API. Finance teams often prefer Slack over email for approval workflows.
- Google Drive / SharePoint — Save the HTML file to a shared drive folder using the Drive API. Finance can download and print directly.
- In-app preview — Render the HTML in an iframe inside your expense management app so employees can review before submitting.
Tip: For in-app previews, set the iframe's srcdoc attribute to the HTML string. This avoids cross-origin restrictions and requires no file upload.
Summary
The ReportForge expense-report template reduces expense reporting to a simple data transformation. Your JSON expense data is already structured — converting it to CSV takes a few lines of code, and the API handles everything else: layout, category grouping, subtotals, and print formatting.
Whether you are building an internal expense tool, automating monthly reimbursements, or integrating with an existing expense platform, the expense-report template gives you professional output without the overhead of a dedicated reporting library.
Try the Expense Report Template Free
Generate your first expense report in under a minute — no account required.
Try It Live →