DEV Community

Zero Lopp Labs
Zero Lopp Labs

Posted on

SWIFT Is Killing MT940 — Here's How to Future-Proof Your Bank Statement Pipeline

On November 22, 2025, SWIFT pulled the plug on legacy MT payment messages. Cross-border payments now run exclusively on ISO 20022's MX format.

MT940 — the bank account statement format your reconciliation pipeline probably depends on — wasn't part of that first wave. It's a reporting message, not a payment message. But it's next. SWIFT has formally deprecated MT940, stopped maintaining it, and announced that disincentives for continued usage are coming.

If your application parses bank statements, you need a migration plan. Here's what's actually happening, what the formats look like under the hood, and how to build a pipeline that handles both.


What MT940 Actually Looks Like

Before we talk about replacing MT940, let's look at what we're replacing. Here's a real MT940 statement:

:20:STMT2603230001
:25:NL91ABNA0417164300
:28C:15/1
:60F:C260322EUR1234,56
:61:2603220322D45,00NTRFNONREF//ACME-INV-2026-042
:86:999~00SEPA OVERBOEKING~20KENMERK: ACME-INV-2026-042
~21Acme Corp Ltd~22PAYMENT FOR SERVICES~23March 2026
~30DEUTDEDB~31DE89370400440532013000~32Acme Corp Ltd
~33Frankfurt
:62F:C260322EUR1189,56
Enter fullscreen mode Exit fullscreen mode

If you've never parsed this, here's a field-by-field breakdown:

Tag Name What's in it
:20: Transaction Reference Message identifier (max 16 chars)
:25: Account ID Your IBAN or account number (max 35 chars)
:28C: Statement Number Sequence number (15/1 = statement 15, page 1)
:60F: Opening Balance C = Credit, 260322 = 22 Mar 2026, EUR, 1234,56
:61: Statement Line Date + D/C + amount + type code + reference (max 80 chars)
:86: Information Free-text transaction details (up to 6 lines × 65 chars)
:62F: Closing Balance Same format as opening balance

The :86: field is where the real pain lives. Banks cram debtor names, payment references, creditor IBANs, and remittance information into a single text block. There is no universal standard for how this data is structured.

The ~ delimiters you see above? That's one bank's convention (common in SEPA countries). Other banks use / prefixes, ? codes, or just dump everything as plain text. You end up writing bank-specific regex patterns to extract structured data — and they break every time the bank changes their layout.

What CAMT.053 Looks Like Instead

Here's the same transaction in CAMT.053:

<Ntry>
  <Amt Ccy="EUR">45.00</Amt>
  <CdtDbtInd>DBIT</CdtDbtInd>
  <BkgDt><Dt>2026-03-22</Dt></BkgDt>
  <VlDt><Dt>2026-03-22</Dt></VlDt>
  <AcctSvcrRef>ACME-INV-2026-042</AcctSvcrRef>
  <NtryDtls>
    <TxDtls>
      <Refs>
        <EndToEndId>ACME-INV-2026-042</EndToEndId>
      </Refs>
      <RltdPties>
        <Cdtr>
          <Nm>Acme Corp Ltd</Nm>
          <PstlAdr>
            <TwnNm>Frankfurt</TwnNm>
          </PstlAdr>
        </Cdtr>
        <CdtrAcct>
          <Id><IBAN>DE89370400440532013000</IBAN></Id>
        </CdtrAcct>
      </RltdPties>
      <RltdAgts>
        <CdtrAgt>
          <FinInstnId>
            <BIC>DEUTDEDB</BIC>
          </FinInstnId>
        </CdtrAgt>
      </RltdAgts>
      <RmtInf>
        <Ustrd>PAYMENT FOR SERVICES March 2026</Ustrd>
      </RmtInf>
    </TxDtls>
  </NtryDtls>
</Ntry>
Enter fullscreen mode Exit fullscreen mode

No regex. No bank-specific parsing rules. The creditor name is in <Cdtr><Nm>. The IBAN is in <CdtrAcct><Id><IBAN>. The BIC is in <CdtrAgt><FinInstnId><BIC>. The remittance info has its own dedicated <RmtInf> element — with support for both unstructured text and structured sub-fields.

The full CAMT.053 document wraps everything in a clear hierarchy:

Document
  └─ BkToCstmrStmt (Bank-to-Customer Statement)
      ├─ GrpHdr       → Message ID, creation timestamp
      └─ Stmt         → One per account
          ├─ Acct      → IBAN, BIC, account name
          ├─ Bal[]     → OPBD, CLBD, CLAV, FWAV (all timestamped)
          └─ Ntry[]    → One per transaction
              └─ NtryDtls
                  └─ TxDtls → Refs, parties, agents, remittance
Enter fullscreen mode Exit fullscreen mode

Here's the technical comparison side-by-side:

Feature MT940 CAMT.053
Format Proprietary text (SWIFT FIN) XML (ISO 20022, XSD-validated)
Transaction details Packed into :61: (80 chars) + :86: (6×65 chars) Dedicated elements: Refs, RltdPties, RltdAgts, RmtInf
Remittance info Crammed into :86:, bank-specific delimiters Structured <RmtInf> with <Strd> sub-fields, unlimited length
Balance types Opening (:60F:) and Closing (:62F:) only OPBD, CLBD, CLAV, PRCD, FWAV — all with timestamps
Currency Single currency per statement Multi-currency with exchange rate details per transaction
Character set SWIFT X charset (A-Z, 0-9, basic punctuation — no accents) Full UTF-8 / Unicode
Date format YYMMDD (ambiguous century) ISO 8601: YYYY-MM-DD
Validation Manual — hope the parser handles edge cases XSD schema validation built-in

The Migration Timeline — What's Actually Happening

There's a lot of confusion around "SWIFT is killing MT940" because the migration is happening in waves. Here's the accurate picture:

What already happened (November 22, 2025):
SWIFT ended the MT/ISO 20022 coexistence period for payment messages. MT103 (credit transfers) and MT202 (institution transfers) are formally retired. All cross-border payments now use ISO 20022 MX format exclusively via the FINplus service.

What's happening now (2026):
MT940 and other reporting messages are deprecated and no longer maintained by SWIFT — but they haven't been withdrawn yet. J.P. Morgan's ISO 20022 FAQ puts it clearly: "Reporting and statement messages will not be immediately withdrawn from the FIN service. Although these message types are deprecated and no longer maintained by SWIFT, disincentives for their use will be introduced at a later date."

Banks are transitioning on their own timelines. J.P. Morgan has been accepting CAMT.052/053/054 since Q4 2024. Bank of America completed its Fedwire ISO 20022 implementation in July 2025. 44% of banks are behind schedule on their November 2026 milestones.

What's coming (2027+):
SWIFT's roadmap targets full MT message retirement, including enquiry and investigation messages (MT199/MT299 → camt.110/camt.111) by November 2027. The reporting messages (MT940 → CAMT.053) will follow.

The bottom line: MT940 still works today, but no one is maintaining or improving it. New features, new validation rules, new regulatory requirements — all of that goes into CAMT.053. Building on MT940 now is building on a dead-end.

Your Options as a Developer

Option 1: Build It Yourself

There are open-source libraries for individual formats:

  • mt940-rs (Rust) — MT940 parser
  • pycamt / camt_parser (Python) — CAMT.053 parsers
  • ofxstatement (Python) — OFX converter
  • Cmxl (Ruby) — MT940 parser with extensible design

The problem: you need multiple libraries plus glue code to normalize outputs into a common model. You need to handle bank-specific :86: field variations, character encoding edge cases (MT940's SWIFT X charset vs. UTF-8), date format conversions (YYMMDDYYYY-MM-DD), and amount parsing (comma vs. dot decimal separators).

For a single-format, single-bank integration, this works. For anything multi-bank or multi-format, you're signing up for ongoing maintenance as banks change their implementations.

Option 2: Enterprise Aggregators

Plaid (12,000+ institutions), Finicity (Mastercard), and Wise offer bank data APIs. But they solve a different problem — they connect to live bank accounts via OAuth. If you already have statement files (SFTP drops, email attachments, file exports), these platforms are overkill. They're also expensive and come with heavy onboarding.

Option 3: A Dedicated Conversion API

This is the gap. You have files in format A, you need them in format B. No bank connections, no OAuth flows, no aggregation. Just conversion — stateless, fast, spec-compliant.

That's what we're building.

Introducing FinConvert

FinConvert is a REST API that converts bank statement files between financial formats. One endpoint, any supported format in, any supported format out.

The core architecture uses a Universal Transaction Model: every input format is parsed and normalized into a single internal representation, then serialized to the requested output format. This means adding new formats requires N+M adapters, not N×M conversion paths.

Currently supported:

Direction Formats
Input MT940, CAMT.053 (OFX, BAI2, QIF coming soon)
Output CAMT.053, CSV, JSON, OFX (MT940 output coming soon)

Design principles:

  • Privacy-first — No files are stored. Conversion is stateless. Your financial data is processed in memory and discarded.
  • Spec-compliant — Output is validated against official SWIFT and ISO 20022 XSD schemas.
  • Fast — Sub-200ms average conversion time. Pure computation, no I/O bottleneck.
  • London-hosted — EU data residency for compliance-conscious teams.

Show Me the Code

Convert an MT940 file to structured JSON:

curl:

curl -X POST https://api.finconvert.dev/v1/convert \
  -H "Authorization: Bearer fc_your_api_key" \
  -F "file=@statement.mt940" \
  -F "output_format=json" \
  -o converted.json
Enter fullscreen mode Exit fullscreen mode

TypeScript:

async function convertStatement(file: File): Promise<ConvertedStatement> {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("output_format", "json");

  const response = await fetch("https://api.finconvert.dev/v1/convert", {
    method: "POST",
    headers: {
      Authorization: "Bearer fc_your_api_key",
    },
    body: formData,
  });

  if (!response.ok) {
    throw new Error(`Conversion failed: ${response.status}`);
  }

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

What you get back — structured, typed, no regex required:

{
  "statement": {
    "account": "NL91ABNA0417164300",
    "currency": "EUR",
    "opening_balance": 1234.56,
    "closing_balance": 1189.56,
    "statement_number": "15/1",
    "date": "2026-03-22"
  },
  "transactions": [
    {
      "date": "2026-03-22",
      "amount": -45.00,
      "currency": "EUR",
      "type": "DEBIT",
      "reference": "ACME-INV-2026-042",
      "description": "PAYMENT FOR SERVICES March 2026",
      "creditor": {
        "name": "Acme Corp Ltd",
        "iban": "DE89370400440532013000",
        "bic": "DEUTDEDB"
      }
    }
  ],
  "transaction_count": 1,
  "format_source": "MT940",
  "format_output": "JSON"
}
Enter fullscreen mode Exit fullscreen mode

That MT940 :86: field with bank-specific ~ delimiters? Parsed into clean, typed JSON. The creditor IBAN that was buried in ~31? Extracted into creditor.iban. No bank-specific logic on your end.

Pricing

Usage-based — you pay for what you convert:

Plan Price Conversions/month
Free $0 100
Pro $49/mo 5,000
Business $149/mo 50,000
Enterprise Custom Custom

The free tier is enough for testing and low-volume integrations. No credit card required.

What's Next

FinConvert is currently in early access. We're onboarding developers from the waitlist and expanding format support based on demand.

On the roadmap:

  • OFX, BAI2, and QIF input support
  • MT940 output (for systems that still require it during the transition)
  • Auto-format detection — upload any file, we figure out what it is
  • Batch conversion endpoint for bulk processing
  • Bank-specific :86: field profiles for higher extraction accuracy

If you're building anything that touches bank statement data — accounting software, reconciliation tools, fintech integrations, ERP connectors — the MT940 deprecation is real. It still works today, but the writing is on the wall: SWIFT has stopped maintaining it, banks are migrating, and every new feature goes into CAMT.053.

Join the waitlist at finconvert.dev to get early access and lock in free-tier usage during beta.


Built by Zero Loop Labs — the same team behind SealTrail (tamper-proof audit trails) and PDFForge (document generation API).

Top comments (0)