<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Satwinder Dev</title>
    <description>The latest articles on DEV Community by Satwinder Dev (@satwinder-codes).</description>
    <link>https://dev.to/satwinder-codes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4204610%2F8c35d158-4d21-4a9a-b21b-8070006f0cfd.png</url>
      <title>DEV Community: Satwinder Dev</title>
      <link>https://dev.to/satwinder-codes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/satwinder-codes"/>
    <language>en</language>
    <item>
      <title>What I learned building a bank statement PDF to Excel converter</title>
      <dc:creator>Satwinder Dev</dc:creator>
      <pubDate>Sat, 24 Oct 2026 12:33:24 +0000</pubDate>
      <link>https://dev.to/satwinder-codes/what-i-learned-building-a-bank-statement-pdf-to-excel-converter-4lef</link>
      <guid>https://dev.to/satwinder-codes/what-i-learned-building-a-bank-statement-pdf-to-excel-converter-4lef</guid>
      <description>&lt;p&gt;A PDF bank statement looks like a table. It isn't one. There are no rows or columns inside the file, only pieces of text placed at x/y positions on a page. Everything that makes it a "table" is in your head.&lt;/p&gt;

&lt;p&gt;I spent the last few months building &lt;a href="https://bankstatementhub.com" rel="noopener noreferrer"&gt;BankStatementHub&lt;/a&gt;, a free tool that turns PDF bank and credit card statements into Excel, CSV and QuickBooks files. Here is what broke along the way, and the one check that caught most of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Columns are a guess
&lt;/h2&gt;

&lt;p&gt;To rebuild rows you group text by its vertical position, then split it into columns by horizontal position. That works until:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a description is long enough to run into the amount column&lt;/li&gt;
&lt;li&gt;one transaction's description wraps onto two or three lines&lt;/li&gt;
&lt;li&gt;the page header (bank name, account number, "continued") repeats on every page and lands in the middle of your data&lt;/li&gt;
&lt;li&gt;a "Balance forward" or "Subtotal" line looks exactly like a transaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every bank does this a little differently, and the same bank changes its layout every few years.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The sign of an amount lives in different places
&lt;/h2&gt;

&lt;p&gt;"Is this money in or money out?" has no single answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate &lt;strong&gt;Withdrawals&lt;/strong&gt; and &lt;strong&gt;Deposits&lt;/strong&gt; columns (most US checking accounts)&lt;/li&gt;
&lt;li&gt;one &lt;strong&gt;Amount&lt;/strong&gt; column with a minus sign&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;CR&lt;/code&gt; or &lt;code&gt;DR&lt;/code&gt; suffix, or a trailing minus like &lt;code&gt;45.00-&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;credit card statements, where a positive number is a charge and a payment is negative, the opposite of a checking account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get this wrong and the totals still look plausible, which is the dangerous part.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Numbers and dates are not what you expect
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;1,234.56&lt;/code&gt; in the US, &lt;code&gt;1.234,56&lt;/code&gt; in much of Europe and Latin America&lt;/li&gt;
&lt;li&gt;amounts with no leading zero, like &lt;code&gt;.15&lt;/code&gt;, which a careless regex reads as &lt;code&gt;15&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;dates printed without a year (&lt;code&gt;Dec 28&lt;/code&gt;, &lt;code&gt;Jan 03&lt;/code&gt;), so a statement that runs from December into January needs the year worked out from the statement period&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. The check that saved me: the balance
&lt;/h2&gt;

&lt;p&gt;The most useful thing I built was not a better parser. It was a check.&lt;/p&gt;

&lt;p&gt;A statement gives you an opening balance, a closing balance, and usually a running balance on every row. So after reading the transactions you can test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;opening balance + money in − money out = closing balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and, row by row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;previous row balance ± this amount = this row balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If either fails, something was misread, dropped or double counted, and you know which row to look at.&lt;/p&gt;

&lt;p&gt;A real example: on a 14-page BBVA México statement, one whole page of transactions was skipped. Every row that &lt;em&gt;was&lt;/em&gt; read looked perfect. The only sign of trouble was that the totals didn't add up to the closing balance, and that check flagged it.&lt;/p&gt;

&lt;p&gt;Credit card statements usually have no running balance, so there the check falls back to the statement totals (previous balance, payments, purchases, new balance). It's weaker, but it still catches a missing page.&lt;/p&gt;

&lt;p&gt;The lesson I'd pass on: &lt;strong&gt;don't trust extracted data you can't reconcile.&lt;/strong&gt; If the numbers can't be checked against something on the page, show the user that clearly instead of handing them a clean-looking spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Export formats have their own traps
&lt;/h2&gt;

&lt;p&gt;CSV and Excel are easy. QuickBooks and Quicken files (QBO/OFX) are not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each transaction needs a &lt;code&gt;FITID&lt;/code&gt;, an ID that should be unique within the account&lt;/li&gt;
&lt;li&gt;QuickBooks silently skips a transaction if it has already seen that FITID, so two genuine $4.50 coffees on the same day can turn into one if the IDs collide&lt;/li&gt;
&lt;li&gt;the account type (checking vs credit card) changes how the signs are read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran into the duplicate FITID problem often enough that I made a small free tool just for that: &lt;a href="https://bankstatementhub.com/fix-duplicate-fitid" rel="noopener noreferrer"&gt;Fix duplicate FITIDs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PDF to Excel, CSV, QuickBooks (QBO), Xero and Quicken (OFX)&lt;/li&gt;
&lt;li&gt;every row checked against the balance, with mismatches flagged&lt;/li&gt;
&lt;li&gt;bulk conversion of up to 12 statements at once&lt;/li&gt;
&lt;li&gt;password-protected PDFs&lt;/li&gt;
&lt;li&gt;free, no signup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've worked with bank statement PDFs, I'd like to hear about the strangest layout you've come across. And if you try &lt;a href="https://bankstatementhub.com" rel="noopener noreferrer"&gt;BankStatementHub&lt;/a&gt; on a statement it gets wrong, tell me in the comments; that's how most of these fixes happened.&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>bankstatementconverter</category>
      <category>satwinder</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
