@fin.cx/sepa
SEPA credit transfer files as German banks take them: pain.001.001.09 after the rules of the Deutsche Kreditwirtschaft (DK) in Anlage 3 des DFÜ-Abkommens, valid against the DK's schema pain.001.001.09_GBIC_5. Any batch of transfers from one account goes to the bank as one file, which the person uploads to their online banking or sends over EBICS: a supplier run, expense reimbursements, a tax payment, a payroll's net pay. The purpose of each transfer is the caller's choice of ISO 20022 code.
It also reads such files, pain.001.001.09 and the older pain.001.001.03: a file another program wrote, such as the payment list of a payroll program, comes back as its blocks and payments, with every value checked.
The package is pure: it reads no clock, keeps no state and writes nothing anywhere. The file goes back to the caller as text and as UTF-8 bytes.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm install @fin.cx/sepa
Usage
import { writeCreditTransfer, SepaError } from '@fin.cx/sepa';
const file = writeCreditTransfer({
messageId: 'ACME-2026-03-27-1', // new for every file: a bank refuses a second file with the same id
createdAt: new Date(),
debtor: { name: 'Acme GmbH', iban: 'DE89 3704 0044 0532 0130 00' }, // the BIC is optional
executionDate: '2026-03-27',
payments: [
{
endToEndId: 'INV-2026-0815', // travels to the creditor and comes back with a return
amountCents: 119_000,
creditor: { name: 'Bürobedarf Beispiel GmbH', iban: 'DE02120300000000202051' },
remittance: { unstructured: 'Rechnung 2026-0815' },
},
{
endToEndId: 'EXP-2026-03-0007',
amountCents: 4_350,
creditor: { name: 'Jonas Müller', iban: 'DE44500105175407324931' },
remittance: { unstructured: 'Auslagen 03/2026' },
},
],
});
file.xml; // '<?xml version="1.0" encoding="UTF-8"?>\n<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09">…'
file.bytes; // the same, UTF-8 without a byte order mark
file.controlSumCents; // 123350
file.numberOfTransactions; // 2
Codes are the caller's choice. A payroll's net pay, for example, names its category for the debtor's bank and each payment's purpose for the creditor's bank:
const payroll = writeCreditTransfer({
messageId: 'ACME-PAYROLL-2026-03',
createdAt: new Date(),
debtor: { name: 'Acme GmbH', iban: 'DE89370400440532013000' },
executionDate: '2026-03-27',
categoryPurpose: 'SALA', // for the debtor's bank: a payroll
payments: [
{
endToEndId: 'PAYROLL-2026-03-0001',
amountCents: 298_017,
creditor: { name: 'Jonas Müller', iban: 'DE44500105175407324931' },
purpose: 'SALA', // the employee's bank books it as wages
remittance: { unstructured: 'Lohn/Gehalt 03/2026' },
},
{
endToEndId: 'PAYROLL-2026-03-0002',
amountCents: 4_000,
creditor: { name: 'Bausparkasse Beispiel AG', iban: 'DE02120300000000202051' },
ultimateCreditorName: 'Jonas Müller',
purpose: 'CBFF', // vermögenswirksame Leistungen
remittance: { unstructured: '006/VL-4711' }, // "XXJ/contract number", as the DK asks for VL
},
],
});
What the file holds
- One group header and one payment information block: one debtor account, one execution day, payment method
TRF, service levelSEPA, chargesSLEV, batch booking unlessbatchBooking: false(which a bank grants only by agreement). - The number of transactions and the control sum in both, the amounts in euros with two decimals.
- The debtor's bank by its BIC, or
NOTPROVIDEDwhere none is given; the creditor's bank by its BIC where one is given (the IBAN alone routes a SEPA payment). - The category purpose of the file (
PmtTpInf/CtgyPurp), stated once at the payment information level as the DK recommends. It is not shown on statements. - Per payment: the end-to-end id, the amount, the creditor with an optional postal address, the ultimate creditor's name, the purpose code (
Purp) and either an unstructured remittance text of up to 140 characters or an ISO 11649 creditor reference (RF…, checked through@fin.cx/calculation).
The purpose code is what the creditor's bank shows. The DK's mapping of business transaction codes (Anlage 3, Anhang 1) books SALA, BONU, PENS, PAYR and SPSP as code 153 (wages, salaries, pensions), CBFF as 154 (vermögenswirksame Leistungen) and GOVT, SSBE and BENE as 156 (public treasuries). Codes are checked to be four capital letters; whether a code is on the ISO 20022 external code list is not checked.
What is refused
Every value is checked before anything is written. The first one a German bank or the DK's schema would refuse throws a SepaError with a code and the field it is about (payments[2].creditor.iban):
| Code | Refused |
|---|---|
invalid_identifier |
A message, payment information or end-to-end id that is empty, over 35 characters, outside A–Z a–z 0–9 + ? / - : ( ) . , ' and the blank, or that starts or ends with / or contains // |
duplicate_end_to_end_id |
Two payments with the same end-to-end id (NOTPROVIDED may repeat) |
invalid_text |
A name (70), remittance text (140) or address field (street 70, building number 16, post code 16, town 35, two lines of 70) that is empty, too long or outside the SEPA character set; a country that is not two capital letters |
invalid_iban |
An IBAN that is not of the form or whose check digits do not hold (ISO 7064 MOD 97-10) |
invalid_bic |
A BIC that is not 8 or 11 characters of its form |
invalid_amount |
An amount that is not whole cents from 0.01 to 999 999 999.99 euros |
invalid_date |
An execution day that is no calendar day YYYY-MM-DD; a creation time that is no date |
invalid_code |
A purpose or category purpose that is not four capital letters |
invalid_remittance |
A creditor reference that is not ISO 11649 with valid check digits |
invalid_payments |
No payments, or more than 9 999 999 |
invalid_document |
Reading only: see Reading a file |
invalid_totals |
Reading only: see Reading a file |
IBANs and BICs are taken as typed or printed. Blanks are removed and letters put in capitals; nothing else is changed.
The SEPA character set
German banks take the "Latin character set" of the EPC: the digits, A–Z, a–z, ' : ? , - ( + . ) / and the blank. They also take what the DK added: Ä Ö Ü ä ö ü ß & * $ % (Anlage 3 V3.9, chapter 2.1). isSepaText and nonSepaCharactersOf test a text against this set.
A name like Antonín Dvořák is refused rather than changed. toSepaText brings a text into the set and reports every character it changed, so the caller decides whether the result may be sent:
import { toSepaText } from '@fin.cx/sepa';
toSepaText('Antonín Dvořák – „Ærø“');
// { text: "Antonin Dvorak - 'AEro'", replaced: [{ offset: 5, character: 'í', replacement: 'i' }, …] }
The conversion rules are:
- a letter with a diacritic becomes the letter without it;
Æ Œ Ø Ł Đ Ð Þ ẞ ıand their lower-case forms becomeAE OE O L D D TH SS i;- typographic quotes become
'and dashes become-; - white space becomes a blank;
- anything else becomes
., the replacement the DK gives for*,$and%where a bank cannot show them.
These are this package's rules, not the EPC's conversion table.
Reading a file
readCreditTransfer takes a file as text or as bytes and returns what it holds:
import { readCreditTransfer } from '@fin.cx/sepa';
const document = readCreditTransfer(uploadedBytes); // a string or a Uint8Array
document.version; // 'pain.001.001.09' or 'pain.001.001.03'
document.messageId; // 'ACME-PAYROLL-2026-03'
document.createdAt; // '2026-03-24T09:30:00.000Z', as written
document.controlSumCents; // 814362
for (const block of document.paymentInformations) {
block.debtor; // { name: 'Acme GmbH', iban: 'DE89370400440532013000' }
block.executionDate; // '2026-03-27'
block.categoryPurpose; // 'SALA'
for (const payment of block.payments) {
payment.endToEndId; // 'PAYROLL-2026-03-0002'
payment.amountCents; // 298017
payment.creditor; // { name: 'Jonas Müller', iban: 'DE44500105175407324931', bic: 'INGADEFFXXX' }
payment.purpose; // 'SALA'
payment.remittance; // { unstructured: 'Lohn/Gehalt 03.2026 Müller & Co' }
}
}
A payment has the shape writeCreditTransfer takes for one, without a postal address. It may also carry what a file names beside it: an instruction id, its own category purpose and its local instrument. A block may carry an execution time, its batch booking and its local instrument, such as INST for instant credit transfers.
A file is read as the DK's schemas pain.001.001.03_GBIC_3 and pain.001.001.09_GBIC_5 describe it. Within those rules the reader takes what a program may write:
- any namespace prefix;
- comments, processing instructions and
xsi:schemaLocation; - a byte order mark;
- white space around numbers, dates and booleans where XML Schema collapses it.
Values are checked as the writer checks them:
- identifiers, codes, amounts in euros and BICs;
- IBANs and creditor references with their check digits;
- the numbers of transactions and the control sums against the payments.
Names and remittance texts are returned as written, in any character, within their lengths. IBANs and creditor references are returned in their electronic form.
A file is refused with a SepaError, whose field is a path into the result (paymentInformations[0].payments[2].creditor.iban), or empty for a file refused as a whole. Besides the codes above:
| Code | Refused |
|---|---|
invalid_document |
A file that is no UTF-8 text or declares another encoding; that is no well-formed XML or holds a document type declaration; that is neither message. Also a missing element the message requires, an element more often than allowed, and an element or attribute the schema does not admit in its place. Also a file that is no SEPA credit transfer: another payment method, service level or charge bearer. |
invalid_totals |
A number of transactions or a control sum that is malformed or does not agree with the payments |
Not read, though a file may hold them: postal addresses, the parties' organisation and private ids, ultimate debtors, account proxies, the priority of a block, proprietary category purposes, and the order of the elements. Two payments with the same end-to-end id are read, not refused.
Other exports
isSepaIdentifiertests an id against the DK's identifier rules.normalizeIban,isIbanandnormalizeBicgive the electronic form of an IBAN or BIC as typed or printed, ornull.PAIN_001_NAMESPACEandPAIN_001_SCHEMAname the message and the DK schema a file is written for.TSepaCreditTransferVersion,ISepaCreditTransferDocument,ISepaPaymentInformationandISepaReadPaymenttype whatreadCreditTransferreturns.
Which format, and why
- Anlage 3 V3.9 of the DFÜ-Abkommen (12 March 2025) took effect for SEPA on 5 October 2025. It specifies credit transfers as
pain.001.001.09(ISO 20022 version 2019) with the DK's schemapain.001.001.09_GBIC_5.xsd. That schema introduced the hybrid address and requires the currency euro. - Anlage 3 V26.11 (9 April 2026, in force from 15 November 2026) keeps the same message and schema for SEPA credit transfers.
- Older versions: V3.9 names
pain.001.001.03_GBIC_4as the previous schema for SEPA credit transfers (chapter 2.4). A bank may still take it during a transition, by agreement. The DK's format life cycle (version 2.0 of 24 September 2026) lists the versions. This package writes only the current one. It readspain.001.001.03as well, after the DK's schemapain.001.001.03_GBIC_3(Anlage 3 V3.3 to V3.6), for files from programs that still write it.
Postal addresses follow the hybrid rule of V3.9: the town and the country always, street, building number and post code where known, and at most two free address lines beside them. The DK recommends leaving the debtor's address out.
The tests validate every file against the DK's schema with libxml2 (xmllint-wasm). The schema itself checks less than the DK's rules. For example, it takes any four-character service level, while the writer always writes SEPA. The reader's tests read the DK's own examples for both versions, after validating them against their schemas.
The DK and SIZ GmbH reserve all rights to the schemas and examples, so they are not part of this repository:
test/dkdocuments.tsfetches them from ebics.de on the first run and caches them under.nogit/dk/;- it takes each only with the SHA-256 it had when it was read (2026-10-07 for the
GBIC_5schema, 2026-10-08 for the others); - the tests fail when a document cannot be fetched or has changed.
The DK's example for pain.001.001.03 carries the namespace of pain.001.001.09 over a body of pain.001.001.03 (its note says only the namespace changed). The reader refuses it as published, and the tests read it with the namespace of pain.001.001.03 restored.
Not covered
- Writing SEPA instant credit transfers (
LclInstrm INST, an execution time); the reader reads them. Direct debits (pain.008) and the payment status report (pain.002). - Writing more than one payment information block per file (one debtor account, one execution day per file); the reader reads any number.
- Structured addresses with fields beyond street, building number, post code, town, country and two lines.
- Organisation or private identifications of the parties, and ultimate debtors.
- Whether an IBAN has its country's length and whether the country takes part in SEPA. Only the form and the check digits are checked.
Sources
- DK, Anlage 3 des DFÜ-Abkommens "Spezifikation der Datenformate", V3.9 of 12 March 2025, chapter 2 (SEPA payment transactions) and 2.3 (simple types), and V26.11 of 9 April 2026 with its amendment history: ebics.de/de/datenformate
- DK, Anhang 1 zu Anlage 3, Mapping GVC ► ISO BTC, V3.9: ebics.de/de/datenformate/ergaenzende-dokumente
- DK, schema
pain.001.001.09_GBIC_5.xsdinDK-TVS_SEPA_GBIC_5zzglISO_Originale.zip, SHA-256d64da6e1553cf8dd38f98818fe3234fb79daec792d0a69483a1ab8e90dec8b8c: ebics.de/de/datenformate/ergaenzende-dokumente - DK, examples
pain.001.001.09.xml(SHA-2568a4dd9d04dee9286090cebc9263bad4b5ffdbfdfc8f21eccbe70c85d1c8df016) andpain.001.001.09Echtzeit.xml(SHA-256296eb291397c6e5327a06167d65983a659f3d20966f3dd382c03ea56e2b027e8) inXML-Beispiele_SEPA.zip: ebics.de/de/datenformate/ergaenzende-dokumente - DK, schema
pain.001.001.03_GBIC_3.xsd(SHA-2564116195dd80c84821b82287dc96ad066a4adf5a54d74ae3f80c0873d7da6e97c) and examplepain.001.001.03.xml(SHA-256dbd77707100f98bb9f925a736075f71a987bf316c5d4bd8e22a2829e59ce5b9f) inAnlage3_V3.3-3.6_Beispiele_Schemadateien_GBIC3.zip: ebics.de/de/datenformate/archiv - DK, SEPA-Formatversionen im Verfahren "DFÜ mit Kunden", version 2.0 of 24 September 2026: ebics.de/de/datenformate/format-lifecycle
Unconfirmed:
- That
SALAis on the ISO 20022 list of category purpose codes rests on secondary sources: banks' implementation guides and the DK's own use ofSALAas a purpose code. The ISO code set itself could not be fetched. - The EPC's character conversion table (EPC217-08) was not read.
- The reader is tested against the DK's examples and against files this package writes, not yet against files that payroll or accounting programs export.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.