SEPA Files Break on 15 November 2026. A Type-Safe Way to Be Ready.
On 15 November 2026 the EPC stops accepting unstructured addresses in SEPA payment files. If you generate pain.001 or pain.008 XML, here's what changes, and a tested, type-safe TypeScript library that already targets the new format.
There is a date sitting in the SEPA rulebook that most application code has never heard of.
On 15 November 2026, the European Payments Council and SWIFT stop accepting fully unstructured postal addresses in payment messages. After that weekend, a payment file that crams the debtor’s address into free-text lines is not “discouraged” or “deprecated.” It gets rejected, surcharged, or quietly fails straight-through processing at the bank. The address has to be structured: separate elements for town, post code, country.
That date moved, by the way. It used to be 22 November. The EPC pulled it forward a week to line up with the SWIFT Standards MX release weekend, so anyone who pinned the old date now has one week less than they planned.
If your system writes SEPA files, this is a format migration with a hard deadline. And I think most teams will discover it the week a bank rejects a batch.
What actually changes
Two things, and they travel together.
The message versions. SEPA is moving to the 2019 ISO 20022 message set. For the formats most applications touch, that means pain.001.001.09 for credit transfers and pain.008.001.08 for direct debits. The older versions you may be emitting today are on their way out.
The address. This is the part with teeth. The modern messages carry a structured PstlAdr block. The unstructured variant, the one where you dump a name and two address lines and hope, is what gets banned on 15 November 2026. Many banks already reject it ahead of the deadline.
So the work is not cosmetic. You are changing the schema you emit, and you are changing how one of the most error-prone fields in the whole document is structured. Under a deadline. Against a bank that will not tell you what is wrong, it will just say no.
Why hand-rolling this is the trap
I know the instinct, because I had it. SEPA XML looks like “just XML.” You open the spec, you see angle brackets, you think you will template your way out in an afternoon.
Then you meet the details. IBAN check digits (mod-97). The EPC character set, which is narrower than UTF-8 and silently corrupts files if you let an accented name through. The control sum that has to be the exact integer total of every transaction, computed without a single floating-point rounding error. Identifier rules that forbid a leading or trailing slash. Per-transaction amount caps. And now a structured address type with per-variant rules, where the German DK schemas accept only a subset and reject the rest.
Here is the governing fact that makes this domain different from most: a subtly wrong payment file is worse than no file at all. A file that fails loudly costs you an afternoon. A file that passes your tests, passes the bank’s first check, and then moves money to the wrong place, or double-collects a direct debit, costs you something you cannot easily take back.
That is a bad place to be improvising under a deadline.
So I built the library I wanted
The decision to build rather than wait is one I had made before. A few months earlier, a blocked TypeScript migration led to building the @codewithagents OpenAPI toolchain from scratch. Same pattern: dependency gap, Claude as co-builder, published to npm.
It is called sepa-xml-ts. It is a TypeScript library that parses, writes, and validates ISO 20022 SEPA files behind a typed model that hides the XML. It is MIT-licensed, on npm, and as of today it is at 1.0. The docs, with every field and variant, are at sepa.codewithagents.de.
The design rule is the one above: correctness is not a feature, it is the whole product. So it is enforced, not hoped for.
Every file the library generates is validated against the official EPC / ISO 20022 XSD, in CI, on every commit. Not against my idea of the schema. Against the real one. The way I trust that claim is a property test: generate any valid model, write it to XML, and assert the output validates against the actual XSD. Two hundred random models per run for credit transfers, two hundred more for direct debits, plus two hundred round-trip checks each (model to XML to parse, deep-equal back to the original). That suite is the reason I sleep at night about output validity, and it is the reason the test count sits at 671 passing rather than a dozen happy-path examples.
That oracle has already earned its place. One reference field in the structured remittance block is typed in the XSD as an enum, not free text. A plausible-looking string model would have emitted a file the bank’s schema rejects. No amount of reading the spec caught it. The property test failed within the first handful of runs, handed me the exact counterexample, and the fix followed in minutes. That is the difference between “looks right” and “verified against the standard.”
And the deadline-relevant part: structured addresses are built in. The optional address on each party is emitted as a structured PstlAdr for every write variant. The modern pain.001.001.09 and pain.008.001.08 messages carry the full field set. The format the deadline is forcing everyone toward is the format the library already targets.
Writing a credit transfer looks like this:
import { writeCreditTransfer, validateCreditTransfer } from "sepa-xml-ts";
// validateCreditTransfer() returns a typed result instead of throwing
const result = validateCreditTransfer(doc);
if (!result.ok) {
console.error(result.errors);
} else {
const xml = writeCreditTransfer(result.data);
console.log(xml); // <Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09">...
}
You build a typed model. The library validates the business rules, computes NbOfTxs and CtrlSum with exact integer arithmetic, and returns a pain.001.001.09 string. It cannot emit a structurally invalid file. Parsing goes the other way and auto-detects the message type.
NOTE
The structured-address requirement above is the EPC’s, and it is real, but the legal and operational details of your migration depend on your banks and your country. Validate your own output against your bank’s test environment well before November. This library gets you a correct, schema-valid file. It does not replace your bank’s acceptance testing.
What it does not do
Promotional posts tend to skip this part. I would rather you know the edges before you install it.
sepa-xml-ts is a payment file library. It produces, reads, and validates ISO 20022 SEPA XML. It does not talk to banks. No EBICS, no FinTS, no transmission. It does not do pain.002 status reports or camt statements. It is EUR and SEPA only, by design.
National write variants are demand-driven on purpose. A flavor only ships alongside that schema’s official XSD and golden samples, because, again, a wrong flavor is worse than none. Today it covers the modern credit-transfer and direct-debit messages, the legacy pain.001.001.03, and the German DK variants, with read support for an older coexistence direct debit. If you need Swiss .ch or another national flavor, that is an issue, not a surprise.
Five and a half months
The deadline is real, the format change is concrete, and the address rule is the kind of thing that does not show up in your tests, it shows up in a rejected batch.
If you write SEPA files in a TypeScript or Node codebase, the move I would make now is small: generate your output with a library that already targets pain.001.001.09 and pain.008.001.08, emits structured addresses, and proves every file against the official XSD. Then validate that output against your bank’s sandbox while there is still time on the clock.
The library is on npm as sepa-xml-ts, the source is on GitHub, and the full docs, with every field and variant, live at sepa.codewithagents.de.
It is open source and MIT-licensed. Give it a try, and if something is missing or wrong for your bank, open an issue. That is exactly the feedback that decides what ships next.