OFX looks like XML. It isn't, and QuickBooks will reject it
We built Luventra, a converter that turns a bank's CSV export into OFX, QBO or QFX entirely in the browser. Parsing the CSV was the expected hard part. The surprise was how much of the work was the output format. These a
We built Luventra, a converter that turns a bank's CSV export into OFX, QBO or QFX entirely in the browser. Parsing the CSV was the expected hard part. The surprise was how much of the work was the output format. These are the rules that cost the most time.
1. Leaf elements have no closing tag
OFX 1.x is SGML, not XML. A leaf element is written open-only:
<TRNAMT>-42.10
<FITID>ABC123
Only aggregates, the elements that contain other elements, get a closing tag. Emit well-formed XML and Quicken and QuickBooks reject the file.
So the writer makes the mistake impossible instead of relying on anyone to remember. It has open() and close() for aggregates and a leaf() that physically cannot produce a close tag.
2. A plain-text header comes first
Before the first <OFX> there is a KEY:VALUE block, ended by one blank line:
OFXHEADER:100
DATA:OFXSGML
VERSION:<1.x version>
SECURITY:NONE
ENCODING:USASCII
CHARSET:1252
COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:<derived, see below>
Line endings are CRLF everywhere, including inside that header and after the final closing tag.
3. The body has to be ASCII
The header declares ENCODING:USASCII, so the body must match. Accented characters are decomposed and stripped of their combining marks (é becomes e), anything still non-ASCII becomes ?, control characters are dropped, and & < > are entity-escaped. Whitespace is collapsed so a value never spans a line.
Field length caps from the OFX 1.0.2 spec are enforced too, and if a truncation lands in the middle of an entity, the entity is repaired rather than left half-written.
4. Same input, same bytes
The output is reproducible. Nothing reads the clock, the locale or Math.random, so a test can compare the result byte for byte.
Two decisions follow from that:
-
TRNUIDis fixed rather than random. Consumers only use it to correlate a request they never sent, so reproducibility beats uniqueness. -
NEWFILEUIDis derived from the content by hashing. The header value is capped at 36 characters, and splicing two transaction ids together would truncate mid-token, which could let two statements that share a first transaction claim the same file id. A hash stays stable per statement and distinct across statements.
The hash is SHA-1, used purely as a content fingerprint and not for anything security related. We wrote a small synchronous implementation because crypto.subtle.digest returns a Promise, and that would have forced the whole emitter to be async for what is a pure derivation.
5. QBO and QFX need an Intuit extension
For .qbo and .qfx the sign-on block needs an INTU.BID element. QuickBooks refuses a QBO without it, and Quicken uses it to route a QFX to a subscribed institution. Plain OFX does not need it.
All of this runs in a Web Worker in the browser, so the statement is never uploaded. It is free without an account: two files a day, 50 transactions per file, at luventra.co. It is our own product, so weigh that accordingly.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.