How my bank emails fill my Actual Budget
I track my personal finances in Actual Budget. It works a lot like YNAB and has the same problem: transactions have to get into it somehow. Typing them in by hand bored me, and services like Plaid or Salt Edge mostly don’t support regional European banks.
Actual can connect banks through GoCardless, so I tried that first. But the sync ran only a few times a day and some transactions never loaded at all. When a budget is missing transactions or gets them 12 hours late, you stop trusting it and the whole thing loses its point.
Then it hit me that my bank sends me this data anyway. A few minutes after every payment I get an email with the amount, the date, the merchant and usually a transaction ID. These notifications are reliable, in my experience about 99% of the time, because the bank has a strong reason to deliver them. So I decided to read the emails, even though it meant more setup work.
Since early 2024 I’ve had an import running that handles about 95% of my transactions on its own. Thousands of transactions from several banks and in different currencies have gone through it, and maintenance costs me about two hours a year.
How it works
On a schedule, the script connects to my mailbox over IMAP and fetches unread emails from the bank’s sender address. Each email goes through a parser that pulls out the amount, currency, date, merchant and transaction ID. The script then sends the transaction to Actual Budget through its API and marks the email as read, so the next run skips it. I don’t need instant imports, so once an hour or once a day is enough.
Every bank needs its own parser, because every bank formats its emails differently. I wrote my first one too rigidly. It looked for the exact text “Amount:”, and when the bank changed it to “Transaction amount:”, the import stopped. Rewriting it with looser patterns took me 20 minutes, and it has been quiet since.
For a coffee shop payment, the extraction looks like this.
| Field | In the email | API field |
|---|---|---|
| Amount | ”Amount: 45.67 EUR” | amount, currency |
| Date | ”2025-11-02 14:35” | date |
| Merchant | ”STARBUCKS #8234 SEATTLE WA” | payee_name |
| Transaction ID | ”ID: 221025/131110-2” | imported_id |
| Account | ”Card ending **5633” | account routing |
The transaction ID is the most important field, because it handles duplicates. Sometimes the bank sends the same notification twice, or the script crashes after the import but before it marks the email. Actual rejects the duplicate by its imported_id, so a repeated import breaks nothing and I never have to clean up by hand. Without IDs I would have to match on amount, date and merchant, and two coffees on the same day would collide.
The script keeps no state. Everything it needs is in the mailbox and in Actual, so I can run it by hand, change the schedule or skip a run. For the first run, when the mailbox holds hundreds of old notifications, a limit such as 100 emails per run keeps the script from hitting API limits.
If you have several accounts at one bank, aliases help. Send notifications from your checking account to you+checking@example.com and from savings to you+savings@example.com. The parser then knows from the address which budget account the transaction belongs to. Not every email provider and not every bank supports this, though.
Security
I give no third party access to my account and I store bank credentials nowhere. The script reads emails I get anyway, so anyone who breaks into my mailbox sees the notifications without it. For comparison, about 75% of Plaid connections go through bank APIs. For the rest, you give Plaid your bank login and it signs in to your online banking for you.
Don’t connect to your mailbox with your main password. Use an app password or OAuth, because you can revoke them at any time and your main access stays untouched. The connection should use TLS, and the script should only read emails and mark them as read. If you run it on a machine you control, such as your own server or a Raspberry Pi, the data stays with you.
When the bank changes its email
Banks rarely change their email templates, about once a year. When they do, the parser breaks and transactions stop coming in, which I notice right away. A fix takes me 15-30 minutes. I look at the new format, adjust the patterns and test it.
Those two hours a year are the price of my bank having no API. For me the trade is worth it, because a failure is always visible and the data in my budget never goes bad quietly.
I built it with Claude Code
I built the whole thing with Claude Code. It wrote the TypeScript implementation, the IMAP client, the parsing regexes and the Actual Budget API connection, so the system was running within a few hours. I spent most of the time testing different email formats and tuning edge cases.
I wouldn’t recommend it without basic programming skills. When the bank changes its format, you need to understand what the code does and be able to fix it. If you can read TypeScript or JavaScript and you’re comfortable on the command line, AI does most of the work. If not, it will only frustrate you.
When I wouldn’t do it
If your budget app or Plaid supports your bank, use that, because a native integration doesn’t break when an email changes. I also wouldn’t do it if your bank sends notifications only above a certain amount or only for some payment types, because part of your transactions would be missing from the budget. And for some people, typing transactions in by hand helps them notice where the money goes, which automation would weaken.
If you’ve built something similar, write to me at hi@andrejkostal.sk. I’m most curious about what broke for you.