How to merge monthly bank statement csvs
- Step 1Export each month from your online banking — Most retail banks allow per-month CSV export from the Statements page: pick the month, download. Some (Chase, Wells Fargo) allow custom date-range exports — pick 1-month ranges for consistency. Some (HSBC Business) email a download link rather than direct-download. Most digital-first banks (Monzo, Revolut, Starling) allow per-month or full-history CSV from the app. Whatever the source, save each month's CSV with a consistent filename pattern (
chase-checking-2025-01.csv,chase-checking-2025-02.csv, ...) for traceability. - Step 2Drop all the monthly CSVs onto the merger above — Free tier allows 2 files; Pro removes the limit. Files are concatenated in the order you add them — drag them in chronologically (January first, December last) for clean output ordering. Account numbers, balances, payee names never reach a server.
- Step 3Use strict mode for same-bank monthly exports — Same bank + same account = same schema across every month. Strict mode catches any schema drift immediately — useful when a bank adds a new column mid-year (rare but happens) or when you accidentally drop a different bank's export in by mistake. If strict rejects the merge, switch to union mode (next step) and investigate which file caused the mismatch.
- Step 4Use union mode for known schema drift across the year — If your bank added a new column (
Merchant Category Code,Currency Code, etc.) mid-year, you'll have N months with one schema and 12-N months with another. Union mode keeps all columns and fills missing values with empty cells. Add asource_monthcolumn to each input file before merging if you want to trace which rows came from which schema-era — particularly useful when the schema drift includes semantic changes (an existing column's meaning changed). - Step 5Add a `source_account` column for multi-account merges — Merging the same account across 12 months: no extra column needed. Merging multiple accounts from the same bank (checking + savings + credit card): add a
source_accountcolumn to each input before merging so you can filter or group by account downstream. Add the column with csv-column-merger or manually via a header rename step. - Step 6Optional: dedupe pending-vs-posted across month boundaries — Banks often duplicate the last 1–3 days of one month into the first 1–3 days of the next month while transactions transition from
PendingtoPostedstatus. Run csv-deduplicator on the merged output withcolumns: Date,Amount,Descriptionandstrategy: last(keeps the Posted version). Genuine same-day same-amount transactions (e.g. two £4.50 coffees) survive because they differ inTimeorTransaction IDif your bank's CSV includes those columns. For full bank-cleanup before QuickBooks import, fork the bank-statement cleanup workflow which chains this with currency-symbol stripping and bracket-negative conversion. - Step 7Use the merged file downstream — For analysis (Excel pivot tables, Python pandas, BigQuery): the merged CSV is ready as-is. For QuickBooks / Xero / FreeAgent re-import: per Intuit's documented limit, QuickBooks Online caps CSV uploads at 350 KB. An annual statement file often exceeds this — split with csv-row-splitter or fork the bank-statement cleanup workflow which handles format conversion + size management together.
Same-bank monthly merge scenarios
When pure header-matched merge is the right tool. Cross-bank scenarios require schema normalisation first — see the bank-statement cleanup workflow for those.
| Scenario | Source files | Schema stability | Recommended mode |
|---|---|---|---|
| Annual tax-season archive (same account) | 12 monthly CSVs from one account | 100% stable across same year | Strict |
| Multi-account annual roll-up (same bank) | Checking + savings + credit card monthly CSVs | Same bank usually means same schema across account types — but verify: some banks differentiate Checking Account.csv from Credit Card.csv | Strict if schemas match; union otherwise. Add source_account column |
| Year-over-year archive (same account, multi-year) | 5 years × 12 months = 60 monthly CSVs | Banks occasionally add columns over years — Merchant Category Code (2022+ for many), Currency Code (Brexit-era for UK banks) | Union mode + source_year column |
| Mid-year schema change | 8 monthly CSVs with old schema, 4 with new | Schema drift in the middle of the year | Union mode, audit empty cells in the new column for older rows |
| Bank-emailed multi-file ZIP | HSBC Business / Revolut Business email a ZIP with multiple CSVs for full-history exports | Same bank guarantees same schema | Strict |
| Cross-bank annual roll-up ⚠️ | Chase + Wells Fargo + HSBC + Monzo monthly CSVs | Different banks = different schemas — pure merger fails | Not single-tool — fork the cleanup workflow |
Strict vs union mode for bank CSVs
Same-bank monthly statements rarely have schema drift, but when they do union mode handles the rare mid-year column addition.
| Mode | Behaviour on mismatched headers | Use when | Risk |
|---|---|---|---|
| Strict | Rejects entire merge if any file has a different header set | Same bank + same account + same year usually means identical schema across all 12 months | None — strict surfaces unexpected schema drift immediately |
| Union (default) | Takes union of all headers; missing columns in any file get empty cells | Bank added a column mid-year (rare but documented for Merchant Category Code rollout in 2022-2023 for many UK + US banks) | Empty cells for older months could be misread as 'no value' instead of 'column didn't exist yet' — note dates in a separate audit column |
Documented downstream caps
What you can do with the merged annual file. These are the constraints from the accounting tools, not the merger — verified against each tool's own docs.
| Destination | Documented file-size cap | Mitigation |
|---|---|---|
| QuickBooks Online (CSV import) | 350 KB | Split with csv-row-splitter, or import quarter-by-quarter rather than annually |
| FreeAgent (CSV import) | 5 MB (documented in the FreeAgent docs) | Annual files usually fit; split if you exceed |
| Xero (CSV import) | No documented hard limit but practical ceiling ~10,000 rows per import | Split by quarter if exceeding |
| Excel-on-Windows (display) | 1,048,576 row hard cap | Most personal-banking annual files are 2k-50k rows; commercial accounts may exceed and need pandas / DuckDB / Power Query instead |
| Sage 50 (CSV import) | Not publicly documented | Test with progressively larger files |
| Plain analysis (Python / DuckDB / BigQuery) | No cap relevant to typical bank-CSV sizes | Use as-is |
Cookbook
Real banking scenarios with the corresponding merger configuration.
12 months of Chase checking → one annual file
ExampleSame account, same export type, same year. Strict mode catches any schema drift.
Inputs (in chronological order):
chase-checking-2025-01.csv (245 rows)
chase-checking-2025-02.csv (211 rows)
...
chase-checking-2025-12.csv (302 rows)
Config: mode: strict
Output: chase-checking-2025-annual.csv (~3,200 rows)
Same headers as each monthly file (typically:
Details, Posting Date, Description, Amount, Type, Balance,
Check or Slip #)
Rows concatenated in file order — January transactions first.Multi-account same-bank annual roll-up
ExampleChecking + savings + credit card monthly exports. Add source_account to each file before merging so downstream pivot tables can split by account.
Pre-merge step: add 'source_account' column to each file: chase-checking-2025-01.csv → adds Source_Account='Checking' chase-savings-2025-01.csv → adds Source_Account='Savings' chase-credit-2025-01.csv → adds Source_Account='Credit Card' ... × 12 months × 3 accounts = 36 files Merger config: mode: strict (if schemas all match) OR mode: union (if Credit Card has different columns) Result: 36-file merge produces one annual cross-account file with a 'Source_Account' column to filter / group by.
Mid-year column addition (union mode)
ExampleLloyds added Currency Code to UK personal-banking exports in mid-2025. Files from Jan-Jul have 8 columns; Aug-Dec have 9. Strict rejects; union keeps all 9 columns with empty cells in the new column for Jan-Jul rows.
Inputs: lloyds-2025-01.csv (8 columns, no Currency Code) ... lloyds-2025-07.csv (8 columns) lloyds-2025-08.csv (9 columns, with Currency Code = 'GBP') ... lloyds-2025-12.csv (9 columns) Strict mode: error — header mismatch Union mode: succeeds → 9-column output Jan-Jul rows: Currency Code cell is empty Aug-Dec rows: Currency Code cell = 'GBP' (or actual currency)
Pending-vs-posted dedup after merge
ExampleMonths 6 and 7 each have 2-3 rows that appear in both files because the transactions crossed the month-end boundary while still in Pending status. Optional follow-up dedup catches them.
Merger output (Jan-Dec concatenated): Date,Description,Amount,Status 2025-06-29,STARBUCKS,-4.50,Posted 2025-06-30,STARBUCKS,-4.50,Pending 2025-07-01,STARBUCKS,-4.50,Posted ← duplicate of 06-30 row! ... Follow-up: csv-deduplicator columns: Date,Description,Amount strategy: last (keeps the Posted version) Result: one row per actual transaction; pending/posted pairs collapsed.
HSBC Business multi-file ZIP for full-history export
ExampleHSBC Business emails a ZIP with multiple CSVs for full-history exports. All files share the same schema (HSBC personal-banking format). Strict mode handles them.
Inputs (extracted from HSBC's emailed .zip): full-history-part-1.csv (Jan-Mar) full-history-part-2.csv (Apr-Jun) full-history-part-3.csv (Jul-Sep) full-history-part-4.csv (Oct-Dec) Config: mode: strict (HSBC guarantees identical schemas across parts) Output: hsbc-business-2025-annual.csv
Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
QuickBooks Online 350 KB CSV import cap
Intuit documented limitQuickBooks Online caps CSV bank-import files at 350 KB. Annual statement files for active accounts often exceed this. Three mitigations: (1) split with csv-row-splitter into quarter-sized chunks before upload; (2) import quarter-by-quarter rather than annually; (3) fork the bank-statement cleanup workflow which chains the merge + cleanup + size management. The error wording when you hit the cap is documented as 'The uploaded file is too large.'
Same-day same-amount duplicate transactions are NOT pending/posted artefacts
Genuine duplicates — don't dedupeTwo £4.50 coffees on the same day at the same Starbucks produce two rows that look identical. csv-deduplicator with columns: Date,Amount,Description and strategy: last would collapse them, but the second coffee is a real second transaction. To distinguish: dedupe only on rows where the Status column transitions Pending → Posted (the actual pending/posted artefact pattern). For banks that include a Time or Transaction ID column (Monzo, Revolut, Starling, Wise), add those to the dedup columns to preserve genuine same-day same-amount transactions.
Cross-bank merging requires schema normalisation
Wrong tool — needs workflowChase's columns are different from Wells Fargo's, which are different from HSBC's, which are different from Monzo's. Pure-merger strict mode rejects; union mode produces a 30-column garbage file where the same logical data lives under different headers per bank. For cross-bank merging, fork [the bank-statement cleanup workflow](/workflows/clean-bank-statement-workflow) which chains csv-header-rename (per-bank rename map) + csv-cleaner + csv-merger + csv-deduplicator. The pure merger isn't the right tool.
Bank adds a column mid-year (Merchant Category Code, Currency Code)
Schema driftBanks occasionally add columns as features ship — Merchant Category Code rollout for many US banks in 2022-2023; Currency Code for UK banks post-Brexit-related multi-currency support. Strict mode rejects this merge; union mode keeps all columns. Add a source_month audit column to each input file before merging so future-you can interpret empty cells as 'column didn't exist yet' vs 'no value'.
HSBC summary block preamble breaks the merge
Bank-specific quirkHSBC UK CSV exports include a 5-10 row unstructured summary block (account holder name, IBAN, statement period) before the column headers. The merger reads the first row as the header — for an HSBC file, that's a meaningless 'Account holder: J D Smith' row. Strip the preamble with csv-cleaner (skipRows: 8 typically) before merging. The bank-statement cleanup workflow handles this as Step 1.
Wells Fargo has NO header row at all
Source omits headersWells Fargo personal-banking exports omit the header row entirely — the first row of the file is the first transaction. csv-merger can't detect this (it treats row 1 as the header). Manually prepend a header row (Date,Amount,*,*,Description) to each Wells Fargo file before merging, or use the bank-statement cleanup workflow which detects and handles the missing-header pattern.
Different bank-export delimiter (semicolon, tab)
Locale-specificSome EU bank exports use ; as field delimiter because , is the decimal separator. csv-merger expects all input files to share the same delimiter. If you mix a ,-delimited US export with a ;-delimited EU export, the merger reads one of them as a single-column file. Normalise the delimiter first by running each non-comma file through csv-cleaner with explicit delimiter override before merging.
Statement-month boundaries don't align with calendar months
User configurationSome bank statements run from the 15th to the 14th rather than calendar-month boundaries. Merging by 'monthly' file label doesn't guarantee calendar-month coverage — sort by Date within the merged file to confirm transaction-date order. For tax purposes, what matters is calendar-year inclusion, not file-name inclusion.
Frequently asked questions
When is a pure merge the right choice vs the cleanup workflow?
Pure merge when: (1) same bank + same account + same export type — schema stable across files; (2) goal is analysis / archive / reporting (not re-import to QuickBooks etc.); (3) you don't need to dedupe pending/posted overlaps. [Cleanup workflow](/workflows/clean-bank-statement-workflow) when: (1) cross-bank merging — different schemas need normalising first; (2) re-importing to QuickBooks / Xero / FreeAgent — needs format conversion + 350 KB size management; (3) need to strip HSBC summary blocks, day-of-week date suffixes, currency symbols, bracket-negative notation; (4) need pending/posted dedup as a default step.
How do I merge 12 months of Chase / Wells Fargo / HSBC into one annual file?
Export each month as CSV from your bank's Statements page. Drop all 12 onto the merger above in chronological order (January first, December last). Use strict mode — schema is stable across same-bank same-account exports. If strict rejects with a schema mismatch, your bank added a column mid-year — switch to union mode. For Wells Fargo specifically, prepend a header row manually to each file before merging (Wells Fargo CSVs omit headers — documented bank-specific quirk).
Does the merger remove duplicate transactions?
No — by design. The merger only concatenates. Genuine duplicate transactions are rare; pending-vs-posted overlaps are more common. For dedup after merge, run csv-deduplicator with columns: Date,Amount,Description and strategy: last. Be careful: this can collapse genuine same-day same-amount duplicates (two £4.50 coffees on the same day). For banks that include Time or Transaction ID columns (Monzo, Revolut, Starling, Wise), add those to the dedup columns.
Will the merged annual file fit into QuickBooks Online?
Probably not. QuickBooks Online caps CSV bank-import files at 350 KB (Intuit's documented limit). An annual statement file with monthly activity often exceeds this. Three options: (1) split with csv-row-splitter into quarterly chunks before upload; (2) import quarter-by-quarter directly; (3) use the bank-statement cleanup workflow which chains merge + cleanup + size management. Xero has no documented size cap; FreeAgent caps at 5 MB; Excel display works up to 1,048,576 rows.
Can I merge statements from different banks (Chase + Wells Fargo + HSBC)?
Not with pure csv-merger — different banks have different column schemas. Strict mode rejects; union mode produces an unusable file with the same logical data (Amount, Date, Description) appearing under 4-6 different column names. Fork [the bank-statement cleanup workflow](/workflows/clean-bank-statement-workflow) for cross-bank merges — it adds csv-header-rename + csv-cleaner steps that normalise schemas before merging.
Does it matter what order I drop the files in?
Yes for output row order. The merger concatenates in input order — first file's rows first, second file's rows next, etc. For chronological output, drop files in chronological order (2025-01.csv, 2025-02.csv, ..., 2025-12.csv). If you forget, the output can be re-sorted afterwards with csv-sorter on the Date column.
What about credit-card statements?
Same approach as checking/savings. Most credit-card CSV exports share their issuer's standard schema across months. Chase credit-card has 7 columns (Transaction Date, Post Date, Description, Category, Type, Amount, Memo); Chase checking has different columns. Merge within issuer + within account type — don't mix credit-card with checking unless you've used csv-header-rename to align schemas first.
Will my account numbers and balances be uploaded to JAD Apps?
No. PapaParse runs entirely in your browser. Account numbers, sort codes / routing numbers, balances, payee names, transaction descriptions never reach a server. The only thing saved server-side is a single counter (file merged, no content) for signed-in dashboard stats. You can opt out in account settings. This matters for financial PII handling — most banks' terms permit only the account-holder's own use, and SOX-compliant orgs need on-device handling to satisfy data-residency requirements.
Can I run this as part of an automated tax-prep pipeline?
Yes — pair the @jadapps/runner once; POST monthly CSVs to 127.0.0.1:9789/v1/tools/csv-merger/run. Common pipeline: monthly bank-statement-converter PDFs → CSV → cleaner (skip HSBC preamble) → merger (annual roll-up) → accountant handoff. Account data never reaches JAD's servers — everything runs on your local runner.
How large a merged file can the tool produce?
Per-step: Free tier 2 MB total input, Pro unlimited. The practical ceiling is your browser memory (~5–10M rows on desktop Chrome). For typical retail-banking accounts that's 50–100 years of monthly statements before hitting browser-memory limits. The downstream constraint is usually the destination tool — QuickBooks 350 KB, FreeAgent 5 MB, Excel 1,048,576 rows — not the merger itself.
What if my bank exports as OFX / QBO / QFX instead of CSV?
csv-merger only handles CSV. For OFX/QBO/QFX (Open Financial Exchange / QuickBooks / Quicken financial-data formats), convert to CSV first using one of the bank-statement-converter tools that handle the proprietary formats. Once you have CSV files for each month, the merger works as normal.
Should I add a 'source_month' or 'source_account' column before merging?
Yes for multi-account merging; optional for single-account year-roll-up. The merger doesn't add provenance — it just concatenates. If you'll need to filter or group the merged file by source (account, month, year), add the column to each input file first via csv-column-merger or a manual edit. For single-account same-year roll-up, the Date column already provides month-traceability without a separate column.
Privacy first
Processing runs locally in your browser with PapaParse. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.