When you download transactions from a bank "for QuickBooks", you get a .qbo file. I recently added a QBO export to BankStatementHub, a free tool that turns PDF bank statements into spreadsheets and accounting files. It turned out a QBO file is mostly something older and simpler, plus one extra line. There's also one field that decides whether your transactions quietly disappear.
Here's what I learned.
1. A QBO file is OFX with one extra line
QBO (QuickBooks Web Connect) is OFX 1.x, the bank-download format from the late '90s. It isn't XML. It's SGML: a plain-text header, then tags where the leaf elements are never closed.
A trimmed-down file looks like this:
OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:NONE
ENCODING:USASCII
CHARSET:1252
COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE
<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS><CODE>0<SEVERITY>INFO</STATUS>
<DTSERVER>20260930
<LANGUAGE>ENG
<INTU.BID>3000
</SONRS>
</SIGNONMSGSRSV1>
<BANKMSGSRSV1>
<STMTTRNRS>
<TRNUID>1
<STATUS><CODE>0<SEVERITY>INFO</STATUS>
<STMTRS>
<CURDEF>USD
<BANKACCTFROM>
<BANKID>000000000
<ACCTID>4821
<ACCTTYPE>CHECKING
</BANKACCTFROM>
<BANKTRANLIST>
<DTSTART>20260901
<DTEND>20260930
<STMTTRN>
<TRNTYPE>DEBIT
<DTPOSTED>20260903
<TRNAMT>-54.20
<FITID>20260903-9c1e4a7b-1
<NAME>Green Grocer
<MEMO>Card Purchase Green Grocer
</STMTTRN>
</BANKTRANLIST>
<LEDGERBAL><BALAMT>1765.80<DTASOF>20260930</LEDGERBAL>
</STMTRS>
</STMTTRNRS>
</BANKMSGSRSV1>
</OFX>
Notice <TRNAMT>-54.20 with no </TRNAMT>. That's valid here, and it's the first thing that breaks if you reach for an XML library.
The QuickBooks-specific part is <INTU.BID>, the Intuit bank ID. Without it you have an .ofx file. With it, and the .qbo extension, you have Web Connect.
2. INTU.BID: QuickBooks Desktop checks it
QuickBooks Online accepts .qbo, .ofx and .qfx uploads without much fuss. QuickBooks Desktop is stricter: it checks the bank ID against Intuit's list of supported banks, and if the number isn't on it, the import stops.
So when you generate QBO files yourself:
- use a bank ID that QuickBooks Desktop recognises (converters commonly use a well-known one)
- let the user change it, because the one that works can differ between setups
3. The FITID trap
Every transaction has a FITID. QuickBooks (and Quicken) remember the FITIDs they've imported for an account and silently skip any they've seen before. That's how they avoid duplicates when you download overlapping date ranges.
It also means the way you generate FITIDs decides whether transactions go missing. There are two easy ways to get it wrong.
Wrong #1: ID from the row's position. FITID = statementId + rowIndex looks unique, but convert an overlapping statement next month and the same transaction gets a different index. QuickBooks treats it as new, and you get duplicates.
Wrong #2: ID from the transaction's content only. FITID = hash(date + amount + description) is stable across conversions, which is good. But two genuine $4.50 coffees on the same day hash to the same ID, and QuickBooks drops the second one without a word.
What works: hash the content, then count repeats of that same content within the file.
const seen = new Map();
function fitid(t) {
const key = `${t.date}|${t.amount.toFixed(2)}|${t.description.trim().toLowerCase()}`;
const n = (seen.get(key) ?? 0) + 1; // 1st, 2nd... identical transaction
seen.set(key, n);
return `${t.date.replaceAll("-", "")}-${hash(key)}-${n}`;
}
Converting the same or an overlapping statement again gives the same IDs, so nothing duplicates. Two identical coffees get -1 and -2, so neither disappears.
If you already have a file with colliding IDs from a bank, the repeats need new IDs before import. I made a small free tool for exactly that: Fix duplicate FITIDs.
4. Credit cards have their own block
Bank accounts live in BANKMSGSRSV1 → STMTTRNRS → STMTRS → BANKACCTFROM. Credit cards use a parallel set: CREDITCARDMSGSRSV1 → CCSTMTTRNRS → CCSTMTRS → CCACCTFROM (with no BANKID or ACCTTYPE).
Write a card statement in the bank block and it gets treated as a bank account. For signs, TRNAMT follows the account holder: money out of a bank account is negative, and so is a purchase on a card. A payment to the card is positive.
5. Small things that cause import errors
-
Dates are
YYYYMMDD(optionally with a time), not ISO with dashes. -
Escape
&,<and>inNAMEandMEMO. A payee likeAT&Tbreaks an unescaped file. -
NAMEis short. OFX 1.x limits it to 32 characters. Put the full description inMEMO. -
CHARSET:1252is what most banks send. Stick to plain ASCII in payee names if you can. -
LEDGERBALcarries the closing balance. Include it when you know it.
6. Check the numbers before you write the file
QuickBooks imports whatever is in the file. If a row was misread or a page was skipped, the file looks fine and the problem shows up weeks later as a reconciliation that won't balance.
Before writing any QBO file I check that:
opening balance + every amount in the file = closing balance
and, where the statement prints one, that each row's running balance follows from the one before. If either fails, the user sees which row before downloading anything.
You can try the whole thing with a PDF statement here: Bank statement to QBO converter. Free, no signup. If you already have a bank CSV or QBO file and want to check it before importing, there's a bank CSV checker that reads QBO files too.
Have you run into other OFX or QBO quirks? I'd like to hear about them in the comments.
Top comments (0)