Introduction
My bank exports statements as CAMT.053 XML files — a dry ISO 20022 format nobody outside of finance wants to think about.
Every month I'd download them, manually tag transactions, and copy everything into a spreadsheet. It was tedious enough that I finally automated it.
How it works
Once the app is running, open http://localhost:8080 in your browser. There's a simple drag-and-drop UI — drop a .zip (multiple statements) or a single .xml file and it processes immediately. No curl, no terminal.
If you prefer the command line:
curl -F "file=@statement.xml" http://localhost:8080/api/payments/upload-xml
It parses every transaction out of the ISO 20022 XML, then runs them through a two-pass categorization:
- Keyword matching — fast and deterministic. Rules like
"albert heijn" → Groceriesor"zilveren kruis" → Health Insurancecover the bulk of transactions instantly. - AI fallback — anything that doesn't match a keyword gets sent to GPT-4o-mini in bulk. Optional, but useful for the long tail of one-off merchants.
The response is minimal by design:
{
"fileCount": 1,
"transactionCount": 42,
"uploadedToGoogleSheets": true,
"message": "Parsed 42 transactions from 1 files and uploaded to Google Sheets"
}
The Google Sheets side
Rather than dealing with the Sheets API and OAuth, I used a Google Apps Script web app as a lightweight write endpoint. It's a doPost function deployed publicly that receives a JSON payload and appends rows to an Expenses sheet. No credentials to manage, no SDK — just a URL.
Each row written to the sheet looks like this:
| importedAt | date | amount | currency | type | category | details |
|---|---|---|---|---|---|---|
| 2026-03-01 10:42 | 2026-02-28 | 49.99 | EUR | DBIT | Groceries | Albert Heijn |
The Spring app POSTs to that URL after categorization is done. If the URL isn't configured, it just skips the upload and still returns the parsed transactions — useful for one-off processing without the sheet.
Running it
No Java installation required if you use Docker:
docker build -t bank2budget .
docker run -p 8080:8080 \
-e GOOGLE_SHEETS_URL="https://script.google.com/macros/s/YOUR_ID/exec" \
-e OPENAI_API_KEY="sk-..." \
bank2budget
Both env vars are optional — the app degrades gracefully without them.
What's to come
The categorization rules are Dutch-centric right now (ING/ABN ecosystem keywords). Making them locale-agnostic and user-configurable via a config file would make this genuinely portable. The AI fallback partially compensates, but keyword rules are faster and cheaper for the common cases.
The other thing I'd reconsider is using Google Apps Script as the Sheets adapter. It works, but deployments are manual and version management is clunky. A proper Sheets API integration with a service account would be more reliable for anything beyond personal use.
Github
The source is on my GitHub if you want to try it, or adapt it to your own bank's export format.