BAI2 and accounting CSV
Convert BAI2 cash-management files to and from MT and ISO 20022 statements, and render statements as CSV for Xero, QuickBooks, Sage or your own column layout, plus OFX, QFX and QBO.
BAI2 in both directions
BAI2 is the comma-separated balance-reporting file many US banks deliver. In transmute it's the statement format bai2 (kind file), and it works in both directions:
- a BAI2 file converts to every MT and ISO 20022 statement format (
mt940,mt950,mt942,mt900,mt910,camt.053,camt.052,camt.054), to the accounting files below, and toaccounting-transaction; - every statement source, including canonical JSON, converts to
bai2.
A body whose first record starts with 01, is detected as BAI2, so from is optional. BAI2 is plain text: send it as text/plain or in the JSON envelope's message.
curl -sS -X POST "https://api.transmute.403fin.io/v1/convert?to=camt.053" \
-H "Authorization: Bearer $TRANSMUTE_KEY" \
-H "Content-Type: text/plain" \
--data-binary @- <<'EOF'
01,AAAADEFF,BBBBDEFF,260602,0900,FILE01,,,2/
02,BBBBDEFF,AAAADEFF,1,260602,,USD,2/
03,0123456789,USD,010,100000,,,015,125000,,/
16,195,25000,Z,BANKREF1,INV4711,INCOMING WIRE/
88,INVOICE 4711/
49,250000,4/
98,250000,1,6/
99,250000,1,8/
EOF
The result is one camt.053 statement for account 0123456789 in USD: opening balance 1000.00, closing balance 1250.00, and one 250.00 credit booked as an incoming wire (PMNT/RCDT/DMCT, proprietary code 195) with the remittance lines INCOMING WIRE and INVOICE 4711.
How a BAI2 file is read
| Record | What it carries | Where it goes |
|---|---|---|
01 file header | sender, receiver, creation date and time, file id | file id becomes the statement reference; creation time becomes createdAt; sender and receiver ids have no statement slot (DROP_FIELD info) |
02 group header | originator, as-of date, currency | a BIC-shaped originator becomes the account servicer; the as-of date dates the balances and entries |
03 account | account number, currency, balance and summary codes | one statement per 03 record; balances by the code table below |
16 transaction | type code, amount, funds type, bank reference, customer reference, text | one booked entry each: bank reference becomes acctSvcrRef, customer reference becomes endToEndId, text becomes remittance lines |
88 continuation | more fields or text for the record before it | folded into that record; after a 16, each 88 is one more remittance line |
49, 98, 99 trailers | control totals and record counts | checked, never copied |
Things to know:
- Implied decimals. BAI2 amounts have no decimal point. The number of decimals is the currency's minor units:
25000is 250.00 in USD, 25000 in JPY, and 25.000 in KWD. - Currency comes from the
03record, else the02group, elseUSDwith aMISSING_REQUIRED_DEFAULTEDwarning. - Direction comes from the type code. transmute maps about 70 common detail codes to an ISO 20022 bank transaction code and an MT
:61:type (for example195incoming wire,475cheque paid,661account analysis fee). An unlisted code takes its direction from its range:100-399credit,400-699debit, withCODE_UNMAPPED_PROPRIETARY(info). A code outside both ranges carries no direction, so that entry is dropped with a lossyDROP_FIELD. The original code always travels on as the proprietary bank transaction code. - Value dates. Funds type
Vsupplies the entry's value date; otherwise the as-of date is used. Other availability details have no canonical slot (DROP_FIELDinfo). - Trailers are verified. Each trailer's control total and record count are compared with what was read. A mismatch or missing trailer is
AMOUNT_MISMATCH(warning). Totals are never copied into the statement.
Balance and summary codes on the 03 record:
| Code | Becomes |
|---|---|
010 | opening balance (OPBD) |
015 | closing balance (CLBD) |
030 | current intraday balance, as the closing balance typed ITBD |
045 | closing available balance (CLAV) |
100 / 400 | the stated total credits / debits and their counts, copied as given, never summed |
Any other summary code is dropped with a lossy DROP_FIELD. Limits: 1 MiB per file, 4096 bytes per physical record, 100,000 records.
Writing BAI2
Converting a statement to bai2 writes one file with one 02 group and one 03 account per statement. Here's the quickstart MT940 as BAI2:
01,NOTPROVIDED,NOTPROVIDED,260602,0900,STMT0001,,,2/
02,,NOTPROVIDED,1,260602,,EUR,/
03,DE89370400440532013000,EUR,010,100000,,,015,125000,,/
16,108,25000,,BANKREF1,,INVOICE 4711/
49,250000,3/
98,250000,1,5/
99,250000,1,7/
- Balances go to
010/015(or030for an interim closing balance) and the first available balance to045. A stated transaction summary is copied to100/400. - Each entry's type code is derived from its bank transaction code. When nothing matches, the default is
399(credit) or699(debit) withCODE_UNMAPPED_PROPRIETARY. - Sender, receiver and originator ids the source doesn't have are written as
NOTPROVIDEDwithMISSING_REQUIRED_DEFAULTED. - The trailers are computed from the records just written.
- BAI2 has no field for the account owner, counterparty, reversals, statement and sequence numbers, or a balance date that differs from the as-of date. Each of these is reported with a lossy
DROP_FIELD. - A
,or/inside a text field would break the record, so it's replaced with.and reported asCHARSET_DROP. - An amount with more decimals than the currency allows can't be written as implied decimals. The conversion fails instead of rounding.
Accounting CSV targets
Every statement source (MT, ISO 20022, BAI2 or canonical JSON) renders to four CSV formats for accounting import. Each statement becomes one CSV file (RFC 4180 quoting, CRLF line ends, UTF-8 without BOM). Amounts use a dot decimal with no thousands separator.
| Target | Layout | Default dateFormat |
|---|---|---|
csv-xero | Date, Description, Debit, Credit | dmy |
csv-quickbooks | csvTemplate quickbooks-3col (default): Date, Description, Amount with money out negative; or quickbooks-4col: Date, Description, Credit, Debit | mdy |
csv-sage | Date, Reference, Description, Amount (signed) | dmy |
csv-generic | your own columns (csvColumns) or a built-in template (csvTemplate) | ymd |
dateFormat (dmy gives 02/06/2026, mdy gives 06/02/2026, ymd gives 2026-06-02) overrides the default for any CSV target.
curl -sS -X POST "https://api.transmute.403fin.io/v1/convert?to=csv-quickbooks&csvTemplate=quickbooks-4col" \
-H "Authorization: Bearer $TRANSMUTE_KEY" \
-H "Content-Type: text/plain" \
--data-binary @statement.mt940
Date,Description,Credit,Debit
06/02/2026,INVOICE 4711,250.00,
csv-generic: choose your columns
options.csvColumns is a list of up to 64 columns. Each one is a field id, or Header=field to set your own header:
{
"to": "csv-generic",
"message": "{1:F01AAAADEFFXXXX0000000000}{2:I940BBBBDEFFXXXXN}{4:
:20:STMT0001
...",
"options": { "csvColumns": ["Booked=date.booking", "amount.signed", "currency", "description", "Ref=reference.bank"] }
}
Booked,amount.signed,currency,description,Ref
2026-06-02,250.00,EUR,INVOICE 4711,BANKREF1
On raw requests, repeat the query parameter once per column: ?csvColumns=Booked%3Ddate.booking&csvColumns=amount.signed.
| Field | Cell |
|---|---|
date | booking date, else value date |
date.booking, date.value | that date, empty when absent |
amount.signed | amount, - for debits |
amount.debit, amount.credit | absolute amount on debit or credit rows, empty otherwise |
currency | ISO 4217 code |
description | counterparty name, remittance lines, structured references and extra information joined (500-character cap, TRUNC_REMITTANCE) |
reference.endToEnd, reference.bank | end-to-end reference, bank's reference |
counterparty.name, counterparty.iban, counterparty.bic | the other party |
btc.proprietary | proprietary bank transaction code |
btc.domain | domain/family/subFamily |
balance.opening, balance.closing | the statement's opening or closing balance, repeated on every row |
There is no running-balance column: transmute never calculates money.
Without csvColumns, csv-generic uses csvTemplate: standard (default: Date, Value Date, Amount, Currency, Description, Reference, Bank Reference, Counterparty, Counterparty IBAN, Code) or any of quickbooks-3col, quickbooks-4col, sage.
The following are invalid-input 400: an unknown field or template, csvColumns and csvTemplate together, a template that doesn't belong to the target (csv-quickbooks takes only the two QuickBooks layouts, csv-sage only sage), or csvColumns on csv-quickbooks or csv-sage. Targets that aren't CSV ignore both options.
A text cell that starts with =, +, -, @ or a control character is prefixed with a quote so a spreadsheet can't run it as a formula. This applies to your own headers too and is reported as CSV_FORMULA_NEUTRALIZED. Amount cells are numbers and aren't changed.
The statement's transaction summary has no CSV column and is reported as DROP_FIELD.
OFX, QFX and QBO
Statements also render to ofx (OFX 1.02 SGML by default, or 2.2 XML with ofxVersion: "220"), qfx (Quicken) and qbo (QuickBooks Web Connect). QFX and QBO are always 1.02 SGML. intuBid sets the Intuit institution id in QFX and QBO (default placeholder 3000).
Notes
- All file targets are output-only. Nothing converts from
csv-*,ofx,qfxorqbo. To read payment spreadsheets, see Payment spreadsheets. csvColumnsandcsvTemplateapply to synchronous/v1/convert. Batch items don't carry them.dateFormat,ofxVersionandintuBidwork on batch items too.- Every warning code above is in the warning registry. The live list of pairs is
GET /v1/formats.