How to reconcile camt.053 statements: format, batch entries, reversals, and MT940 mapping
To reconcile a camt.053 bank statement, sign each entry amount from its credit or debit indicator, split batch entries into their transaction details, and match entries on the bank reference and the end-to-end identification. Then check that the opening booked balance plus all entries equals the closing booked balance. Any gap means a parsing defect or a missing entry.
How camt.053 parsing puts wrong numbers in the ledger
A supplier payment run of three invoices leaves the account as one debit of EUR 2,500.00. The bank reports it in camt.053 as a single Ntry with a Btch block and three TxDtls. Later the same day one payment of EUR 850.50 comes back and the bank books it as a credit with RvslInd set to true. A parser that reads only the entry level cannot close any of the three invoices, because the entry carries the payment run reference and no invoice reference. A parser that treats RvslInd as "negate the amount" posts the return as a second debit, so the ledger shows EUR 1,701.00 less cash than the bank holds, and the supplier whose payment was returned stays marked as paid until someone checks the account by hand.
These defects pass tests built on one payment per entry and no returns. They surface on accounts where the bank books payment runs as batches, and during a migration from MT940, when two formats describe the same bookings with different references. For platforms that pay out on behalf of customers, such as AP and AR automation and accounting platforms that import bank feeds, a mismatch between ledger and statement is also a gap in the reconciliation evidence a partner bank reviews; the ISO 20022 testing requirements page lists that evidence. Break classes and date windows of the matcher are in the ledger reconciliation testing guide; this guide covers the statement file that feeds it.
Procedure
1. Pin the message version and the usage guideline
Element names below follow camt.053.001.08, BankToCustomerStatementV08, as restricted by the Payments Canada Lynx corporate-to-bank usage guideline (19 April 2024) and the CBPR+ usage guideline published by Clearstream (SR2025, 26 November 2025), both checked on 2026-09-22. The CBPR+ guideline notes that the CGI-MP camt.053 guidelines are built on version 02, so a platform can receive either version. In version 02 the entry status is a code, <Sts>BOOK</Sts>; in version 08 it is a choice, <Sts><Cd>BOOK</Cd></Sts>. Read the namespace of each file and keep one fixture per version your banks send.
The Lynx guideline states that for camt.053 the status code must be BOOK. The Dutch Payments Association guideline for camt.053.001.02 (version 1.1, October 2013) defines BookgDt as the date the entry is posted on the account servicer's books and ValDt as the date the funds become available to the account owner for a credit, or stop being available for a debit. Match statement entries on booking date. Compute interest and available balance on value date; a business banking account that pays interest on balances misstates interest when a back-valued entry is applied on its booking date.
2. Build a synthetic statement with a batch and a return
The fixture uses synthetic identifiers and validates against the camt.053.001.08 XSD. Production statements and real account data stay out of the test environment.
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08">
<BkToCstmrStmt>
<GrpHdr>
<MsgId>MSG-20270315-0074</MsgId>
<CreDtTm>2027-03-15T23:10:00+01:00</CreDtTm>
</GrpHdr>
<Stmt>
<Id>STMT-20270315-01</Id>
<ElctrncSeqNb>74</ElctrncSeqNb>
<CreDtTm>2027-03-15T23:10:00+01:00</CreDtTm>
<Acct><Id><Othr><Id>ACCT-TEST-0001</Id></Othr></Id><Ccy>EUR</Ccy></Acct>
<Bal>
<Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp>
<Amt Ccy="EUR">10000.00</Amt><CdtDbtInd>CRDT</CdtDbtInd>
<Dt><Dt>2027-03-15</Dt></Dt>
</Bal>
<Bal>
<Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp>
<Amt Ccy="EUR">10167.40</Amt><CdtDbtInd>CRDT</CdtDbtInd>
<Dt><Dt>2027-03-15</Dt></Dt>
</Bal>
<TxsSummry>
<TtlCdtNtries><NbOfNtries>2</NbOfNtries><Sum>2667.40</Sum></TtlCdtNtries>
<TtlDbtNtries><NbOfNtries>1</NbOfNtries><Sum>2500.00</Sum></TtlDbtNtries>
</TxsSummry>
<!-- 1. Single credit: processor payout -->
<Ntry>
<Amt Ccy="EUR">1816.90</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Sts><Cd>BOOK</Cd></Sts>
<BookgDt><Dt>2027-03-15</Dt></BookgDt>
<ValDt><Dt>2027-03-15</Dt></ValDt>
<AcctSvcrRef>BNK0315000001</AcctSvcrRef>
<BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>RCDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn></BkTxCd>
<NtryDtls><TxDtls>
<Refs><EndToEndId>PO-20270312-000412</EndToEndId></Refs>
<Amt Ccy="EUR">1816.90</Amt><CdtDbtInd>CRDT</CdtDbtInd>
</TxDtls></NtryDtls>
</Ntry>
<!-- 2. Batch debit: supplier payment run booked as one entry -->
<Ntry>
<Amt Ccy="EUR">2500.00</Amt>
<CdtDbtInd>DBIT</CdtDbtInd>
<Sts><Cd>BOOK</Cd></Sts>
<BookgDt><Dt>2027-03-15</Dt></BookgDt>
<ValDt><Dt>2027-03-15</Dt></ValDt>
<AcctSvcrRef>BNK0315000002</AcctSvcrRef>
<BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn></BkTxCd>
<NtryDtls>
<Btch>
<PmtInfId>PMT-RUN-20270315-01</PmtInfId>
<NbOfTxs>3</NbOfTxs>
<TtlAmt Ccy="EUR">2500.00</TtlAmt>
<CdtDbtInd>DBIT</CdtDbtInd>
</Btch>
<TxDtls><Refs><EndToEndId>INV-7781</EndToEndId></Refs><Amt Ccy="EUR">1200.00</Amt><CdtDbtInd>DBIT</CdtDbtInd></TxDtls>
<TxDtls><Refs><EndToEndId>INV-7782</EndToEndId></Refs><Amt Ccy="EUR">850.50</Amt><CdtDbtInd>DBIT</CdtDbtInd></TxDtls>
<TxDtls><Refs><EndToEndId>INV-7783</EndToEndId></Refs><Amt Ccy="EUR">449.50</Amt><CdtDbtInd>DBIT</CdtDbtInd></TxDtls>
</NtryDtls>
</Ntry>
<!-- 3. Return of INV-7782: a credit that reverses the original debit -->
<Ntry>
<Amt Ccy="EUR">850.50</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<RvslInd>true</RvslInd>
<Sts><Cd>BOOK</Cd></Sts>
<BookgDt><Dt>2027-03-15</Dt></BookgDt>
<ValDt><Dt>2027-03-15</Dt></ValDt>
<AcctSvcrRef>BNK0315000003</AcctSvcrRef>
<BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>RRTN</SubFmlyCd></Fmly></Domn></BkTxCd>
<NtryDtls><TxDtls>
<Refs><EndToEndId>INV-7782</EndToEndId></Refs>
<Amt Ccy="EUR">850.50</Amt><CdtDbtInd>CRDT</CdtDbtInd>
</TxDtls></NtryDtls>
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>The bank transaction codes come from the ISO 20022 Bank Transaction Code list (30 October 2023): PMNT/RCDT/ESCT is a received SEPA credit transfer, PMNT/ICDT/ESCT an issued one, and PMNT/ICDT/RRTN a reversal due to payment return. Balance check for the fixture: 10,000.00 plus 1,816.90, minus 2,500.00, plus 850.50 equals the closing booked balance of 10,167.40.
3. Check every statement before the matcher sees it
Run the checker on every bank file and every mutated fixture; malformed files produce a result instead of a crash.
import sys
import xml.etree.ElementTree as ET
from decimal import Decimal, InvalidOperation
def local(tag):
return tag.rsplit("}", 1)[-1]
def child(el, *path):
for name in path:
if el is None:
return None
el = next((c for c in el if local(c.tag) == name), None)
return el
def children(el, name):
return [] if el is None else [c for c in el if local(c.tag) == name]
def text(el, *path):
node = child(el, *path)
return node.text.strip() if node is not None and node.text else None
def signed(amount, indicator):
# Amt is never negative (minInclusive 0); the sign comes only from CdtDbtInd.
value = Decimal(amount)
if indicator == "CRDT":
return value
if indicator == "DBIT":
return -value
raise ValueError(f"CdtDbtInd is {indicator!r}")
def one_balance(stmt, code):
found = [b for b in children(stmt, "Bal") if text(b, "Tp", "CdOrPrtry", "Cd") == code]
if len(found) != 1:
raise ValueError(f"{len(found)} balances of type {code}")
return signed(text(found[0], "Amt"), text(found[0], "CdtDbtInd"))
def check_statement(stmt):
issues = []
net = Decimal(0)
for ntry in children(stmt, "Ntry"):
ref = text(ntry, "AcctSvcrRef") or text(ntry, "NtryRef") or "no-ref"
# camt.053.001.02: <Sts>BOOK</Sts>; camt.053.001.08: <Sts><Cd>BOOK</Cd></Sts>
status = text(ntry, "Sts", "Cd") or text(ntry, "Sts")
if status != "BOOK":
issues.append((ref, f"status {status} in an end-of-day statement"))
amount = signed(text(ntry, "Amt"), text(ntry, "CdtDbtInd"))
net += amount # RvslInd does not change the sign
for dtls in children(ntry, "NtryDtls"):
txs = children(dtls, "TxDtls")
declared = text(dtls, "Btch", "NbOfTxs")
if declared and not txs:
issues.append((ref, f"batch of {declared} without TxDtls: match on Btch/PmtInfId"))
elif declared and int(declared) != len(txs):
issues.append((ref, f"NbOfTxs {declared}, TxDtls {len(txs)}"))
if txs and all(text(tx, "Amt") for tx in txs):
total = sum(signed(text(tx, "Amt"), text(tx, "CdtDbtInd") or text(ntry, "CdtDbtInd")) for tx in txs)
if total != amount:
issues.append((ref, f"TxDtls sum {total}, entry {amount}"))
opening, closing = one_balance(stmt, "OPBD"), one_balance(stmt, "CLBD")
if opening + net != closing:
issues.append(("balance", f"OPBD {opening} + entries {net} = {opening + net}, CLBD {closing}"))
return issues
def main(path, previous_seq=None, previous_clbd=None):
try:
root = ET.parse(path).getroot()
except (ET.ParseError, OSError) as exc:
print(f"ERROR {path}: {exc}")
return 2
failed = False
for stmt in root.iter():
if local(stmt.tag) != "Stmt":
continue
sid = text(stmt, "Id") or "no-id"
try:
issues = check_statement(stmt)
seq = text(stmt, "ElctrncSeqNb") or text(stmt, "LglSeqNb")
if previous_seq is not None and seq != str(int(previous_seq) + 1):
issues.append(("sequence", f"expected {int(previous_seq) + 1}, got {seq}"))
if previous_clbd is not None and one_balance(stmt, "OPBD") != Decimal(previous_clbd):
issues.append(("continuity", f"OPBD differs from previous CLBD {previous_clbd}"))
except (ValueError, TypeError, InvalidOperation) as exc:
issues = [("parse", str(exc))]
failed = failed or bool(issues)
for ref, message in issues:
print(f"FAIL {sid} {ref}: {message}")
if not issues:
print(f"PASS {sid}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(*sys.argv[1:4]))python check_camt053.py stmt.xml 73 10000.00 also checks that sequence number 74 follows 73 and that the opening balance equals the previous closing balance. The fixture passes; with the return mis-signed as DBIT, the checker reports a balance of 8,466.40 against 10,167.40.
4. Reconcile batch entries at the level the ledger books them
Assert three things for each entry with a Btch block: NbOfTxs equals the number of TxDtls, the TxDtls amounts sum to the entry amount, and each EndToEndId closes exactly one ledger item. When the bank sends no TxDtls, match the entry on Btch/PmtInfId and TtlAmt against the payment run as a whole. The Lynx guideline allows entry details to be left out for settlements where details should stay confidential, and gives payroll as the example, so a payroll account needs this fixture even when other accounts never produce it.
5. Test reversals as their own direction
The ISO definition of RvslInd, as reproduced in the Dutch guideline, says that CRDT with a reversal indicator of true means the original operation was a debit, and DBIT with true means the original was a credit. The sign is already in CdtDbtInd. The Lynx guideline adds that in a reversal the creditor and debtor parties should not be swapped and stay aligned with the original credit transfer or direct debit. Code that derives the counterparty from the entry direction therefore picks your own customer as the counterparty of a returned payment. Link the return to the original payment through EndToEndId and assert that the original invoice reopens.
6. Map MT940 fields when both formats run in parallel
During a migration, run one business day through both feeds and compare the ledger postings. Our field mapping, built from the Dutch guideline and the Danske Bank MT940 format description (reviewed 23 March 2023, checked on 2026-09-22):
| MT940 field | camt.053.001.08 element | Difference to test |
|---|---|---|
:20: transaction reference | Stmt/Id | 16 characters against 35 |
:28C: statement and sequence number | ElctrncSeqNb or LglSeqNb, StmtPgntn/PgNb | Numbering can restart in the new feed |
:60F: opening balance | Bal with code OPBD | None for a single page |
:60M: intermediate opening balance | Bal with OPBD and sub-type INTM | Pages after the first |
:61: value date, 6 digits | Ntry/ValDt/Dt | Full date in camt.053 |
:61: entry date, MMDD, optional | Ntry/BookgDt/Dt | No year in MT940 |
:61: mark C or D | Ntry/CdtDbtInd | None |
:61: mark RC or RD | CdtDbtInd DBIT or CRDT with RvslInd true | RD is a credit entry |
:61: amount, comma decimal | Ntry/Amt with Ccy, dot decimal | Separator; the amount is never negative |
:61: transaction type, such as NTRF | Ntry/BkTxCd | Different code lists |
:61: reference for the account owner, 16 characters | Refs/EndToEndId or Btch/PmtInfId, 35 characters | Truncation |
:61: // bank reference, 16 characters | Ntry/AcctSvcrRef, 35 characters | Confirm equal values with the bank |
:86: information to the account owner | TxDtls/RmtInf, RltdPties, AddtlNtryInf | Free text against structured data |
:62F: closing balance | Bal with code CLBD | None for a single page |
:64: closing available balance | Bal with code CLAV | Bilateral in the Lynx guideline |
The fixture day in MT940, references cut to 16 characters:
:20:STMT20270315-01
:25:ACCTTEST0001
:28C:74/1
:60F:C270315EUR10000,00
:61:2703150315C1816,90NTRFPO-20270312-0004//BNK0315000001
:86:PAYOUT PO-20270312-000412
:61:2703150315D2500,00NTRFPMT-RUN-20270315//BNK0315000002
:86:BATCH 3 PAYMENTS PMT-RUN-20270315-01
:61:2703150315RD850,50NTRFINV-7782//BNK0315000003
:86:RETURN INV-7782
:62F:C270315EUR10167,40
:64:C270315EUR10167,40Pitfalls that pass a single-file test
Truncated references collide. Danske Bank's field 61 layout gives the reference for the account owner 16 characters and delivers any excess characters in the next subfield. PMT-RUN-20270315-01 and PMT-RUN-20270315-02 both become PMT-RUN-20270315, so a matcher keyed on that reference can assign the second payment run's debit to the first one and mark the wrong invoices paid. After cutover, items still open from the MT940 period carry the 16-character key, new entries carry the full 19-character PmtInfId, and an exact-match rule leaves both sides open. Test two same-day runs sharing the first 16 characters, and one open item across the cutover date.
Entry dates without a year. Field 61 carries the entry date as MMDD. The line :61:2712310103D410,00NTRFINV-9001//BNK1231000007 has a value date of 31 December 2027 and an entry date of 3 January. A parser that takes the year from the value date books the entry on 3 January 2027, a year early. Derive the year from the statement period, and keep a year-end fixture.
Gross and net batch booking. The Dutch guideline describes two bank practices for rejected payments in a batch of ten EUR 10 transfers with two rejects. Under the gross method the batch entry shows EUR 100 debit and the two rejects come back as separate credits. Under the net method the batch entry shows EUR 80 and the rejects arrive only in a pain.002 status report. A matcher built for one method fails on the other: it waits for credits that never come, or finds the batch EUR 20 short. Keep a fixture for each method your banks use.
Intermediate page balances. In the CBPR+ guideline, rules R12 to R15 require every page of a paginated statement to carry one OPBD and one CLBD, with sub-type INTM on the balances between pages. A loader that stores every CLBD as the day's closing balance records a mid-statement figure. Store only balances without INTM and check the balance equation per page.
Test matrix
| Fixture | Expected result |
|---|---|
| Statement from step 2 unchanged | Checker passes; INV-7781 and INV-7783 stay paid; INV-7782 reopens |
One TxDtls removed from the batch | NbOfTxs failure and a sum failure on the batch entry |
Return booked as DBIT with RvslInd true | Balance failure; return not linked |
Batch without TxDtls | Entry matched on PmtInfId and total |
| Sequence number skips one statement | Sequence failure; missing day requested from the bank |
| Two runs sharing the first 16 characters, MT940 feed | Each run matched to its own invoices |
Year-end :61: line with MMDD in January | Booking date in the new year |
| Gross batch with two rejects as credits | Rejected invoices reopen |
Three-page statement with INTM balances | One daily closing balance stored |
| Same day in MT940 and camt.053 | Identical ledger postings from both feeds |
The ledger and reconciliation testing service delivers these fixtures and checks as reconciliation test scenarios against your bank files, and runs them again whenever a bank changes the camt.053 version or its usage guideline.
Related
Segments
Standards
Guides
Questions
What is camt.053?
Scope your testing
Send FintechAssuranceLabs your product, segment, money flows, and the standards that need evidence, and get a proposed test scope with its deliverables.
Scope your testing