Skip to content

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.

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)
},
],
},
],
}
TypeXSD element
CreditTransferDocumentDocument/CstmrCdtTrfInitn
PaymentBatchPmtInf
TransferCdtTrfTxInf
AccountPartyDbtr or Cdtr with account and agent
MoneyAmt/InstdAmt
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.

The writer derives the following fields from the model. You do not supply them:

FieldHow it is derived
GrpHdr/NbOfTxsTotal count of all transfers across all batches
GrpHdr/CtrlSumSum of all transfer amounts (exact bigint addition)
PmtInf/NbOfTxsCount of transfers in this batch
PmtInf/CtrlSumSum of transfer amounts in this batch
PmtInf/PmtMtdAlways TRF
PmtInf/PmtTpInf/SvcLvl/CdAlways SEPA
PmtInf/ChrgBrAlways SLEV

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.

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.

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 unstructured RmtInf/Ustrd.
  • structuredRemittance maps to a structured creditor reference at RmtInf/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.

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.

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.

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.