Skip to content

CSV Export

sarmakska edited this page Jun 7, 2026 · 1 revision

CSV Export

Turn scanned receipts into a CSV file that opens in Excel, Google Sheets, and Numbers, and imports into most bookkeeping tools.

Why CSV alongside OFX

OFX carries one transaction per receipt and has no concept of line items, so itemised detail is lost on that path. CSV fills the gap. It is the universal lowest-common-denominator import format, and the line-item layout preserves every line a receipt printed, which is what you want for category analysis and VAT work.

Use CSV when you want a spreadsheet you can sort, pivot, and reconcile by hand, or when your tool imports CSV but not OFX. Use OFX when you want transactions to land in a bank-feed-style reconciliation queue. They are complementary, not either/or.

The endpoint

POST /api/export/csv with a JSON body:

{
  "receipts": [ { "vendor": "Tesco", "date": "2026-05-21", "total": 5.35, "currency": "GBP", "items": [], "...": "..." } ],
  "layout": "summary"
}

layout is optional and is one of:

  • summary (the default): one row per receipt.
  • items: one row per purchased line, with the parent receipt's vendor and date repeated on each line.

The response is a downloadable file:

Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="receipts.csv"

The items layout downloads as receipt-items.csv.

The UI wires this to two buttons, Export CSV and Export items CSV, which POST the receipts currently on screen and trigger a download.

Summary layout

One row per receipt. Columns:

vendor,vendor_address,date,time,currency,subtotal,tax,tip,total,payment_method,item_count,notes
Tesco,"1 High Street, London, EC1A 1AA",2026-05-21,14:32,GBP,5.35,0,,5.35,Visa ****1234,3,

item_count is the number of line items read for that receipt, so you can spot receipts that scanned without their lines. Null fields are written as empty cells.

Items layout

One row per purchased line. Columns:

vendor,date,currency,item_description,quantity,unit_price,line_total
Tesco,2026-05-21,GBP,Semi-Skimmed Milk 2L,1,1.45,1.45
Tesco,2026-05-21,GBP,Wholemeal Bread,2,0.9,1.8
Tesco,2026-05-21,GBP,Free Range Eggs x6,1,2.1,2.1

A receipt that scanned with no line items still produces a single fallback row carrying its total, so a reconciler never silently drops it.

Format details

The output is RFC 4180:

  • CRLF (\r\n) line endings, with a trailing newline.
  • A field is quoted only when it contains a comma, a double quote, or a newline. Embedded quotes are doubled, so said "cheap" becomes "said ""cheap""" and Smith, Jones & Co becomes "Smith, Jones & Co".
  • A UTF-8 byte-order mark is prepended by default so Excel opens accented vendor names as UTF-8 rather than mojibake. Disable it with { bom: false } when calling the library directly.

Implementation

lib/csv.ts exports receiptsToCsv(receipts, options). It is pure (no I/O) and covered by lib/csv.test.ts against a real receipt fixture. Use it directly if you want CSV outside the HTTP route, for example in a scheduled export job.

import { receiptsToCsv } from '@/lib/csv'

const summary = receiptsToCsv(receipts)                       // one row per receipt, with Excel BOM
const lines = receiptsToCsv(receipts, { layout: 'items' })    // one row per line item
const raw = receiptsToCsv(receipts, { bom: false })           // no BOM, for tools that choke on it

Importing

  • Excel / Numbers: open the .csv directly. The BOM keeps accented characters correct.
  • Google Sheets: File -> Import -> Upload, choose "Replace spreadsheet" or "Insert new sheet".
  • Bookkeeping tools: most accept a CSV bank or expense import; map the columns in their import wizard.

See also

  • OFX-Export: bank-statement export for Xero, QuickBooks, and GnuCash.
  • Batch-Upload: scan a whole folder before exporting.
  • Wire-to-Database: direct accounting-API integration as an alternative to file import.

Clone this wiki locally