Reconciliation is the closed-loop step. You emit pain.001 with an EndToEndId, the bank settles the payment, and an inbound camt.053 lands in your bank-account inbox with — you hope — the same EndToEndId echoed back. The match drives the cash-application step: "invoice 12345 has been paid". Without it, your AR ages, your customer keeps the past-due invoice on the side, and your treasury team writes a manual matching spreadsheet on Thursdays.
The problem: the reconciliation key — your invoice id, order id, or payroll-line id — does not always come back where it was sent. Six carrier fields, one true reference, bank-specific routing rules.
The six carriers
Inside a camt.053 <Ntry> / <TxDtls> block, the customer's reference can live in any of six places:
- <TxDtls>/<Refs>/<EndToEndId> — primary ISO 20022 originator reference.
- <TxDtls>/<Refs>/<InstrId> — the originator's second reference, often used as the durable bank-side handle.
- <TxDtls>/<RmtInf>/<Strd>/<CdtrRefInf>/<Ref> — structured creditor reference (SCOR or QRR).
- <TxDtls>/<RmtInf>/<Ustrd> — unstructured free-text remittance.
- <TxDtls>/<Refs>/<AcctSvcrRef> — the bank's own internal transaction reference.
- <TxDtls>/.../<SplmtryData> — vendor-specific extension blob.
iso-compliant's recon-key-extract module walks all six and emits one CandidateKey per non-empty carrier, tagged with confidence (high / medium / low). The KeyCarrier union at apps/api/src/lib/recon-key-extract.ts is the type-level declaration.
Pattern 1 — Structured reference round-trip
You issue an invoice with a SCOR reference (ISO 11649 — RF + 2 check digits + your invoice id). The customer pays. The reference travels in pain.001 <RmtInf>/<Strd>/<CdtrRefInf>/<Ref> and is echoed back in camt.053 <StrdCdtrRef>. This is the cleanest path — high-confidence, structured, mod-97 protected against corruption.
Works well for: Swiss QR-bill (QRR / SCOR), SEPA SCT with structured remittance, B2B invoicing where you control the reference format on the invoice you send.
Pattern 2 — EndToEndId round-trip
You emit pain.001 with EndToEndId = your invoice id (max 35 chars). The bank preserves it through the SCT scheme. It comes back in camt.053 <TxDtls>/<Refs>/<EndToEndId>.
Works well for: outbound payroll, supplier payments, intra-corporate transfers — anywhere you initiate and have control over both ends. Caveat: some intermediated cross-border MX flows strip EndToEndId; always emit a secondary key as a backup.
Pattern 3 — Ustrd free-text regex
A customer initiates an SCT instant transfer from their banking app and types your invoice id into the "remittance" field. The field maps to camt.053 <Ustrd> — pure free text. Your reconciliation engine has to regex-match.
iso-compliant's default regex is /[A-Z]{2,}[-_]?\d{4,}[-_]?\d+/ — catches INV-2026-001234, UBS-2026-001, ORD20260603_42. Override via ustrd_key_pattern for tenants whose invoice ids do not match the default shape.
Works less well: lower confidence than structured, susceptible to user typos, prone to false positives if the customer types something that matches the regex but is not your reference.
Pattern 4 — AcctSvcrRef as durable handle
PostFinance and Citi WorldLink promote AcctSvcrRef as the durable reconciliation handle on certain flows. The customer-side strategy: catch AcctSvcrRef on inbound, store it as the bank-side handle for any post-settlement recall / refund / dispute conversation.
AcctSvcrRef is NOT your invoice id — it is the bank's internal reference. You match on it indirectly: the same AcctSvcrRef will appear on every subsequent message about the same payment, so a reconciliation engine can chain pain.002 (status report) and a subsequent camt.054 (intraday notification) by AcctSvcrRef even when the original EndToEndId is lost.
Pattern 5 — N-to-1 batched booking
A bank may "batch-book" multiple inbound payments under one camt.053 <Ntry>. The single <Ntry> carries one aggregate amount and N <TxDtls> children, one per original payment. The reconciliation engine has to walk the N <TxDtls> separately.
The iso-compliant camt.053 parser handles this transparently — every <Ntry>/<TxDtls> becomes an "entry" in the response, regardless of whether it was atomically booked or batched. The downstream matching engine treats batched and atomic entries identically.
Closing the loop
POST /v1/reconcile takes the candidate-key list from the parse step plus a list of expected payments (your open invoices, payroll lines, supplier-payment commitments) and produces per-entry matching outcomes: matched / ambiguous / unmatched / out_of_band. The matching score is (carrier confidence × value-equality × amount-tolerance) — see apps/api/src/lib/reconcile.ts.
The output drives your cash-application step. A matched entry triggers the invoice-paid transition in your ledger; an ambiguous entry goes to a human-review queue with the top three candidates pre-ranked; an out_of_band entry (typically bank fee, interest credit, scheme reversal) is captured as a "GL adjustment" item.
This is the durable revenue story. The outbound pain.001 generation is the wedge — once a customer is shipping pain.001 through iso-compliant, the natural next step is to also reconcile the inbound camt.053 through iso-compliant. The reconciliation revenue compounds because the matching engine improves with every customer, every bank, every rail.