Skip to content

Direct debits (pain.008)

A direct debit is a pull payment: a creditor (payee) collects money from one or more debtors, each authorized by a signed mandate. The pain.008.001.08 message type is the modern ISO 20022 Direct Debit Initiation message used across the SEPA zone.

import { euros, type DirectDebitDocument } from 'sepa-xml-ts'
const doc: DirectDebitDocument = {
messageId: 'DD-2026-0001',
createdAt: '2026-06-01T09:00:00Z',
initiatingParty: 'ACME GmbH',
// The creditor is specified once at document level.
// The writer fans it out into every PmtInf automatically.
creditor: {
name: 'ACME GmbH',
iban: 'DE89370400440532013000',
bic: 'COBADEFFXXX',
creditorId: 'DE98ZZZ09999999999', // SEPA Creditor Identifier
},
batches: [
{
id: 'BATCH-001',
collectionDate: '2026-06-10', // ReqdColltnDt/Dt: YYYY-MM-DD
sequenceType: 'FRST', // FRST | RCUR | OOFF | FNAL
localInstrument: 'CORE', // CORE | B2B (defaults to CORE)
collections: [
{
endToEndId: 'SUB-1001',
amount: euros('49.99'),
debtor: {
name: 'Kunde Eins',
iban: 'NL91ABNA0417164300',
},
mandate: {
id: 'MND-001',
signatureDate: '2026-01-15', // YYYY-MM-DD
},
remittanceInfo: 'Subscription June', // optional
},
],
},
],
}
TypeXSD element
DirectDebitDocumentDocument/CstmrDrctDbtInitn
CreditorCdtr + CdtrAcct + CdtrAgt + CdtrSchmeId
DirectDebitBatchPmtInf
CollectionDrctDbtTxInf
MandateDrctDbtTx/MndtRltdInf
SequenceTypePmtTpInf/SeqTp
LocalInstrumentPmtTpInf/LclInstrm/Cd
ValueMeaning
FRSTFirst collection under a mandate
RCURRecurring collection
OOFFOne-off collection (mandate used once)
FNALFinal collection, mandate will be cancelled

The creditorId field on Creditor is a SEPA Creditor Identifier (e.g. DE98ZZZ09999999999). The library validates the check digits strictly using ISO 7064 MOD 97-10, the same algorithm as IBAN validation. An incorrect check digit is caught at validation time before the file is written.

The structure is:

  • 2-letter country code
  • 2 check digits (ISO 7064 MOD 97-10)
  • 3-char creditor business code (e.g. ZZZ)
  • national identifier (1 to 28 alphanumeric characters)

Use buildCreditorId(country, businessCode, nationalId) from the library to compute a correct identifier programmatically.

import { writeDirectDebit } from 'sepa-xml-ts'
const xml = writeDirectDebit(doc)

writeDirectDebit validates the model internally before writing. It throws if the model is invalid.

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

FieldHow it is derived
GrpHdr/NbOfTxsTotal count of all collections across all batches
GrpHdr/CtrlSumSum of all collection amounts (exact bigint addition)
PmtInf/NbOfTxsCount of collections in this batch
PmtInf/CtrlSumSum of collection amounts in this batch
PmtInf/PmtMtdAlways DD
PmtInf/PmtTpInf/SvcLvl/CdAlways SEPA
PmtInf/ChrgBrAlways SLEV
CdtrSchmeId/.../SchmeNm/PrtryAlways SEPA

CdtrAgt (at PmtInf level) and DbtrAgt (per transaction) are structurally required by the XSD even when no BIC is present. The writer emits the correct empty-institution placeholder automatically when bic is absent:

<CdtrAgt><FinInstnId/></CdtrAgt>

This is handled transparently. You only need to supply bic when you know it.

validateDirectDebit and writeDirectDebit enforce four cross-field SEPA rulebook constraints. R1 to R3 apply to both CORE and B2B direct debits, and to both the ISO variant and the German DK variant. R4 relates to mandate amendment (see below).

R1: signature before collection. mandate.signatureDate must not be after the batch collectionDate. A collection cannot be initiated on a mandate that has not been signed yet. Equal dates are allowed (signing and collecting on the same day is valid).

R2: OOFF is single-use. A mandate id used in any batch with sequenceType: 'OOFF' must appear in exactly one collection across the whole document, and must not also appear under any other sequence type. A one-off authorization covers one collection.

R3: consistent scheme per mandate. A given mandate id must not appear under both CORE and B2B local instruments in the same document. A CORE mandate and a B2B mandate are distinct authorizations. Note that localInstrument defaults to CORE when omitted, matching the writer default.

R4: SMNDA requires FRST. If any collection in a batch uses mandate.amendment.sameMandateNewDebtorAccount: true (the SMNDA flag), that batch’s sequenceType must be FRST. A change of debtor account is presented as a first collection.

validateDirectDebit returns the violations as ruleIssues on the result. writeDirectDebit throws before emitting any XML when a rule is violated.

When an existing mandate’s details change, set mandate.amendment. The writer emits MndtRltdInf/AmdmntInd=true and the matching AmdmntInfDtls. This first cut models the common cases:

const collection = {
endToEndId: 'SDD-E2E-001',
amount: euros('10.00'),
debtor: { name: 'Customer', iban: 'NL91ABNA0417164300' },
mandate: {
id: 'MAND-NEW-001',
signatureDate: '2025-01-01',
amendment: {
originalMandateId: 'MAND-OLD-001', // AmdmntInfDtls/OrgnlMndtId, the mandate reference changed
// OR a moved account, give the old IBAN:
// originalDebtorAccount: 'DE89370400440532013000',
// OR same bank, new account (SMNDA), which requires a FRST batch (rule R4):
// sameMandateNewDebtorAccount: true,
},
},
}

originalDebtorAccount (an old IBAN) and sameMandateNewDebtorAccount are mutually exclusive: SMNDA means the new account is at the same bank and is not disclosed, so an explicit old account would contradict it. An amendment object must set at least one detail. Supported for pain.008.001.08 only; the DK variant throws if an amendment is present. The richer AmdmntInfDtls fields (original creditor scheme id, original debtor name and agent, frequency, final collection date, reason) are a documented follow-up.

Each collection accepts an optional ultimateCreditor and ultimateDebtor (name only), emitted as UltmtCdtr/Nm and UltmtDbtr/Nm. These name the party that really collects or really owes, when it differs from the account holder.

const collection = {
endToEndId: 'SDD-E2E-001',
amount: euros('10.00'),
debtor: { name: 'Customer', iban: 'NL91ABNA0417164300' },
mandate: { id: 'MAND-001', signatureDate: '2025-01-01' },
ultimateDebtor: { name: 'Real Account Holder' },
}

Supported for pain.008.001.08 only. The DK variant throws if an ultimate party is present, rather than dropping it silently.

Each collection carries remittance information in one of two mutually exclusive forms: a free-text remittanceInfo (unstructured RmtInf/Ustrd), or a structuredRemittance creditor reference (RmtInf/Strd/CdtrRefInf).

const collection = {
endToEndId: 'SDD-E2E-001',
amount: euros('10.00'),
debtor: { name: 'Customer', iban: 'NL91ABNA0417164300' },
mandate: { id: 'MAND-001', signatureDate: '2025-01-01' },
structuredRemittance: { creditorReference: 'RF18539007547034' },
}

referenceType defaults to SCOR and is restricted to the ISO 20022 code list. A reference starting with the uppercase RF prefix has its ISO 11649 check digits validated; non-RF national references pass through unchecked. Supported for pain.008.001.08 only; the DK variant throws if present. See the credit transfers guide for the full validation policy.

Each collection can carry a purpose code (Purp/Cd), and each batch a category purpose code (PmtTpInf/CtgyPurp/Cd). Both are 4-letter ISO 20022 external codes.

const batch = {
// ...
categoryPurpose: 'SUPP', // batch level
collections: [
{
endToEndId: 'SDD-E2E-001',
amount: euros('10.00'),
debtor: { name: 'Customer', iban: 'NL91ABNA0417164300' },
mandate: { id: 'MAND-001', signatureDate: '2025-01-01' },
purpose: 'GDDS', // collection level
},
],
}

Validated for SEPA charset and length (1 to 4 characters) only, not against the ISO external code list. Supported for pain.008.001.08 only; the DK variant throws if present.

To write a German DK pain.008.003.02 file:

const xml = writeDirectDebit(doc, { variant: 'pain.008.003.02' })

See National variants for the structural differences and XSD details.