sepa

Rust crate · ISO 20022 · SEPA

sepa

SEPA payment messages for Rust: checked identifiers, XML builders, bank-report parsers and integer money.

cargo add sepa@0.9

Latest version on crates.io API documentation on docs.rs CI build status Minimum supported Rust version 1.88

Why another SEPA crate🔗

XSD validity does not establish SEPA profile compliance or bank acceptance. For example, an ISO name field may allow 140 characters while the applicable current customer-to-PSP SEPA profile limits it to 70. Verification of Payee obligations depend on the applicable jurisdiction, PSP and implementation date.

This crate validates supported message rules and preserves unresolved reported facts. It does not execute payments or certify a bank’s acceptance.

A collection run in one file🔗

Sequence type lives on the payment group, so a real direct debit run — first collections alongside recurring ones — is one message, not two submissions.

use sepa::{
    DirectDebitEntry, DirectDebitGroup, IsoDate, Pain008Builder, SequenceType,
    validate_creditor_id, validate_iban,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let creditor = validate_iban("DE89 3704 0044 0532 0130 00")?;
    let debtor = validate_iban("NL91ABNA0417164300")?;
    let ci = validate_creditor_id("DE98ZZZ09999999999")?;

    let xml = Pain008Builder::new("Stadtwerke GmbH", "DD-2026-07-001")
        .add_group(
            DirectDebitGroup::new("Stadtwerke GmbH", &creditor, &ci, IsoDate::new(2026, 7, 20)?)
                .sequence_type(SequenceType::Frst)
                .add_entry(DirectDebitEntry::new(
                    "MND-1",
                    "2026-06-01".parse()?,
                    "Neu Kunde",
                    debtor.clone(),
                    5_000,
                    "E2E-1",
                )),
        )
        .add_group(
            DirectDebitGroup::new("Stadtwerke GmbH", &creditor, &ci, IsoDate::new(2026, 7, 18)?)
                .sequence_type(SequenceType::Rcur)
                .add_entry(DirectDebitEntry::new(
                    "MND-2",
                    "2024-06-01".parse()?,
                    "Alt Kunde",
                    debtor,
                    7_500,
                    "E2E-2",
                )),
        )
        .build()?;

    assert!(xml.contains("<SeqTp>FRST</SeqTp>"));
    assert!(xml.contains("<SeqTp>RCUR</SeqTp>"));
    Ok(())
}

build() returns a Result. The batch is checked against the EPC field rules before any XML exists, so a rejected run never reaches your bank.

Schemes covered🔗

The scheme — not the message — carries the rules that decide whether a bank accepts a file. Two schemes can share a message and share none of its rules.

Scheme
SCT — Credit Transferpain.001 · pain.002 · camt.05x✅
SCT Inst — Instant Credit Transferas SCT, LclInstrm=INST✅
SDD Corepain.008 · pain.007 · pain.002 · camt.05x✅
SDD B2Bas SDD Core, LclInstrm=B2B✅
OCT Inst — One-Leg Out Instantthe same three messages, SvcLvl=EOLO✅

Verification of Payee results are read from pain.002. VoP, SEPA Request-To-Pay and SPAA requests are REST APIs — SRTP’s pain.013/pain.014 travel as JSON — so they are out of scope for a library that builds XML.

Messages covered🔗

DirectionMessageWhat it is
📤 Sendpain.001Credit transfer — SCT, SCT Instant, OCT Inst, scheduled instant
📤 Sendpain.008Direct debit — CORE and B2B
📤 Sendpain.007Direct debit reversal — after settlement
📤 Sendcamt.055Payment cancellation request; not proof of cancellation or return
📥 Receivepain.002Status report, including Verification of Payee
📥 Receivecamt.029Resolution of investigation — the answer to a recall
📥 Receivecamt.052Intraday account report
📥 Receivecamt.053End-of-day statement
📥 Receivecamt.054Debit/credit notification and returns

Plus IBAN and BIC validation, SEPA Creditor Identifiers, ISO 11649 RF references, structured postal addresses, and the real EPC217-08 character conversion table.

Read the documentation →