← All writing2023.04.07 · Payments

Turning CODA Bank Files into an Auditable Reconciliation Pipeline

How a Belgian coded bank statement moves from Isabel or secure file transfer through fixed-width parsing, matching, review and idempotent ledger posting.

Card payments are event-driven and near real time. B2B bank transfers taught me a different kind of payment engineering: a bank can be correct while our application knows nothing until a statement file arrives.

The system I worked on provided a financial solution between businesses. Money moved through bank accounts, while our application had to identify which companies were involved and update the corresponding internal balance or open item. Originally, an operator downloaded CODA files, read the credits and debits, then entered the result into our system by hand.

Automating that process was not “read each line and update a balance.” It was building an evidence-preserving reconciliation pipeline.

The rest combines the public CODA format with general design guidance for a pipeline I would build now. It is not a particular bank’s internal operating procedure and does not disclose customer accounts, transaction samples or connection settings.

What CODA is — and is not

CODA is the Belgian interbank standard for coded electronic account statements, published by Febelfin. It reports booked account movements. It is not a payment initiation API and it does not make a transfer happen.

Delivery depends on the agreement with the bank. A business may download reporting files through the Isabel desktop workflow, receive them through secure EDI/FTP, or use another bank channel. Files can be available per bank working day when movements occur; “once or several times a month” describes one operational setup, not a rule of the CODA format.

The format is fixed width and record based. Header, account information, old and new balances, movements, additional descriptions and trailer data are different record types. One logical movement can span continuation records. Treating every physical line as an independent transaction will eventually corrupt data.

Credit and debit are relative to the statement account

The words credit and debit are easy to misread. They describe the movement from the perspective of the bank account in the statement:

  • a credit increases that bank account;
  • a debit decreases that bank account.

They do not directly mean “top up customer A” or “charge company B.” That business meaning must be derived from the account, counterparty, structured communication, amount, currency, dates and the internal open items.

A counterparty company name is useful evidence, but it is not a safe unique key. Names change, get truncated, include punctuation, or describe a payment agent rather than the actual customer. Structured references and known bank accounts are usually stronger, but even they need conflict handling.

The pipeline I would build now

CODA ingestion, parsing, matching, manual review and ledger posting pipeline

1. Preserve source evidence within a controlled boundary

Whether a human downloads a file or a scheduled job receives it over a secure channel, first establish authorization to receive and retain it. For files that need to be kept as reconciliation evidence, preserve the unmodified raw bytes alongside the source, receive time, account identifier, filename, checksum and parser version. This lets a reviewer reproduce the parsing and matching later.

The file may contain account numbers, counterparty names, amounts and free-text references. It is protected business data, not an ordinary debugging attachment. The archive should be encrypted, access-restricted and access-audited, with retention and deletion rules based on applicable law, contracts and business purpose. Required evidence must not be deleted prematurely either. Keeping an original unchanged does not mean retaining it forever.

Keep that archive separate from logs. Ordinary logs should contain an import ID, processing outcome and error category, letting an authorized reviewer retrieve the source when needed. Use synthetic data for tests and public examples; do not put bank files in public repositories, issues or unapproved AI tools. These are recommended data protection conditions, not a certification of the earlier system’s compliance. OWASP: Logging Cheat Sheet

File-level idempotency matters. A user can download the same statement twice; a bank can re-deliver it; an SFTP poller can see the same file again. A safe identity combines the account, statement sequence and bank-provided identifiers, with the file hash as an additional guard. Do not rely on filename alone.

2. Validate the statement before reading movements

Reject or quarantine a file when the account is unexpected, the statement sequence has a gap, balances do not connect, the format version is unsupported, or mandatory trailer checks fail. An empty statement can still be meaningful for sequence continuity.

The old balance, sum of movements and new balance should reconcile according to the format rules. Passing this check does not prove every customer mapping is correct; it proves the statement was assembled consistently.

3. Parse records, then normalize movements

The parser first assembles all records belonging to one logical movement. Only then does it create a provider-neutral object such as:

BankMovement
  source_account
  statement_sequence
  movement_sequence
  booking_date / value_date
  direction / amount / currency
  counterparty_name / counterparty_iban
  structured_reference / free_text
  transaction_family / bank_reference
  raw_record_ids

This boundary is important. CODA-specific positions and codes belong in the parser; matching and ledger code should operate on normalized movements.

Febelfin explicitly warns that some bank references can vary by bank and can change without notice. A field being present does not automatically make it a durable business key.

4. Match with evidence, not optimism

I would rank matching evidence roughly like this:

Evidence Strength Caveat
Unique structured payment communication mapped to an open item High Must still agree on amount/currency and not already be consumed
Known counterparty IBAN + exact amount + compatible date/open item Medium-high Shared treasury accounts and batch payments exist
End-to-end or internal reference in free text Medium Normalization and truncation can alter it
Company name + amount Low Names are not stable or unique
Amount alone Never enough Collisions are routine

The output is not only matched or unmatched. It should include candidate IDs, matched rules, confidence, rejected alternatives and a reason code.

Only a unique high-confidence result may post automatically. Unknown counterparties, one payment covering several invoices, split payments, fees deducted by the bank, currency differences, reversals and multiple equal candidates belong in a manual review queue.

5. Post an idempotent journal entry

The parser should not directly mutate customer.balance. It should ask the ledger to post a journal entry identified by the bank movement’s stable source key. The ledger enforces that the same source cannot post twice and links the journal back to the CODA movement, source file and matching decision.

If a reviewer changes the match, do not erase history. Reverse the incorrect journal and create a corrected one. That is more work than updating a row, but it is what makes the system explainable to finance, support and auditors.

Manual review is part of the design

Automation is not the absence of humans. It is making sure people see only the cases where judgment adds value.

A useful review screen shows the raw bank description beside the normalized movement, the proposed company and open item, competing candidates, previous payments from the same IBAN, and exactly which rules produced the score. Review actions need a user, timestamp and reason.

The feedback can improve future deterministic rules, but financial posting should not silently change because a fuzzy model became more confident.

Edge cases worth remembering

  • the same file or movement arrives twice;
  • a sequence number is missing, or an empty statement fills the sequence;
  • one movement spans several continuation records;
  • booking date and value date differ;
  • one transfer pays several invoices, or several transfers pay one invoice;
  • the bank deducts fees, so the received amount differs from the invoice;
  • structured communication is absent or malformed;
  • counterparty name is truncated, transliterated or represents an intermediary;
  • a transfer is reversed or corrected in a later statement;
  • foreign currency and FX cause amount differences;
  • a movement is aggregated, with details reported elsewhere;
  • the parser is redeployed and reprocesses historical files;
  • every manual correction remains linked to an auditable reversal and replacement entry.

Metrics should cover more than “files processed”: sequence gaps, duplicate files, parser rejects, unmatched rate, ambiguous rate, auto-match precision, manual-review age, time from bank booking to ledger posting, and balance reconciliation differences.

CODA is also a reminder that payment and settlement are different systems

The real-time PSP flow and the CODA batch flow meet at reconciliation. A PSP can say a card payment was captured, but settlement reporting explains what was actually paid out. A B2B transfer can be booked at the bank before our application recognizes its business meaning.

The reliable architecture keeps three layers separate:

  1. External evidence — PSP events, status APIs, bank statements and raw files.
  2. Normalized financial events — provider-neutral payments and bank movements.
  3. Internal ledger decisions — which account or open item changes, with an audit trail.

That separation is what lets a system survive a new PSP, another bank format, delayed files and human corrections without rewriting its financial history.

References