Receipts to CSV: extract, validate and export
Turn a folder of receipt photos into one CSV for expense reports. Typed JSON per receipt from an API, then a short script for the spreadsheet.
6 min readA receipt to CSV pipeline has one job: take a pile of photos and produce rows your accountant or expense tool can import. The hard part is not writing the CSV. It is getting the same columns out of a crumpled thermal printout, a restaurant slip and a PDF e-receipt, so every row means the same thing.
This guide does it in two steps. An API call turns each receipt into typed JSON, then a short script flattens the JSON into one CSV.
Decide the columns first
CSV has no types. If one receipt gives 12.5 and another gives $12,50, the spreadsheet breaks on import. So fix the columns before you extract, and let the schema enforce them.
For an expense report, a good minimum is:
Resist adding fields you will not use. Output is the expensive side of the per-token bill, and extra fields give the model more chances to get something wrong. See pricing.
Step 1: one job per receipt
Send each photo as an image source with the same schema. Images go in as base64 data URLs, and any raster format works (PNG, JPEG, WebP, HEIC and others). A PDF e-receipt uses a document source instead. Same schema, same output shape. Source: API reference.
Creating a job returns right away with the job in pending. Do not hold a connection open. Create all jobs first, then collect results.
Two limits matter for a batch. POST /jobs/create allows 20 requests per 10 seconds and 60 per minute per key, and GET /jobs/:jobId/get allows 10 per 10 seconds and 30 per minute. A loop over 200 receipts will hit 429 rate_limit_exceeded if it ignores that, so wait for Retry-After seconds when you see it. Better, register a webhook_url and skip polling entirely (Jobs and queue).
Step 2: validate before you export
A CSV is a one-way door. Once a wrong total is in a sheet nobody reopens the photo. So filter at the edge:
Valid receipts go to the CSV. The rest go to a short "needs a look" list with the job id, so someone can open the original. This is the same habit as the invoice case in extracted totals that reconcile.
What goes wrong on real receipts
Three cases show up in every pile. Faded thermal paper may leave the date unreadable, so date must be allowed to come back null, which is why the validator above sends it to review instead of guessing. Receipts in several currencies need the currency column, because a total without one cannot be summed. And a duplicate photo of the same receipt produces two rows, so keep the merchant, date and total together as a key and drop repeats before you export.
Step 3: write the CSV
Quote every field and escape quotes. Merchant names contain commas.
If your accountant uses a locale with decimal commas, format on export, not in the schema. Keep numbers as numbers in the JSON.
Cost, and what to measure
Each job reports usage.cost once settled, so total the batch from your own responses. Do not assume a per-receipt price from a blog post, including this one. Run 20 of your real receipts, sum usage.cost, and multiply. A failed job refunds its hold in full, so a retry costs you nothing for the failed attempt. Top up (from $5) before the first job; there is no free balance.
For the single-receipt version with a longer schema, see Receipt OCR API: photo to typed JSON. For splitting a bill between people, see receipt scanner and bill splitter.
A working example
One receipt photo in, one typed object out. Replace the data URL with your own base64 image and the key with yours.
Poll GET https://veralens.ai/api/v1/jobs/:jobId/get every 2 to 3 seconds until status is completed, then JSON.parse(job.output). Get a key in Getting started and check every error code in the API reference.