Read an OFX File

This example shows how to read a bank statement in Open Financial Exchange format with OpenFinancialExchangeReader. The reader accepts the OFX files banks offer for download as well as their QFX (Quicken) and QBO (QuickBooks) variants and returns the whole file as a single nested Record that mirrors the document's aggregate hierarchy, using the OFX tag names (BANKMSGSRSV1, STMTTRN, TRNAMT, ...) as field names.

Child aggregates become nested records, repeating aggregates such as the transactions become arrays, and elements that are absent or empty are left out. Values keep their types: dates become DATETIME fields and amounts BIG_DECIMAL fields. To flatten the transactions into one row each, see Convert OFX Transactions to CSV.

Input OFX file

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?OFX OFXHEADER="200" VERSION="211" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>
<OFX>
  <SIGNONMSGSRSV1>
    <SONRS>
      <STATUS>
        <CODE>0</CODE>
        <SEVERITY>INFO</SEVERITY>
      </STATUS>
      <DTSERVER>20260131120000.000[0:GMT]</DTSERVER>
      <LANGUAGE>ENG</LANGUAGE>
      <FI>
        <ORG>Sample Bank</ORG>
        <FID>12345</FID>
      </FI>
    </SONRS>
  </SIGNONMSGSRSV1>
  <BANKMSGSRSV1>
    <STMTTRNRS>
      <TRNUID>1</TRNUID>
      <STATUS>
        <CODE>0</CODE>
        <SEVERITY>INFO</SEVERITY>
      </STATUS>
      <STMTRS>
        <CURDEF>USD</CURDEF>
        <BANKACCTFROM>
          <BANKID>111000025</BANKID>
          <ACCTID>000123456789</ACCTID>
          <ACCTTYPE>CHECKING</ACCTTYPE>
        </BANKACCTFROM>
        <BANKTRANLIST>
          <DTSTART>20260101000000.000[0:GMT]</DTSTART>
          <DTEND>20260131000000.000[0:GMT]</DTEND>
          <STMTTRN>
            <TRNTYPE>CREDIT</TRNTYPE>
            <DTPOSTED>20260105120000.000[0:GMT]</DTPOSTED>
            <TRNAMT>2500.00</TRNAMT>
            <FITID>2026010500001</FITID>
            <NAME>PAYROLL DEPOSIT</NAME>
          </STMTTRN>
          <STMTTRN>
            <TRNTYPE>DEBIT</TRNTYPE>
            <DTPOSTED>20260112120000.000[0:GMT]</DTPOSTED>
            <TRNAMT>-86.42</TRNAMT>
            <FITID>2026011200002</FITID>
            <NAME>GROCERY MART</NAME>
            <MEMO>Card purchase</MEMO>
          </STMTTRN>
          <STMTTRN>
            <TRNTYPE>CHECK</TRNTYPE>
            <DTPOSTED>20260120120000.000[0:GMT]</DTPOSTED>
            <TRNAMT>-1200.00</TRNAMT>
            <FITID>2026012000003</FITID>
            <CHECKNUM>1042</CHECKNUM>
            <NAME>RENT</NAME>
          </STMTTRN>
        </BANKTRANLIST>
        <LEDGERBAL>
          <BALAMT>4213.58</BALAMT>
          <DTASOF>20260131000000.000[0:GMT]</DTASOF>
        </LEDGERBAL>
      </STMTRS>
    </STMTTRNRS>
  </BANKMSGSRSV1>
</OFX>

Java Code Listing

package com.northconcepts.datapipeline.examples.openfinancialexchange;

import java.io.File;

import com.northconcepts.datapipeline.core.DataReader;
import com.northconcepts.datapipeline.core.DataWriter;
import com.northconcepts.datapipeline.core.StreamWriter;
import com.northconcepts.datapipeline.job.Job;
import com.northconcepts.datapipeline.openfinancialexchange.OpenFinancialExchangeReader;

public class ReadAnOfxFile {

    public static void main(String[] args) {
        DataReader reader = new OpenFinancialExchangeReader(new File("example/data/input/bank-statement.ofx"));

        DataWriter writer = StreamWriter.newSystemOutWriter();

        Job.run(reader, writer);
    }

}

Code Walkthrough

  1. An OpenFinancialExchangeReader is created for example/data/input/bank-statement.ofx, a sample checking account statement with three transactions.
  2. A StreamWriter is created to print records to the console.
  3. Job.run() transfers the one record the file produces. In the output, SIGNONMSGSRSV1 carries the sign-on status and the financial institution, BANKMSGSRSV1.STMTTRNRS is an array of statement responses, and inside the first one STMTRS holds the account (BANKACCTFROM), the transaction list (BANKTRANLIST.STMTTRN, an array of three records) and the ledger balance (LEDGERBAL).

Console Output

Dates are shown in the time zone of the machine running the example.

-----------------------------------------------
0 - Record (MODIFIED) (has child records) {
    0:[SECURITY]:STRING=[NONE]:String
    1:[NEWFILEUID]:STRING=[NONE]:String
    2:[SIGNONMSGSRSV1]:RECORD=[
        Record (MODIFIED) (is child record) (has child records) {
            0:[SONRS]:RECORD=[
                Record (MODIFIED) (is child record) (has child records) {
                    0:[STATUS]:RECORD=[
                        Record (MODIFIED) (is child record) {
                            0:[CODE]:INT=[0]:Integer
                            1:[SEVERITY]:STRING=[INFO]:String
                        }]:Record
                    1:[DTSERVER]:DATETIME=[Sat Jan 31 07:00:00 EST 2026]:Date
                    2:[LANGUAGE]:STRING=[ENG]:String
                    3:[FI]:RECORD=[
                        Record (MODIFIED) (is child record) {
                            0:[ORG]:STRING=[Sample Bank]:String
                            1:[FID]:STRING=[12345]:String
                        }]:Record
                }]:Record
        }]:Record
    3:[BANKMSGSRSV1]:RECORD=[
        Record (MODIFIED) (is child record) (has child records) {
            0:[STMTTRNRS]:ARRAY of RECORD=[[
                Record (MODIFIED) (is child record) (has child records) {
                    0:[TRNUID]:STRING=[1]:String
                    1:[STATUS]:RECORD=[
                        Record (MODIFIED) (is child record) {
                            0:[CODE]:INT=[0]:Integer
                            1:[SEVERITY]:STRING=[INFO]:String
                        }]:Record
                    2:[STMTRS]:RECORD=[
                        Record (MODIFIED) (is child record) (has child records) {
                            0:[CURDEF]:STRING=[USD]:String
                            1:[BANKACCTFROM]:RECORD=[
                                Record (MODIFIED) (is child record) {
                                    0:[BANKID]:STRING=[111000025]:String
                                    1:[ACCTID]:STRING=[000123456789]:String
                                    2:[ACCTTYPE]:STRING=[CHECKING]:String
                                }]:Record
                            2:[BANKTRANLIST]:RECORD=[
                                Record (MODIFIED) (is child record) (has child records) {
                                    0:[DTSTART]:DATETIME=[Wed Dec 31 19:00:00 EST 2025]:Date
                                    1:[DTEND]:DATETIME=[Fri Jan 30 19:00:00 EST 2026]:Date
                                    2:[STMTTRN]:ARRAY of RECORD=[[
                                        Record (MODIFIED) (is child record) {
                                            0:[TRNTYPE]:STRING=[CREDIT]:String
                                            1:[DTPOSTED]:DATETIME=[Mon Jan 05 07:00:00 EST 2026]:Date
                                            2:[TRNAMT]:BIG_DECIMAL=[2500]:BigDecimal
                                            3:[FITID]:STRING=[2026010500001]:String
                                            4:[NAME]:STRING=[PAYROLL DEPOSIT]:String
                                        }, 
                                        Record (MODIFIED) (is child record) {
                                            0:[TRNTYPE]:STRING=[DEBIT]:String
                                            1:[DTPOSTED]:DATETIME=[Mon Jan 12 07:00:00 EST 2026]:Date
                                            2:[TRNAMT]:BIG_DECIMAL=[-86.42]:BigDecimal
                                            3:[FITID]:STRING=[2026011200002]:String
                                            4:[NAME]:STRING=[GROCERY MART]:String
                                            5:[MEMO]:STRING=[Card purchase]:String
                                        }, 
                                        Record (MODIFIED) (is child record) {
                                            0:[TRNTYPE]:STRING=[CHECK]:String
                                            1:[DTPOSTED]:DATETIME=[Tue Jan 20 07:00:00 EST 2026]:Date
                                            2:[TRNAMT]:BIG_DECIMAL=[-1200]:BigDecimal
                                            3:[FITID]:STRING=[2026012000003]:String
                                            4:[CHECKNUM]:STRING=[1042]:String
                                            5:[NAME]:STRING=[RENT]:String
                                        }]]:ArrayValue
                                }]:Record
                            3:[LEDGERBAL]:RECORD=[
                                Record (MODIFIED) (is child record) {
                                    0:[BALAMT]:BIG_DECIMAL=[4213.58]:BigDecimal
                                    1:[DTASOF]:DATETIME=[Fri Jan 30 19:00:00 EST 2026]:Date
                                }]:Record
                        }]:Record
                }]]:ArrayValue
        }]:Record
}

-----------------------------------------------
1 records
Mobile Analytics