Credit transfers (pain.001)
A credit transfer is a push payment: a debtor (payer) instructs their bank to send money to one or more creditors. The pain.001.001.09 message type is the modern ISO 20022 Credit Transfer Initiation message used across the SEPA zone.
The model
Section titled “The model”import { euros, type CreditTransferDocument, type PaymentBatch, type Transfer } from 'sepa-xml-ts'
const doc: CreditTransferDocument = { messageId: 'MSG-2026-0001', // GrpHdr/MsgId: unique message identifier createdAt: '2026-06-01T10:30:00Z', // GrpHdr/CreDtTm: ISO 8601 datetime initiatingParty: 'ACME GmbH', // GrpHdr/InitgPty/Nm
batches: [ { id: 'BATCH-001', // PmtInfId: unique within the message executionDate: '2026-06-03', // ReqdExctnDt/Dt: YYYY-MM-DD, never a datetime debtor: { name: 'ACME GmbH', // Dbtr/Nm iban: 'DE89370400440532013000', // DbtrAcct/Id/IBAN bic: 'COBADEFFXXX', // DbtrAgt/FinInstnId/BICFI (optional since 2016 rulebook) }, transfers: [ { endToEndId: 'INV-1001', // PmtId/EndToEndId amount: euros('123.45'), // Amt/InstdAmt Ccy="EUR" creditor: { name: 'Beispiel AG', // Cdtr/Nm iban: 'NL91ABNA0417164300', // CdtrAcct/Id/IBAN // bic is optional for pan-European SEPA transfers }, remittanceInfo: 'Invoice 1001', // RmtInf/Ustrd (optional) }, ], }, ],}Model types
Section titled “Model types”| Type | XSD element |
|---|---|
CreditTransferDocument | Document/CstmrCdtTrfInitn |
PaymentBatch | PmtInf |
Transfer | CdtTrfTxInf |
AccountParty | Dbtr or Cdtr with account and agent |
Money | Amt/InstdAmt |
Writing to XML
Section titled “Writing to XML”import { writeCreditTransfer, validateCreditTransfer } from 'sepa-xml-ts'
const result = validateCreditTransfer(doc)if (!result.ok) { console.error(result.errors)} else { const xml = writeCreditTransfer(result.data)}writeCreditTransfer validates the model internally before writing. It throws a ValidationError
if the model fails validation, so it cannot emit a structurally invalid file.
Derived fields
Section titled “Derived fields”The writer derives the following fields from the model. You do not supply them:
| Field | How it is derived |
|---|---|
GrpHdr/NbOfTxs | Total count of all transfers across all batches |
GrpHdr/CtrlSum | Sum of all transfer amounts (exact bigint addition) |
PmtInf/NbOfTxs | Count of transfers in this batch |
PmtInf/CtrlSum | Sum of transfer amounts in this batch |
PmtInf/PmtMtd | Always TRF |
PmtInf/PmtTpInf/SvcLvl/Cd | Always SEPA |
PmtInf/ChrgBr | Always SLEV |
Structured postal address
Section titled “Structured postal address”Each party (debtor and creditor) accepts an optional structured address, emitted as a
PstlAdr element. The EPC makes a structured address mandatory for the modern messages from
15 November 2026, and many banks reject unstructured addresses already.
const doc = { // ... batches: [ { // ... debtor: { name: 'Test Corp', iban: 'DE89370400440532013000', bic: 'COBADEFFXXX', address: { streetName: 'Rue de la Loi', buildingNumber: '16', postCode: '1000', townName: 'Brussels', country: 'BE', // 2-letter ISO country code }, }, // ... }, ],}All address fields are optional, but the object must have at least one field set, and country
must be a 2-letter uppercase code. The full field set is supported for pain.001.001.09 and
pain.008.001.08 (PostalAddress24) and for the legacy pain.001.001.03 (PostalAddress6). The German
DK variants (pain.001.003.03, pain.008.003.02) use the restricted PostalAddressSEPA type, which
supports only country and up to two addressLines: setting any other field (streetName,
buildingNumber, postCode, townName, countrySubDivision) throws a clear error for those
variants rather than dropping it silently.
Ultimate parties
Section titled “Ultimate parties”A transfer can name an ultimate debtor and/or ultimate creditor: the party on whose behalf the payment is really made, or who really receives it. This is common in factoring and payment-service-provider flows where the account holder differs from the economic party. Both are optional and set per transfer.
const transfer = { endToEndId: 'INV-1001', amount: euros('123.45'), creditor: { name: 'Factoring GmbH', iban: 'NL91ABNA0417164300' }, ultimateCreditor: { name: 'Beispiel AG' }, // UltmtCdtr/Nm: who the money is really for ultimateDebtor: { name: 'ACME Subsidiary' }, // UltmtDbtr/Nm: who really owes it}This first cut carries the name only (the common case). It is supported for pain.001.001.09 and
pain.008.001.08. The legacy and DK variants throw a clear error if an ultimate party is present,
rather than dropping it silently.
Remittance information
Section titled “Remittance information”A transfer carries remittance information in one of two mutually exclusive forms (the SEPA rulebook allows only one per transaction):
remittanceInfo(a free-text string) maps to the unstructuredRmtInf/Ustrd.structuredRemittancemaps to a structured creditor reference atRmtInf/Strd/CdtrRefInf.
const transfer = { endToEndId: 'INV-1001', amount: euros('123.45'), creditor: { name: 'Beispiel AG', iban: 'NL91ABNA0417164300' }, structuredRemittance: { creditorReference: 'RF18539007547034', // RmtInf/Strd/CdtrRefInf/Ref referenceType: 'SCOR', // CdtrRefInf/Tp/CdOrPrtry/Cd, defaults to SCOR issuer: 'ISO', // CdtrRefInf/Tp/Issr, optional },}referenceType is restricted to the ISO 20022 code list (RADM, RPIN, FXDR, DISP, PUOR,
SCOR), since the XSD types that element as an enumeration. For a SEPA structured creditor reference
it is SCOR. Setting both remittanceInfo and structuredRemittance on the same transfer is
rejected at validation.
The reference value is validated for SEPA charset and length. If it starts with the uppercase RF
prefix (declaring itself an ISO 11649 creditor reference), its check digits are validated with the
ISO 7064 MOD 97-10 algorithm and a broken reference is rejected. National or proprietary references
that do not use the RF prefix pass through unchecked, so a legitimate national format is never
falsely rejected. Structured remittance is supported for pain.001.001.09 and pain.008.001.08; the
legacy and DK variants throw rather than dropping it silently.
Purpose codes
Section titled “Purpose codes”A transfer can carry a purpose code (Purp/Cd), and a batch can carry a category purpose code
(PmtTpInf/CtgyPurp/Cd). Both are 4-letter ISO 20022 external codes (for example SALA for salary,
SUPP for supplier payment, TAXS for tax).
const doc = { // ... batches: [ { // ... categoryPurpose: 'SALA', // PmtTpInf/CtgyPurp/Cd, batch level transfers: [ { endToEndId: 'INV-1001', amount: euros('123.45'), creditor: { name: 'Beispiel AG', iban: 'NL91ABNA0417164300' }, purpose: 'SUPP', // Purp/Cd, transaction level }, ], }, ],}The codes are validated for SEPA charset and length (1 to 4 characters) only. They are NOT checked
against the ISO external code list, which is published separately and updated quarterly: validating
membership would risk falsely rejecting a valid but newer code. Supported for pain.001.001.09 and
pain.008.001.08; the legacy and DK variants throw rather than dropping the codes silently.
Variants
Section titled “Variants”To write a German DK pain.001.003.03 file, pass { variant: 'pain.001.003.03' }:
const xml = writeCreditTransfer(doc, { variant: 'pain.001.003.03' })See National variants for the structural differences and XSD details.
Bank profiles
Section titled “Bank profiles”To add bank-specific validation or output tweaks:
import { requireBic } from 'sepa-xml-ts'
const xml = writeCreditTransfer(doc, { profile: requireBic })See Bank profiles for the full profile API.