camt.053 in practice
The schema is public, the standard is well written, and the files will still surprise you. Eleven things that actually break parsers.
Mikel Aranburu
Founding Engineer, Formats
ISO 20022 is a genuine improvement over what came before. camt.053 is structured, extensible, and unambiguous in most of the places that MT940 was ambiguous. None of which prevents a bank statement from arriving in a shape your parser did not anticipate.
What follows is not a criticism of the standard. It is a list of things that are permitted by the standard, appear in production files from real European banks, and will break an implementation written against the happy path.
Structure
1. Namespace prefixes vary, and one bank changed theirs mid-year
The namespace URI is stable; the prefix is not. Files arrive with no prefix, with ns:, with Doc:, and in one memorable case with a prefix that changed between the header and the body. Match on local names, never on qualified names.
// Wrong: assumes an un-prefixed default namespace.
entry.querySelector("Amt")
// Right: namespace-agnostic descendant lookup.
Array.from(entry.getElementsByTagName("*"))
.filter(el => el.localName === "Amt")2. <Cd> means six different things
A code element appears inside the entry status, the bank transaction code domain, the family, the purpose, the return reason and the proprietary block. A parser that collects every <Cd> in an entry and takes the first one will read the booking status BOOK as a transaction domain. Scope every lookup to its parent element.
3. One entry can contain many transactions
An <Ntry> holding a batched credit may carry dozens of <TxDtls>, each with its own amount, debtor and remittance information. The entry amount is the total. If you flatten to entry level you lose the ability to match individual items; if you flatten to transaction level you double-count against the balance. Keep both, and reconcile the sum.
Amounts and dates
4. Direction is a field, not a sign
<Amt> is always positive. <CdtDbtInd> carries CRDT or DBIT. This is cleaner than the alternative, and it means any code path that infers direction from the sign of the amount is wrong by construction. It also means a missing CdtDbtInd is unrecoverable — reject the entry rather than assuming.
5. Reversal indicators change the meaning of the sign
<RvslInd>true</RvslInd> on a credit entry means money left the account. Treating it as an ordinary credit produces a statement that balances against itself and disagrees with the bank by twice the amount — a difference that is very hard to find later, because both figures look plausible.
6. Booking date and value date are not interchangeable
Matching should use value date, because that is when the money is economically yours. The close should use booking date, because that is what the bank's balance is built from. Storing one and deriving the other works for eleven months and then produces a cliff error at the year-end boundary.
7. Value date can precede booking date
Back-valued entries are ordinary. Any validation asserting valueDate >= bookingDate will start rejecting good files the first time a correction is posted.
The parts humans have to read
8. Structured remittance information is optional and usually absent
<Strd> exists, is well designed, and shows up in a minority of the files we see. The rest use <Ustrd>, which is free text with a length limit, populated by whatever system the sender happens to run. Reference recovery is a text problem, not a schema problem, and no amount of standards adoption has changed that yet.
9. Debtor and creditor swap by direction
On an incoming credit, the counterparty is in <Dbtr>. On an outgoing debit it is in <Cdtr>. Both blocks may be present on the same entry. Picking the wrong one gives you your own company name as the counterparty on half the statement.
10. Proprietary codes carry the information you need
The standardised bank transaction code will tell you an entry is PMNT/RDDT/UPDD — a returned direct debit. The scheme reason code explaining why it was returned lives in the proprietary block, in a bank-specific format. That reason is the difference between retrying the collection and closing the account, so the proprietary block has to be parsed per bank rather than ignored.
11. Balances are the only checksum you get
<Bal> with OPBD and CLBD gives opening and closing figures. Opening plus the signed sum of entries must equal closing. If it does not, the file is incomplete or your parser is wrong, and either way nothing downstream should run.
What this means for reconciliation
Every one of these is a normalisation concern, and every one of them has to be settled before matching begins. A matching engine that also parses is a matching engine that will eventually make a matching decision on the basis of a parsing bug, and it will be almost impossible to tell the two apart from the output.
Separate them. Normalise into one canonical record — signed minor units, both dates, a flattened transaction code, a recovered reference and the untouched original narrative. Assert the balance checksum. Only then match. When something goes wrong you will know which half it went wrong in, and that is worth more than any individual rule.
Keep reading
Operations
The 3% problem
Match rate is the metric everyone reports and the one that tells you least. What matters is the shape of what did not match.
7 min read
Engineering
Where rules-only matching stops
A well-written rule set gets you to about 92%. The remaining 8% is not more rules — it is a different class of problem.
8 min read
Compliance
An exception agent you can audit
Putting a language model near the general ledger is a governance question before it is an engineering one. Here is the architecture we could defend.
8 min read