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
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>
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
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 (YYMMDD → YYYY-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
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();
}
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"
}
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)