Skip to content
Finlecy

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.

MA

Mikel Aranburu

Founding Engineer, Formats

30 Jun 2026 · 9 min read

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.

typescript
// 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.

Finlecy, C/ Hijuela de Lojo 75, 20491 Belauntza, Guipúzcoa, Spain