Skip to main content
Import bank exports with the CLI

Import bank exports with the CLI

Preview a bank export with bea, review duplicate candidates, and apply validated transactions to your local Beancount ledger.

Use bea import to preview a bank export, review duplicates, and append validated entries to your local ledger.

You need an existing ledger and a Python importer for your bank's exact export format. If you are starting new books, follow the CLI quick start. Keep the original bank export so you can compare it with the preview.

1. Choose an importer

An importer reads the bank's file and supplies the transaction accounts. Bea does not guess the format or categorize purchases with an AI model.

Your importers.py configuration exports CONFIG = [importer, ...]. Importers use the current Beangulp interface: identify(filepath), account(filepath), and extract(filepath, existing). Source-account postings need explicit amounts for duplicate matching.

For a first practice run, save the example categorized CSV configuration as importers.py beside your root ledger. It uses only Beancount and Python's standard library, so it works with the Homebrew installation.

Save this sample as bank.csv in the same directory:

Date,Payee,Narration,Amount,Currency,Category,BankID
2026-08-02,Cafe,Coffee,-5.25,USD,Expenses:Dining,bank-001
2026-08-03,Employer,Salary,1000,USD,Income:Salary,bank-002

The sample uses a signed checking-account amount: spending is negative and a deposit is positive. Category supplies the other account. Both categories are in the USD template created by bea init.

Use an importer written for your bank when importing its native CSV, OFX, or QIF. The sample configuration expects exactly the columns above. Only execute Python configurations you trust.

2. Preview the entries

Run this from the directory containing main.bean:

bea import bank.csv --config importers.py

Nothing is written to the ledger yet. Review the preview's dates, payees, signed source amounts, destination accounts, duplicate matches, and proposed file diff.

For the sample, the preview should contain a 5.25 USD dining expense and a 1,000 USD salary deposit. Fix an incorrect category in the importer or source data, then preview again. Open any missing accounts before applying the import.

If several importers recognize the file, select one by name:

bea import bank.csv --config importers.py --importer categorized-checking

An unknown name lists the configured names. A known importer that does not recognize the file reports that separately.

3. Apply the reviewed entries

bea import bank.csv --apply
bea check
bea list transaction --limit 10

The CLI remembers the configuration path for this root ledger. Future runs choose the explicit --config, then the remembered path, then importers.py beside the root. The output names the path and where it came from.

--apply recomputes the preview against the current files. It validates the complete candidate ledger before writing. A validation failure leaves the original ledger unchanged and exits 1. A concurrent ledger change exits 4; inspect the change and run a fresh preview before retrying.

4. Resolve possible duplicates

Repeating the same sample import skips its existing entries. An overlapping export can also contain rows that need a decision:

Preview statusMeaningWhat to do
newNo duplicate evidence foundCheck the amounts and categories
duplicateA stable ID and transaction details match, or an identical nontransaction directive existsAlready skipped
possible_duplicateThe date, normalized payee, and signed source amount/currency matchCompare the preview with the existing entry
conflictA stable ID matches different transaction detailsResolve the ID or data discrepancy, then preview again

A different bank ID does not rule out a duplicate. Banks can change IDs on later downloads. Two real purchases can also share a date, payee, and amount.

After reviewing every possible match, choose one of these alternatives:

bea import bank.csv --apply --duplicates skip
bea import bank.csv --apply --duplicates include

The decision applies to all possible matches in that invocation. Exact duplicates remain skipped. ID conflicts still block the write.

The default --duplicates review refuses to apply unresolved matches. It exits 4 and names the affected preview rows. --no-input and --yes do not bypass that review. An intentional decision to skip every row exits 0 with no ledger additions.

Keep imports repeatable

By default, duplicate matching checks bank_id, fitid, transaction_id, and imported_id metadata within the importer's source account. Use repeated --id-key KEY options to replace that set.

The CLI also writes bea_import_id metadata to identify the row in the original export. Retain it when editing imported entries. Possible matches are checked against existing transactions and accepted rows in the same batch.

Payees, narrations, and string metadata replace line breaks with spaces before preview and writing. Quotes and backslashes keep their contents. Imported merchant text therefore stays readable on a single ledger line.

Importing adds entries; it does not update or delete an existing transaction. Make corrections deliberately in your ledger and run bea check afterward. Bulk JSON entry with bea add transactions has no duplicate detection.

Write to an included file

Keep --file pointed at the root and select the destination with --into:

bea --file ~/my-books/main.bean import bank.csv --into 2026.bean
bea --file ~/my-books/main.bean import bank.csv --into 2026.bean --apply

2026.bean must already exist and be included by the root. Its path is relative to the root directory. The export path remains relative to your working directory. The preview identifies the file that will change.

Use imports in a script

bea --json --no-input import bank.csv --apply --duplicates skip

Choose skip only when that is your intended policy for possible matches. JSON returns the preview and write count inside data. Refused applications put the preview in error.result on stderr, with written: 0. Always check the exit status. See the JSON and exit-code reference before scheduling unattended imports.

Troubleshoot an importer

If the configuration imports third-party packages, those packages must be in the Python environment running bea. For example:

uv run --with beancount-io --with beangulp \
  bea --file ~/my-books/main.bean import bank.ofx --config importers.py

Add --with YOUR_IMPORTER_PACKAGE for a separately installed bank importer. This uses a separate environment from Homebrew.

For an importer exception, put --debug before the command to show its traceback:

bea --debug import bank.csv --config importers.py

Importer output is captured in importer_output so it does not corrupt JSON. In JSON debug mode, the traceback is error.traceback.

For a one-time conversion without a Python importer, try the CSV converter or OFX and QIF converter. Review the generated entries before adding them to your books.