← Blog · June 22, 2026 · 9 min read
Hands-on test

How to check for statement duplicates: the same-file-three-times test

There is a three-minute test that tells you more about a finance app than any polished screenshot: take one closed bank statement, import it once, then import the same file again, and watch whether the total doubles. I ran it with the same CSV across Capi, YNAB and Monarch. The results were not the same, and the difference is exactly where your budget keeps or loses your trust. Here is the test, what happened in each app, and how to check your own month at home.

Re-uploading the same statement sounds like something no one would do on purpose, but it happens constantly. You import a file, you are not sure it worked, so you import it again. A bank connection drops and re-pulls a window it already loaded. You download May, then June, and the two overlap by a few days. Each of those is a re-upload test you never asked for, and an app that does not handle it turns one real purchase into two, three, or eight rows. The test below just does on purpose what daily life does by accident.

What is the re-upload statement test?

It is re-sending the same statement file to your finance app and checking whether the total changes. You import a closed statement once and note the total and the transaction count, then send the same file again with nothing edited. If the total doubles, the app inserts the repeat rows. If it stays the same, the app recognizes those transactions already exist and skips them. It is the fastest way to measure real deduplication rather than the promised kind.

I like this test because it does not depend on my word or yours. It is not a marketing claim, it is a number that either moves or does not move in front of you. Almost every bank lets you export a statement, so anyone can run it at home with their own data. And because it uses a real file, it exercises the messy path the app actually walks rather than the clean demo every tool can pass. I wrote the original version of this experiment in the bank statement re-upload test, and here I tighten it into a repeatable check.

Why do bank statements duplicate when you import them?

Because the input is repeated or overlapping, not because the spending happened twice. The same file sent again, a re-connection that re-pulls a window already loaded, a manual entry colliding with the automatic import, and installments that all land on the purchase date are the common sources. The transaction is real once. The import plumbing is what turns one into several, almost always without a warning.

There is a wrinkle that makes it worse during migrations. When an app shuts down and a wave of people move their history elsewhere, the way the Mint crowd did when it closed on March 23, 2024, the export often carries copies along with it. Aggregator re-syncs do the same after a connection drops and reauthorizes. The transaction was always real once, but the path it took into your new app decides whether it lands once or twice. I went deeper on the cross-app version of this in why budgeting apps duplicate transactions.

How do you export a statement for the test?

Download a finished month as CSV, OFX or PDF from your bank app or web portal, and keep the original file untouched. Use a closed statement, not the current one, because an open period still receives charges and would make the test unstable. Most banks and cards let you export a closed statement cleanly. With the file in hand, the test runs the same way regardless of which institution it came from.

Always use a closed month rather than the live one. An open statement changes from one day to the next, so the test wobbles and you cannot tell whether a difference came from the app or from a new charge that landed mid-test. A closed month is frozen: the total the bank prints is your reference truth, and anything above it inside the app is a duplicate. Save the original export, because you will re-send it on purpose in the next steps.

How do you run the re-upload test in three minutes?

Import the closed statement once and record the month total and the transaction count, then send the same file again, and again, without renaming or editing it. Compare the new total to your reference. If it doubled or tripled, the app duplicates. If it matches, the app matched the repeat rows and skipped them. Finally, reconcile against the amount printed on the statement to close the loop.

  1. Export one closed statement as CSV, OFX or PDF and keep the original file.
  2. Import it once, confirm, and write down the month total and the transaction count.
  3. Send the same file a second time, and a third if you like, changing nothing.
  4. Compare the new total to the first import. Sort by amount so identical pairs sit together.
  5. Reconcile the app total against the printed statement for the same month. They must agree.

What happens when you re-upload the same statement to Capi, YNAB and Monarch?

Each app treats the re-upload differently. Capi fingerprints each row with a hash and skips at commit, so the total does not move when you re-send. YNAB shows matched imports and makes you approve, so a human is the final check. Monarch leans harder on automatic matching between pending and posted, smooth when the connection is healthy and fragile when it is not. Here is the honest map of where copies come from and how each one tries to block them.

App Common duplicate source Re-upload the same file Who checks at the end Price (2026)
Capi Overlapping statements on upload Skips by row hash The app, at commit Free 30/mo; $9.90/mo or $69.90/yr
YNAB Manual entry plus import Matches, you approve You, on approval $14.99/mo or $109/yr
Monarch Re-sync after reauth Matches on sync You, in review $99.99/yr (Plus $199/yr)
Mint to Credit Karma Migration carryover Often manual You, by hand Mint shut down in 2024

The cleanest contrast is between Capi and YNAB, and both designs are defensible. YNAB shows the matched imports and makes you approve them, so the human is the last check. I lay out where the two differ in Capi vs YNAB. Among the Telegram trackers, the comparison with Moneko shows how capture changes when everything lives in chat. None of these approaches is wrong. They just put the safety check in different places, and the re-upload test reveals where each one put it.

The short version. Re-uploading the same closed statement is the fastest honesty test for a finance app. Import once, note the total, re-send the same file, and watch whether the number doubles. In Capi the total does not move, because each row is hashed and the skip runs at commit. In apps that lean on sync, check again after a re-connection. Always close the loop against the amount printed on the statement.

How does Capi avoid duplicates when you re-upload a statement?

Capi fingerprints each source row with a hash and compares new rows against what is already in your ledger, so re-sending the same CSV or PDF imports only what is genuinely new. During an internal audit I found that the skip step flagged duplicates in the preview but never saved the flag, so the commit re-read the unflagged data and inserted everything. I fixed it, so the skip now runs at commit, not just on screen, and the reader spreads installments across the months they belong to.

Let me be specific about where Capi still needs you, because an honest promise has edges. The row hash protects file and statement imports well, but a duplicate you type by hand that matches nothing in a file is yours to catch, the same as in any app. Photo and voice capture is fast, but it is worth a glance before you confirm, since a vision model can still misread a faded receipt. And no app removes the need to reconcile with the bank now and then. I told the full story of how I found all of this by using my own app in the 25 duplicate transactions essay.

How do you remove the duplicates that already got in?

Match each duplicate by amount, date and merchant, then delete one copy and keep the other, never both. Keep the version that carries the right category and any note, and remove the uncategorized one. If the duplicate came from a repeat import, fix the source so the next import does not repeat. After each batch, reconcile the corrected total against the statement amount before you trust the number again.

Cleaning by hand is tedious, but with the right sort it goes fast. Sort by amount instead of date, because two identical amounts sit next to each other while the date view scatters them across the month. Stacked installments you delete and re-import once the reader spreads them properly. Rows dated outside the month, from a photo that misread the year, you move or delete. The better an app prevents this on the way in, the fewer times you redo the cleanup. If you want the bigger picture of how a statement becomes a budget, the statement-to-budget guide covers the whole flow.


Run the re-upload test on your own month.

Send Capi a closed statement, then send the same file again. The total will not double, because each row is fingerprinted and the skip runs at commit.
Capi Free covers 30 transactions a month. Capi Core is $9.90 a month or $69.90 a year.

Try Capi free on Telegram →

Frequently asked questions about statement duplicates

Why does my statement show duplicates after importing?

Almost always because the same file went in more than once, or an automatic connection re-pulled a window that was already loaded. A manual entry colliding with the import, a pending charge that later posts, and installments that all land on the purchase date also create copies. The app total reads higher than the printed statement.

Can I export my bank statement as CSV, OFX or PDF?

Most banks and cards let you export a closed statement as CSV, OFX or PDF from the app or web portal. Only finished statements export cleanly, because an open one still receives charges. Use a closed month so the printed total is fixed and gives you a stable reference to test against.

Does re-uploading the same statement to Capi duplicate my spending?

No. Capi fingerprints each source row with a hash and compares new rows against what already exists in your ledger, so re-sending the same CSV or PDF imports only genuinely new rows. After an internal audit I fixed the step that flagged duplicates but never saved the flag, so the skip now runs at commit, not just in the preview.

Do budgeting apps duplicate transactions on import?

Many do under the right conditions, because the input is messy. Overlapping statements, re-connections, pending-versus-posted pairs and manual entries colliding with imports all create copies. The difference is whether the app catches it. Some match on import and ask you to approve, some leave it to you, and some skip silently. None are immune by default.

How do I remove duplicate transactions safely?

Match each duplicate by amount, date and merchant, then delete one copy and keep the other, never both. Keep the version that carries the right category and any note. If the duplicate came from a repeat import, fix the source so the next import does not repeat. Reconcile the corrected total against the bank statement before trusting it again.

How many times should I re-upload the file in the test?

Two shows the behavior, three makes it obvious. Import once and record the total, then re-send the same file with nothing changed. If the total doubles on the second pass and triples on the third, the app inserts everything again. If it matches the first import, the app recognizes the repeat rows and skips them.

Written by Daniil Kozin, founder of Capi. More in this series: The best money tracker in 2026 · The bank statement re-upload test · Why budgeting apps duplicate transactions · Statement PDF to budget · Capi vs YNAB.