SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
23.2 KB

# Bank Statement Formats & Reconciliation Research

Research for Phase 6 (bank reconciliation): parsing bank statements (camt.053, MT940) and matching statement lines against ledger cash movements. All findings below are sourced from web research performed on 2026-08-05; nothing is from memory.


# 1. ISO 20022 camt.053 — BankToCustomerStatement

camt.053 is the ISO 20022 XML end-of-day bank account statement (the "MX" successor to MT940). It is part of the Bank-to-Customer Cash Management message set maintained by the ISO 20022 Registration Authority (iso20022.org).

# 1.1 Document root and namespace

  • Root element: <Document> with a version-specific default namespace: urn:iso:std:iso:20022:tech:xsd:camt.053.001.NN (e.g. camt.053.001.02, camt.053.001.08). The version is identified via the XML namespace, so the parser must read the namespace, not assume one.
  • Inside: <BkToCstmrStmt> (BankToCustomerStatement), containing one <GrpHdr> and one or more <Stmt> blocks (one per account/period).

# 1.2 Structure (elements the AIR parser must handle)

text
Document                                    @xmlns = urn:iso:std:iso:20022:tech:xsd:camt.053.001.NN
└── BkToCstmrStmt
    ├── GrpHdr
    │   ├── MsgId                           unique message id
    │   └── CreDtTm                         file creation timestamp (ISO 8601)
    └── Stmt                                [1..n] one per account statement
        ├── Id                              statement id
        ├── ElctrncSeqNb                    electronic sequence number (optional)
        ├── CreDtTm                         statement creation timestamp
        ├── Acct
        │   ├── Id/IBAN  or  Id/Othr/Id     account identifier (IBAN or domestic)
        │   └── Ccy                         account currency (ISO 4217)
        ├── Bal                             [1..n] balances
        │   ├── Tp/CdOrPrtry/Cd             OPBD=opening booked, CLBD=closing booked,
        │   │                               CLAV=closing available, OIBD/CIBD=interim (multi-page)
        │   ├── Amt @Ccy                    decimal amount, currency as XML attribute
        │   ├── CdtDbtInd                   CRDT | DBIT (sign of the balance)
        │   └── Dt/Dt                       balance date
        └── Ntry                            [0..n] statement entries (transactions)
            ├── NtryRef                     entry reference (optional)
            ├── Amt @Ccy                    ALWAYS positive decimal; currency attribute mandatory
            ├── CdtDbtInd                   CRDT (money in) | DBIT (money out) — gives the sign
            ├── RvslInd                     true if this entry reverses a previous one (optional)
            ├── Sts                         BOOK (booked) | PDNG (pending) | INFO
            ├── BookgDt/Dt (or /DtTm)       booking (posting) date
            ├── ValDt/Dt (or /DtTm)         value date
            ├── AcctSvcrRef                 bank's own reference (optional)
            ├── BkTxCd                      bank transaction code (Domn/Fmly/SubFmlyCd or Prtry/Cd)
            ├── NtryDtls                    [0..n]
            │   ├── Btch                    batch info (NbOfTxs, TtlAmt) when 1 entry = n transactions
            │   └── TxDtls                  [0..n] underlying transactions
            │       ├── Refs/EndToEndId     payer-assigned end-to-end reference (invoice/PO ref)
            │       ├── Refs/TxId, InstrId, MndtId
            │       ├── AmtDtls             InstdAmt / TxAmt (useful for FX)
            │       ├── RltdPties           Dbtr / Cdtr names and accounts
            │       └── RmtInf/Ustrd        unstructured remittance info (free text)
            │           RmtInf/Strd         structured remittance (creditor reference etc.)
            └── AddtlNtryInf                free-text description of the entry

Key semantics:

  • Amt carries no sign; direction comes exclusively from CdtDbtInd (CRDT = credit to the account = money received; DBIT = money out).
  • The currency is the mandatory Ccy attribute on Amt.
  • One Ntry may aggregate many underlying transactions (batch/lump-sum entries): NtryDtls/Btch + repeated TxDtls. This is the structural basis for one-to-many reconciliation matching.
  • EndToEndId (max 35 chars) travels unaltered from the payment initiation (pain.001) to the statement — it is the best deterministic matching key.

# 1.3 Version differences (brief)

  • Common versions in the wild: .02 (2009 base, still the most widely delivered by banks, e.g. Nordea, Dutch banks), .06, .08 (2019 — the CBPR+/SEPA-aligned version banks are converging on), .10/.11/.13 (latest, Feb 2025).
  • Newer versions mostly add optional fields without removing existing ones; the structural core (GrpHdr / Stmt / Bal / Ntry) is stable across versions. Notable mechanical difference: in .02 Sts is a simple code (<Sts>BOOK</Sts>); in .08+ it becomes a complex element (<Sts><Cd>BOOK</Cd></Sts>). Party identification blocks also gain structure in .08+.
  • Practical parser strategy (used by open-source parsers): parse namespace-tolerantly, target the common core, handle the Sts shape difference.

Sources: iso20022.org Message Definition Report Part 2 (Bank-to-Customer Cash Management), Payments Canada camt.053.001.08 usage guideline, Nordea camt.053.001.02 standard, ValidateFin structural guide, darko-mijic camt.053 parser spec (URLs in §7).


# 2. MT940 — SWIFT Customer Statement Message

MT940 is the legacy SWIFT FIN "MT" end-of-day customer statement: a line/tag-based text format. Still extremely widespread in bank connectivity (EBICS, host-to-host, e-banking exports) even though it is deprecated by SWIFT (see §3).

# 2.1 Tag structure

Tag Name Format Notes
:20: Transaction Reference Number 16x Sender's reference for the message
:25: Account Identification 35x Account number (option P adds BIC)
:28C: Statement Number/Sequence 5n[/5n] e.g. 151/1
:60F: Opening Balance (First) 1!a6!n3!a15d D/C mark + date YYMMDD + currency + amount
:61: Statement Line see §2.2 Repeats per transaction
:86: Information to Account Owner 6*65x Optional; up to 6 lines × 65 chars; must follow its :61:
:62F: Closing Balance (Final) 1!a6!n3!a15d Same layout as :60F:
:60M:/:62M: Intermediate opening/closing balances same Used when a statement spans multiple messages
:64:/:65: Closing available / forward available balance same layout Optional

Constraint: a :86: must be preceded by a :61: and belongs to it; when a period spans several messages, all but the last carry :62M: and the last carries :62F:.

# 2.2 The :61: statement line — full format specification

text
:61:  6!n [4!n] 2a [1!a] 15d 1!a3!c 16x [//16x] [34x]
      │    │    │   │    │   │      │    │       └─ supplementary details (optional, new line)
      │    │    │   │    │   │      │    └─ bank reference (optional, after //)
      │    │    │   │    │   │      └─ customer reference (16x; NONREF if none)
      │    │    │   │    │   └─ transaction type code: 1 letter + 3 chars
      │    │    │   │    │      S103 = via SWIFT MT103, NTRF = non-SWIFT transfer,
      │    │    │   │    │      NMSC = misc, NCHK = cheque, NDDT = direct debit...
      │    │    │   │    └─ amount: max 15 digits, COMMA as decimal separator,
      │    │    │   │       no thousands separators, ALWAYS UNSIGNED
      │    │    │   └─ funds code (optional, 3rd char of currency)
      │    │    └─ debit/credit mark: D | C | RD (reversal of debit) | RC (reversal of credit)
      │    └─ entry (booking) date MMDD (optional; year inferred from value date)
      └─ value date YYMMDD

Key semantics:

  • Amount is unsigned; the D/C mark gives direction (D = debit = money out, C = credit = money in; RD/RC mark reversals).
  • Decimal separator is a comma (1150,00), never a dot; the currency is NOT on the :61: line — it comes from the :60F:/:62F: balance lines.
  • Value date is YYMMDD (2-digit year → pivot logic needed); entry date is MMDD with the year inferred from the value date (beware year boundaries).
  • The customer reference (16x) is the counterpart of camt's EndToEndId, but is often NONREF; the free-text :86: line then carries the useful description (bank-proprietary sub-formats exist inside :86:, e.g. German /GVC/ and ?20-style subtags — treat :86: as opaque text in v1).
  • Integrity check available to the parser: :60F: balance ± Σ(:61: lines) = :62F: balance — AIR should verify this.

Sources: Paiementor MT940 detailed analysis, National Bank of Canada MT940 user guide, Citi MT940 export guide, Huntington developer portal (URLs in §7).


# 3. Adoption status: camt.053 vs MT940 (MT→MX migration)

  • 22 November 2025: SWIFT ended MT/ISO 20022 coexistence for cross-border payment instruction messages (MT103/MT202 retired for FI-to-FI traffic).
  • Cash-management / reporting messages (MT9xx) got an extended timeline: MT940/MT942/MT950/MT900/MT910 are deprecated and no longer maintained by SWIFT but not yet withdrawn; their removal in favour of camt.052/camt.053/camt.054 is being phased in roughly 2027–2028, and major banks tell clients they must be able to receive camt messages by November 2027.
  • Practical consequence (2026): both formats are current reality. camt.053 is the strategic/current standard; MT940 remains the most widely deployed corporate statement format worldwide (bank portals, ERP integrations, EBICS). → AIR must parse both, normalizing into one internal model (§6).

Sources: SWIFT ISO 20022 programme pages, J.P. Morgan and Citi migration FAQs, RedCompass Labs 2026 deadline analysis, PaymentExpert on the Nov-2025 cutover (URLs in §7).


# 4. Bank reconciliation matching practice

How mainstream systems match statement lines to ledger (book) cash entries:

  • Exact match first: same amount, same currency, same/near date. This is the universal baseline (QuickBooks Online "matches", Xero "suggested matches").
  • Date tolerance windows: book date and bank date rarely coincide (cheques clear days later; card settlements lag 1–3 days). Treasury/ERP tools (e.g. Oracle Fusion reconciliation) expose configurable date tolerance rules; date tolerances exist mainly for instruments with clearing lag.
  • Amount tolerance: used for FX rounding differences or bank fees embedded in the statement line. QuickBooks does not natively do tolerance-based matching; Oracle-class treasury tools do (with a write-off/adjustment posting for the difference). Xero handles the fee case via "adjustments" during reconciliation.
  • Reference matching: strongest deterministic signal — cheque number, invoice number in remittance text, and in camt.053 the EndToEndId / AcctSvcrRef. Xero bank rules can condition on bank text fields, direction ("Received"/"Spent") and amount; a rule sets contact/account/tax code.
  • One-to-many / many-to-one (batch) matching: one bank deposit = many customer receipts (or one payroll debit = many payslips). Xero supports "Match > multiple items"; QuickBooks Online is weak here natively; camt.053 expresses it structurally (NtryDtls/Btch + several TxDtls). Treasury reconciliation engines support 1:1, 1:n, n:1 and n:m rule tiers, applied in priority order (exact reference → exact amount+date → amount+date window → aggregate/batch), each with confidence scoring.
  • Output convention: after matching, both sides' residues are reported — unmatched statement lines (bank has it, books don't: missing entry / fee / fraud) and unmatched ledger entries (books have it, bank doesn't: outstanding cheque / deposit in transit). This two-sided exception report is the classical bank reconciliation statement.

Sources: Oracle Fusion tolerance-rule docs, Xero reconciliation guides, QuickBooks Online reconciliation guides (URLs in §7).


# 5. Aggregator APIs (future ingestion frontends)

# 5.1 Plaid (/transactions/sync, /transactions/get)

JSON transaction objects; key fields:

  • amount — decimal number. Sign convention (Plaid docs): "Positive values when money moves out of the account; negative values when money moves in." I.e. a debit-card purchase is positive, a deposit/refund is negative — the inverse of a naive signed-bank-balance convention. Two decimal places.
  • iso_currency_code (ISO 4217; null when unofficial_currency_code is used).
  • date (YYYY-MM-DD; posting date for posted transactions, occurrence date for pending), plus authorized_date / datetime / authorized_datetime.
  • name (raw description) and merchant_name (Plaid-enriched, cleaner).
  • pending (bool — details may change at settlement; the posted transaction arrives with a new transaction_id and a pending_transaction_id link).
  • transaction_id (unique, case-sensitive), payment_channel (online / in store / other), personal_finance_category.

⚠ Plaid delivers amount as a JSON number → the AIR parser must decode it via decimal-preserving parsing (e.g. json.loads(..., parse_float=Decimal)), never through binary floats.

Transactions array per account; each item:

json
{
  "Date": "2025-01-31",
  "Code": null,
  "Description": "PAYROLL - Stripe Paycheck",
  "Debit": 1000.4,
  "Credit": 1500.25,
  "Balance": 5105.6,
  "Id": "94584aed-7c98-42a4-9836-9f8557db63f5"
}
  • Direction is expressed by separate Debit / Credit fields (one populated, the other null in real responses), not a signed amount — a third sign convention to normalize. Balance is the running balance after the transaction; Id is Flinks' unique transaction id.
  • Same JSON-number caveat as Plaid: parse into Decimal, never float.

Sources: Plaid Transactions API reference, Flinks GetAccountsDetail docs (URLs in §7).


# 6. Example fixtures (small, structurally correct)

Both fixtures describe the same statement: CAD account, opening balance 25,000.00, one outgoing supplier payment of 1,150.00 on 2026-08-01, one incoming customer payment of 3,449.93 on 2026-08-04, closing balance 27,299.93 (25,000.00 − 1,150.00 + 3,449.93 = 27,299.93 ✓).

# 6.1 camt.053 (version 053.001.02)

xml
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
  <BkToCstmrStmt>
    <GrpHdr>
      <MsgId>AIR-STMT-20260805-001</MsgId>
      <CreDtTm>2026-08-05T06:00:00</CreDtTm>
    </GrpHdr>
    <Stmt>
      <Id>STMT-2026-0151</Id>
      <ElctrncSeqNb>151</ElctrncSeqNb>
      <CreDtTm>2026-08-05T06:00:00</CreDtTm>
      <Acct>
        <Id><Othr><Id>00112233445</Id></Othr></Id>
        <Ccy>CAD</Ccy>
      </Acct>
      <Bal>
        <Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp>
        <Amt Ccy="CAD">25000.00</Amt>
        <CdtDbtInd>CRDT</CdtDbtInd>
        <Dt><Dt>2026-07-31</Dt></Dt>
      </Bal>
      <Bal>
        <Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp>
        <Amt Ccy="CAD">27299.93</Amt>
        <CdtDbtInd>CRDT</CdtDbtInd>
        <Dt><Dt>2026-08-04</Dt></Dt>
      </Bal>
      <Ntry>
        <NtryRef>BKREF001</NtryRef>
        <Amt Ccy="CAD">1150.00</Amt>
        <CdtDbtInd>DBIT</CdtDbtInd>
        <Sts>BOOK</Sts>
        <BookgDt><Dt>2026-08-01</Dt></BookgDt>
        <ValDt><Dt>2026-08-01</Dt></ValDt>
        <BkTxCd><Prtry><Cd>NTRF</Cd></Prtry></BkTxCd>
        <NtryDtls>
          <TxDtls>
            <Refs><EndToEndId>INV-2026-0042</EndToEndId></Refs>
            <RmtInf><Ustrd>PAYMENT ACME INC INVOICE INV-2026-0042</Ustrd></RmtInf>
          </TxDtls>
        </NtryDtls>
        <AddtlNtryInf>Supplier payment Acme Inc.</AddtlNtryInf>
      </Ntry>
      <Ntry>
        <NtryRef>BKREF002</NtryRef>
        <Amt Ccy="CAD">3449.93</Amt>
        <CdtDbtInd>CRDT</CdtDbtInd>
        <Sts>BOOK</Sts>
        <BookgDt><Dt>2026-08-04</Dt></BookgDt>
        <ValDt><Dt>2026-08-04</Dt></ValDt>
        <BkTxCd><Prtry><Cd>NTRF</Cd></Prtry></BkTxCd>
        <NtryDtls>
          <TxDtls>
            <Refs><EndToEndId>E2E-SALE-7781</EndToEndId></Refs>
            <RmtInf><Ustrd>CUSTOMER CUST 123 SALE 7781</Ustrd></RmtInf>
          </TxDtls>
        </NtryDtls>
        <AddtlNtryInf>Customer payment, sale 7781</AddtlNtryInf>
      </Ntry>
    </Stmt>
  </BkToCstmrStmt>
</Document>

(For .08+ fixtures the only mechanical change in these fields is <Sts><Cd>BOOK</Cd></Sts> and the namespace suffix.)

# 6.2 MT940

text
:20:AIR-STMT-0805
:25:BOFCCAM2/00112233445
:28C:151/1
:60F:C260731CAD25000,00
:61:2608010801D1150,00NTRFINV-2026-0042//BKREF001
:86:PAYMENT ACME INC INVOICE INV-2026-0042
:61:2608040804C3449,93NTRFE2E-SALE-7781//BKREF002
:86:CUSTOMER CUST 123 SALE 7781
:62F:C260804CAD27299,93

Reading :61:2608010801D1150,00NTRFINV-2026-0042//BKREF001: value date 260801 (2026-08-01), entry date 0801, D = debit, amount 1150,00 (comma decimal), transaction type NTRF, customer reference INV-2026-0042, bank reference BKREF001.


# 7. Design decisions for AIR

  1. Common internal model. Both parsers (and future Plaid/Flinks ingesters) produce the same BankTransaction:
    • date (booking date, ISO date; value date kept separately if present)
    • amountsigned decimal serialized as a string (positive = money in to the account, negative = money out; AIR convention, normalized from CdtDbtInd / D-C mark / Plaid inverse sign / Flinks Debit-Credit columns)
    • currency (ISO 4217; from Amt@Ccy in camt, from :60F: in MT940)
    • description (from AddtlNtryInf/RmtInf/Ustrd or :86:)
    • reference (from EndToEndId / NtryRef / :61: customer+bank refs; NONREF normalized to null)
    • plus provenance: source format, statement id, raw line/entry.
  2. Amounts are never floats. camt/MT940 amounts are parsed from text into Decimal; MT940 comma decimals converted textually (1150,001150.00); aggregator JSON parsed with parse_float=Decimal.
  3. Statement integrity check at parse time: opening balance + Σ signed entries must equal closing balance (:60F:/:62F:, OPBD/CLBD); mismatch is a compiler-grade diagnostic, not a warning.
  4. Matching algorithm (v1): match = amount + currency exact, with a configurable date tolerance window (default e.g. ±3 days, policy-driven via ALSL, never hard-coded); reference/EndToEndId equality is used first as a higher-priority deterministic tier and as a tie-breaker when several candidates share amount+date. One-to-many (batch) matching deferred to a later iteration but the model keeps TxDtls multiplicity so it stays possible.
  5. Two-sided residue reporting: the reconciliation result lists matched pairs and unmatched items on both sides (unmatched bank lines, unmatched ledger entries), mirroring standard reconciliation-statement practice.
  6. Version-tolerant camt parser: read the namespace to detect the version, target the stable core (§1.2), handle the .02 vs .08 Sts shape.
  7. Both formats are required (§3): camt.053 is strategic, MT940 remains ubiquitous until at least 2027-2028.

# 8. Sources (all consulted 2026-08-05)

# camt.053 / ISO 20022

# MT940

# MT → MX migration

# Reconciliation practice

# Aggregator APIs


End of research note — AIR project — Simon-Pierre Boucher — contact@spboucher.ai