Note

BAI2 File Format: Records, Type Codes and How to Read One

Every BAI2 record explained: the 01–99 hierarchy, balance codes 010/015/040/045, credit vs debit type-code ranges, the funds-type field and control totals.

·Published ·14 min read·#treasury#bank-connectivity#integration

Connectivity 7 of 15 see the reading order →

Reviewed by Tan Gravam Fact-checked

On this page

A BAI2 file is a comma-delimited flat file of numbered records nested three envelopes deep: an 01 file header, one or more 02 group headers, an 03 account record carrying balances as three-digit type codes, 16 records for transaction detail, 88 continuations wherever a record runs long, and 49/98/99 trailers that close each envelope with a control total. If you're choosing between statement formats, that's the MT940 vs camt.053 vs BAI2 comparison; if your file is tagged SWIFT text or XML, you want the MT940 tag walkthrough or the camt.053 field walkthrough. This post is the third of the trio: for the day a .bai file from a US bank is in front of you and you need to know what each record and code holds — verified against the BAI specification itself and two banks' published implementation guides (HSBC's and SouthState Bank's), because as always the bank guide, not the standard, is the contract.

One naming point first, because the search results are a mess. "BAI2" is not a second file format that replaced a first one called BAI — it is Version 2 of BAI's Cash Management Balance Reporting Specifications, and the version-number field in every file header literally says 2. The lineage since: BAI handed the copyright to Accredited Standards Committee X9 in 2008, ANSI approved the successor standard BTRS in 2012, and X9 versioned it straight to "BTR3" in 2016 precisely so the numbering would read as a continuation of BAI2. BTRS readers were designed to accept BAI2 files (with exceptions where codes were deleted or repurposed); a BAI2 reader cannot accept a BTRS file. In the field, the guides your banks publish still say BAI2, which is why this post does too.

The shape of the file

The specification organizes data into three envelopes — account inside group inside file. A group holds accounts from one originator institution with one as-of-date; a file can hold many groups, which is how a concentration bank forwards other banks' data to you in a single transmission. That's a structural difference from MT940, where every message is one account's statement: a BAI2 importer must be written for the multi-account, multi-group, multi-originator case on day one.

The syntax is three rules:

  • Fields are delimited by commas.
  • A slash / ends a logical record (with a text-field exception we'll get to — it's the format's nastiest corner).
  • Defaulted optional fields are shown by adjacent delimiters — ,, mid-record, ,/ at the end.

Amounts have no decimal point. The currency code in force (from the 02 or 03 record) determines the implied decimals — two for most currencies, zero for some such as JPY — so 5525116638 in a USD account is $55,251,166.38. And amounts on detail and summary records are always positive or unsigned: direction lives in the type code, not in a sign. Only status (balance) amounts can carry a -.

A minimal real file, from SouthState Bank's guide:

TEXT
01,053200983,053200983,241210,1228,1,,,2/
02,,063114030,1,241209,,USD,2/
03,9999999999991,USD,015,69627820,0,,045,69627820,0,,100,0,0,,400,8949876,0,/
16,506,8949876,,,,TRANSFER TO COMMERCIAL ANALYSIS ACCOUNT 9999999999992/
49,157155392,3/
98,245520532,1,5/
99,245520532,1,7/

(Structure per SouthState's published sample, trimmed to its first account with the trailer record counts adjusted to match; the control totals are the bank's printed figures — checksums, not balances, as the trailer section below explains.)

01 and 02 — the headers

01 File Header — sender ID, receiver ID, file creation date (YYMMDD), file creation time in military format, a file identification number that must be unique per creation date, optional physical record length and block size, and the version number: 2. Two field-level traps worth wiring in. Times run 0000–2400, and the spec itself warns that some processors use 9999 to indicate end-of-day — be prepared to recognize it. And the file ID is your duplicate-file detector: same sender, same date, same file ID means you've been sent the same file twice.

02 Group Header — optional ultimate receiver, the originator (US institutions are identified by ABA routing number, foreign ones by SWIFT BIC or another agreed identifier), a group status, the as-of-date, optional as-of-time, optional currency code (default USD — this is a US format to its bones), and an as-of-date modifier. The two enumerations:

FieldValues
Group status1 = update, 2 = deletion, 3 = correction, 4 = test only
As-of-date modifier1 = interim previous-day, 2 = final previous-day, 3 = interim same-day, 4 = final same-day

The modifier is how one format serves both jobs camt splits into camt.053 and camt.052: a previous-day BAI2 file (modifier 2) is your end-of-day statement, an interim same-day file (modifier 3) is your intraday feed. HSBC's implementation increments the file ID for each intraday transmission and resets it daily. And group status 3 — correction — means replace previously sent data; an importer that appends corrections instead of replacing them double-counts the day.

03 — the account record, where the balances live

The 03 record identifies the account and then carries the day's balance and summary figures as a repeating series of field groups: type code, amount, item count, funds type — again and again until the record (or its 88 continuations) runs out. The account number is alphanumeric, leading zeros significant. This is the record the whole cash position is built from, and here it is in HSBC's worked example:

TEXT
03,260093320000,USD,010,5525116638,,/
88,015,6105839257,,,040,5524958738,,,045,6105681357,,/
88,100,784117150,,,102,,18,/
88,400,203394531,,,402,,63,/

Read the first line with the repeating-group rule: account 260093320000, currency USD, then type code 010 (opening ledger) with amount $55,251,166.38, item count and funds type defaulted. The continuations add 015 (closing ledger), 040/045 (opening/closing available), then the summaries — 100 total credits with 102 giving the count of credits (18), 400 total debits with 402 the count (63). Everything you need for a position and a completeness check, in four lines.

The rules attached to this record are exactly the ones parsers get wrong:

  • Status amounts may be signed negative; summary amounts may not. An overdrawn opening ledger arrives as 010,-500000.
  • Item count and funds type are for summary codes only and must be defaulted (adjacent commas) after a status code.
  • Only one amount per status or summary code may stand per account per as-of-date. Detail codes have no such limit.
  • An 03 can also carry no data at all03,5765432,,,,,/ is legal, an account number serving only to anchor the 16 records that follow.

The type-code system

Every figure in an 03 and every transaction in a 16 is classified by a three-digit type code from BAI's uniform list — and all three digits are significant, so a parser that strips leading zeros turns opening ledger (010) into nonsense. Codes come in three kinds: status (a balance at a point in time), summary (a total of activity, may carry an item count), and detail (one transaction). The ranges, from the specification:

RangeMeaning
001–099Account status (balances, float)
100Total Credits summary
101–399Credit summary and detail codes
400Total Debits summary
401–699Debit summary and detail codes
700–799Loan summary and detail codes
900–999Customized, bank-defined codes

So credit vs debit is a property of the code range — the amount itself is unsigned. Two more special cases: code 890 is a non-monetary detail code whose text field carries free-form messages (16,890,,,,,detail reports will be delayed until 11:00 AM. is the spec's own example), and the 900 range is explicitly a free-for-all — BAI assigned nothing there, banks define their own, and HSBC's guide duly fills it with dozens of US- and UK-specific codes. If your parser meets a 9xx code, the answer is in that bank's guide and nowhere else.

The status codes you'll actually build a cash position from, per the specification's Appendix A:

CodeMeaningWhen you see it
010Opening LedgerEnd-of-day and intraday
015Closing LedgerEnd-of-day
030Current LedgerIntraday
040Opening AvailableEnd-of-day and intraday
045Closing AvailableEnd-of-day
060Current AvailableIntraday
063Total FloatWhere the bank reports float
0721-Day FloatWith 074 (2+ days) and 075 (3+ days)

The end-of-day/intraday columns follow HSBC's implementation table: its previous-day files carry 010/015/040/045, its intraday files 010/030/040/060 — no closing balances, because the day isn't closed. Ledger vs available is the same booked-versus-usable distinction as MT940's :62F: against :64:, or camt.053's CLBD against CLAV, except BAI2 goes further and itemizes the float that separates them. And the code lists are genuinely bank-specific in the small print: SouthState labels 010/015 "Starting Balance" and "Ending Balance" — same codes, its own names — and reports a fixed subset of about thirty detail codes, while HSBC publishes hundreds, with per-country reinterpretations (115 is a lockbox deposit in the US and an India lockbox deposit in its India implementation). Print the code, then look it up in that bank's guide.

16 — the transaction detail record

One booked transaction per record, six fields: type code, amount, funds type, bank reference number, customer reference number, text. From SouthState's guide:

TEXT
16,475,119480,,910211979,35705,Check Paid/

Type code 475 is Check Paid — a 4xx code, so a debit — for $1,194.80; funds type defaulted; the bank's reference is 910211979; and the customer reference is 35705, which for code 475 is the check number. That last mapping is in the specification itself: for detail codes 474, 475 and 395, the customer reference number carries the check number. For payments you originated, the customer reference is where your own reference comes back — BAI2's version of the reconciliation round trip that MT940 runs through :61: subfield 7 and camt.053 through EndToEndId. HSBC populates NONREF when it has nothing, same convention as SWIFT.

The text field is the format's free-text tail — the counterparty names and descriptions that don't fit anywhere structured — and it comes with the file format's sharpest edge: text must not begin with a slash but may contain slashes and commas after the first character, and per the specification the end of a text field is marked not by a / but by the beginning of the next record. HSBC's guide states it flatly: the / is not used when the record ends in text. SouthState's examples, as printed above, do end text with a slash. Both banks publish BAI2; a parser that hard-codes either convention breaks on the other. Split the file on line boundaries and record codes, treat everything after the fifth comma as text, and treat a trailing slash as content you may need to trim per bank.

88 — continuation, the record that isn't one

An 88 record continues the previous record — any type, including another 88 — in exactly the same format, picking up at the field where the previous physical line stopped. Three rules from the specification: a non-text field may never be split across records; a record about to be continued ends in / (not ,/); and a text field can split anywhere — HSBC's example wraps a counterparty name mid-word, …POWER CONVERSION PRAVITE LIMITE continued by 88,D. So an 88 is not a data record at all: your parser's first pass should splice 88s onto their parents to rebuild logical records, and only then parse fields. Most real-world balance records arrive as an 03 plus two or three 88s — the HSBC example above is the normal case, not the exception.

49, 98, 99 — trailers and the arithmetic they hand you

Each envelope closes with a trailer, and each trailer carries a control total and a record count:

  • 49 Account Trailer — the algebraic sum of every amount field in the preceding 03 and its 16/88 records, plus a count of records including the 03 and the 49 itself.
  • 98 Group Trailer — sum of the account control totals; number of accounts (03 records); number of records in the group.
  • 99 File Trailer — sum of the group control totals; number of groups; number of records in the file.

Note what the account control total is: balances and transactions summed together — opening ledger plus closing ledger plus total credits plus each detail amount, one number. It is meaningless as money, and that's fine; it's a checksum, not a balance. Two things are excluded: amounts inside funds-type availability fields, and item counts. Recompute all three levels on import and reject the file on mismatch — this is BAI2's equivalent of the MT940 balance-chain check, and it catches truncation before the truncation becomes a wrong cash position. The balance continuity check (previous 015 equals today's 010) isn't a stated rule of the format the way :60a:/:62a: chaining is in MT940, but it's the same free detector, and I'd wire it in regardless.

The funds-type field

The odd one out among the fields, because it's composite — one field that can expand into several, changing the record's field count. It reports availability of the amount it follows:

CodeMeaningWhat follows
ZUnknown (default)Nothing
0, 1, 2Whole amount available in 0 / 1 / 2+ daysNothing
VValue datedValue date (YYMMDD), then optional value time
SDistributed availabilityThree amounts: immediate, one-day, two-or-more-days
DDistributed availabilityA count, then that many day/amount pairs

So 16,115,10000000,S,5000000,4000000,1000000/ is a $100,000 lockbox deposit of which $50,000 is available now, $40,000 tomorrow and $10,000 in two or more days — and the record is suddenly three fields longer than the plain-vanilla case. V with a date is how value-dated entries arrive (…,V,040701,1300,… in the spec's example). A fixed-position parser dies here; the funds-type code must drive how many fields you consume next. Availability amounts can be signed (float adjustments), are excluded from control totals, and — the spec is explicit — may not sum to the amount they describe. Whether the field is used at all is bank policy: SouthState doesn't populate it; HSBC uses V when value, posting and reporting dates differ.

Reading one end to end

The reading algorithm that falls out of all this: split on record codes; splice 88s onto parents; walk 01 → 02 → 03 → 16s → 49, maintaining the envelope stack; apply implied decimals from the currency in scope; take direction from the type-code range, never from a sign; expand funds-type fields by their own code; verify all three trailer totals. When an electronic bank statement run in SAP imports a BAI2 file, the type code is the external transaction code its posting rules key on — the same job MT940's :61: transaction type and camt.053's Bank Transaction Code do — so the per-bank code list isn't trivia, it's the input to your posting-rule design.

What usually goes wrong

  • Parsing amounts as signed decimals. No decimal points (implied by currency), no signs on detail amounts. Direction is the type-code range: 100–399 credit, 400–699 debit.
  • Treating 88 as a record type. It's a continuation. Splice first, parse second — and expect text to split mid-word.
  • Hard-coding the text terminator. The spec ends text at the next record boundary; HSBC omits the trailing slash, SouthState prints one. Per-bank tolerance, not one rule.
  • Fixed field positions through funds type. S, D and V change the field count. The composite must drive the cursor.
  • Skipping the trailer arithmetic. Three levels of control totals are sitting there; recompute them or ship truncated files into your cash position.
  • Stripping leading zeros from type codes. 010 is a code, 10 is not. Same for account numbers — leading zeros are significant.
  • Assuming one bank's code list. The 900 range is bank-defined by design, and even uniform codes get bank-specific names and per-country meanings. One code map per bank, from that bank's guide.

The format is older than almost everything that parses it, and its US home means most treasuries meet it only on the American accounts in an otherwise MT940/camt landscape — the comparison post covers when you'd choose it. But the engineering habit is the same one the whole statement trio teaches: the standard gives you the skeleton and the integrity checks for free, and every hard part — code lists, text conventions, which optional fields arrive — is written down in a per-bank guide that you should read before the first production file, not after.


See also bank statement formats: MT940 vs camt.053 vs BAI2, the MT940 tag walkthrough, the camt.053 field walkthrough, and electronic bank statement processing in SAP.

Primary sources

Record layouts, field rules, type-code ranges and the printed codes verified against BAI's Cash Management Balance Reporting Specifications v2 (2005 Technical Reference Manual) and the published BAI2 implementation guides of HSBC and SouthState Bank; the BAI1/BAI2/BTRS history against ASC X9's BTR3 format guide. Bank code lists diverge — your bank's guide is the contract.

Frequently asked questions

4

What are the record types in a BAI2 file?

Eight, arranged as nested envelopes. An 01 file header opens the file and a 99 file trailer closes it with a control total. Inside, one or more groups each open with an 02 group header (one originator, one as-of-date) and close with a 98 group trailer. Inside a group, each account opens with an 03 account identifier record carrying the balance and summary data, is followed by zero or more 16 transaction detail records, and closes with a 49 account trailer. An 88 continuation record can follow any record type — including another 88 — and carries the overflow in the same format. Every trailer's control total and record count is checkable arithmetic, which is the format's built-in integrity test.

What do BAI2 type codes 010, 015, 040 and 045 mean?

They are account status codes from BAI's uniform type-code list, reported on the 03 record (or its 88 continuations): 010 is the opening ledger balance, 015 the closing ledger, 040 the opening available and 045 the closing available. Ledger is what the books say; available is what the funds-availability rules let you use. Intraday files use different codes for the same idea — 030 current ledger and 060 current available; in HSBC's implementation, for example, an end-of-day file carries 010/015/040/045 while an intraday file carries 010/030/040/060 and omits the closing balances, because the day isn't closed yet. Status amounts may be signed negative, and each status code may appear only once per account per as-of-date.

How does a BAI2 file distinguish debits from credits?

By the type code on each record, not by a sign — detail and summary amounts are always positive or unsigned. Codes 100–399 are the credit family (100 itself is the Total Credits summary) and 400–699 are the debit family (400 is Total Debits), with 700–799 reserved for loan reporting and 900–999 for codes each bank defines itself. So a 16 record with code 475 (Check Paid) is a debit and one with 142 (ACH Credit Received) is a credit, and the amount field looks identical on both. A parser that expects signed amounts, or that ignores the code and guesses direction some other way, will book half the statement backwards.

What is the difference between BAI and BAI2?

Versions of the same specification. The Bank Administration Institute's Cash Management Balance Reporting Specifications went through a Version 1 (retroactively 'BAI1') and a Version 2 — and BAI2 is simply that second version, the one the industry standardized on; the version-number field every file header carries is 2. In 2008 BAI transferred the copyright to Accredited Standards Committee X9, and in 2012 ANSI approved the Balance and Transaction Reporting Standard (BTRS) as its successor — versioned straight to BTR3 in 2016 so the numbering would continue from BAI2. BTRS was built so a BAI2 file could be read by a BTRS reader (with exceptions for deleted or repurposed codes), but not the reverse, and in practice banks' guides still say BAI2.