You do not need to write OFX, but being able to open one and see what it says turns a failed import from a mystery into a two-minute check.
What OFX is for
Open Financial Exchange is a specification for exchanging financial data between institutions and software. CheckFree, Intuit and Microsoft published the first version in 1997, and the specification is now looked after by the Financial Data Exchange consortium, whose current banking specification is version 2.3 from 2020. In practice, though, most statement files in circulation are still written in the 1.0.2 dialect from the late nineties, because that is what desktop accounting software expects.
Its advantage over CSV is that it is self-describing. A CSV date is ambiguous and its amount column may or may not be signed; an OFX date is always written year first and its amounts are signed by definition. There is no mapping step because there is nothing to map. Xero, Sage, MYOB, Wave, Zoho Books and QuickBooks Online all read it, and QBO and QFX are OFX with one Intuit tag added.
Where a CSV import has fought you twice over column mapping, an OFX file with unambiguous dates and signed amounts removes the mapping step entirely because the format describes itself.
The two versions, and how to tell them apart
Version 1.x is SGML: a header block of colon-separated lines, then tags, where elements that hold a value are usually not closed. Version 2.x is XML: it begins with an XML declaration and an OFX processing instruction, and every tag is closed. Both are legitimate OFX, and a program that reads one may reject the other. QuickBooks Desktop in particular expects 1.0.2.
OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:NONE
ENCODING:USASCII
CHARSET:1252
COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE
<TRNAMT>-45.20 no closing tag needed<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?OFX OFXHEADER="200" VERSION="220" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>
<TRNAMT>-45.20</TRNAMT> closing tag requiredA quick tell
If a file opens in a browser as a tidy expandable tree, it is OFX 2.x. That is a warning sign rather than a good one where the target software expects the older form.
Reading one
After the header, a statement file has three layers: a sign-on response saying who produced it, a statement response for one account, and inside that the transaction list and balances. Bank accounts use BANKMSGSRSV1 and STMTRS; credit cards use CREDITCARDMSGSRSV1 and CCSTMTRS, with a CCACCTFROM block that has an account number but no routing number. The parts worth knowing are few:
| Tag | Holds | Notes |
|---|---|---|
| <CURDEF> | Default currency | ISO code such as USD, GBP, AUD |
| <BANKID> | Routing number or sort code | Bank accounts only |
| <ACCTID> | Account number | How the importer recognises the account next time |
| <ACCTTYPE> | CHECKING, SAVINGS, MONEYMRKT, CREDITLINE, CD | Card statements use CCACCTFROM instead |
| <DTSTART> / <DTEND> | Statement period | Some importers reject an end date in the future |
| <LEDGERBAL> | Closing balance and its date | The figure the transactions should reach |
| <AVAILBAL> | Available balance | Not the same figure; ignore it for checking |
| Tag | Holds | Notes |
|---|---|---|
| <TRNTYPE> | Transaction type | DEBIT, CREDIT, CHECK, FEE, INT and others |
| <DTPOSTED> | Date posted | YYYYMMDD, optionally with time and zone |
| <DTUSER> | Date initiated | Optional, e.g. the card swipe date |
| <TRNAMT> | Amount | Signed: negative is money out |
| <FITID> | Transaction ID | Unique within the account; the duplicate key |
| <CHECKNUM> | Cheque number | Only for cheques |
| <REFNUM> | Reference number | Optional |
| <NAME> | Payee or description | Short: 32 characters in the specification |
| <MEMO> | Longer description | Up to 255 characters; where full text survives |
Most import failures are visible here. A missing ACCTID, a period in the future, an ACCTTYPE that disagrees with the account you are importing into, a card statement written as a checking account: all findable in under a minute with a text editor.
Dates and amounts, precisely
OFX dates are written year, month, day, optionally followed by hours, minutes, seconds, milliseconds and a time zone in square brackets. All of these are valid and mean the same day:
20260315
20260315120000
20260315120000.000[-5:EST]The time zone is the one place dates can still go wrong. A transaction stamped at midnight UTC can appear a day earlier in software that converts it to a US time zone, so careful writers either omit the time or set it to midday. Amounts use a full stop as the decimal separator and no thousands separator, whatever the country, and the sign carries the direction.
Why it needs no bank ID
OFX carries a BANKID, a routing or sort code, which identifies the bank in the ordinary sense, and an FI block naming the institution. What it does not carry is Intuit’s INTU.BID, which exists to confirm the institution participates in Web Connect. That tag is a commercial check, not a technical one, which is why plain OFX imports into non-Intuit software without one.
In practice this makes OFX the best default for anything that is not QuickBooks or Quicken: same data, no number to hunt for. QuickBooks Online also accepts plain OFX through Upload from file, which is useful when your bank has no Intuit ID.
The transaction types, and what they change
TRNTYPE classifies each transaction, and importers use it to suggest a posting or to filter. The specification lists about seventeen values; the ones you will actually meet are few.
- DEBIT and CREDIT: the generic pair, and what most converters write when the statement does not say more.
- CHECK: a cheque, usually with a CHECKNUM alongside it.
- FEE, SRVCHG and INT: bank charges and interest, which some importers route to their own accounts automatically.
- DEP, DIRECTDEP and DIRECTDEBIT: deposits and direct debits.
- XFER: a transfer between accounts, worth distinguishing because posting one as income overstates revenue.
- ATM, POS, PAYMENT, CASH and OTHER: useful for filtering rather than for posting.
The sign in TRNAMT is authoritative regardless. A DEBIT with a positive amount is a contradiction some software resolves one way and some the other, so the type should never be relied on to carry direction.
The status block, and what a rejection looks like
Near the top of every file sits a sign-on response with a status code inside it. Zero means success. Anything else is the bank or the file telling you why it will not proceed, and it is worth reading before assuming the transactions are the problem.
<STATUS>
<CODE>0
<SEVERITY>INFO
</STATUS>Statement files produced by converters generally set this to zero as a formality. Files downloaded from a bank sometimes carry a real error here, which is the actual cause of an import that appeared to fail for no reason.
Balances, and why they matter for checking
A statement block carries LEDGERBAL, the closing balance, and often AVAILBAL, the available balance. These are the closest thing OFX has to a self-check: opening balance plus the sum of the transactions should reach the ledger balance. OFX has no opening balance field, so the opening figure comes from your books or the statement.
Available balance is not the same figure and should never be used for that arithmetic. It reflects pending items and any overdraft facility, so it will not agree and is not supposed to.
Common mistakes in hand-built OFX
- Unescaped characters. An ampersand in a payee name, as in M&S, has to be written &, and a raw one breaks the parse, often with an error that points nowhere useful.
- The wrong character set. A 1.0.2 file declaring CHARSET:1252 but saved as UTF-8 turns accented names into stray symbols.
- Mixing dialects: closing tags in a file whose header says SGML, or an XML header in front of SGML-style tags. Some readers tolerate it; QuickBooks often does not.
- Writing a credit card as a bank account. Card statements belong in CCSTMTRS, or the importer offers the wrong accounts.
- Opening an OFX in Excel to check it. Excel can read the XML version as a table, but the SGML version arrives as a single column of tags. Use a text editor.