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 three cross-field SEPA rulebook constraints. These apply to both CORE and B2B direct debits, and to both the ISO variant and the German DK variant.

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.

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

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.