DEV Community

Cover image for What's inside a QBO file (and why QuickBooks silently skips transactions)
Satwinder Dev
Satwinder Dev

Posted on

What's inside a QBO file (and why QuickBooks silently skips transactions)

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>
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

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 > in NAME and MEMO. A payee like AT&T breaks an unescaped file.
  • NAME is short. OFX 1.x limits it to 32 characters. Put the full description in MEMO.
  • CHARSET:1252 is what most banks send. Stick to plain ASCII in payee names if you can.
  • LEDGERBAL carries 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
Enter fullscreen mode Exit fullscreen mode

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)