sepa
🏦 SEPA payment utilities for Rust. ⚡ Zero I/O. No async. 🔢 No
f64in monetary arithmetic. ✅ Generated files are validated against the real ISO 20022 XSDs in CI.
✨ Features
| Module | What it provides |
|---|---|
iban |
🔍 IBAN validation — ISO 13616 mod-97 + 89-country registry + SEPA membership |
bic |
🔍 BIC/SWIFT validation — ISO 9362, SEPA pattern |
creditor_id |
🔍 SEPA Creditor Identifier — EPC AT-02 |
pain001 |
📤 pain.001 XML builder — SCT + SCT Instant |
pain008 |
📤 pain.008 XML builder — SDD CORE + B2B |
pain002 |
📥 pain.002 XML parser — Payment Status Report (bank → customer) |
camt052 |
📥 camt.052 XML parser — intraday account report |
camt053 |
📥 camt.053 XML parser — Bank-to-Customer Statement (end-of-day) |
camt054 |
📥 camt.054 XML parser — debit/credit notification, incl. returns |
reference |
🔗 ISO 11649 RF Creditor Reference — validate and generate |
party |
👥 Ultimate debtor/creditor (UltmtDbtr / UltmtCdtr) |
purpose |
🏷️ Purp and CtgyPurp codes, as the two distinct code sets |
validate |
✅ EPC field rules the XSD does not enforce |
charset |
🔤 SEPA Basic Latin set + the real EPC217-08 conversion table |
ct_to_eur_str / ct_from_eur_str |
💶 Integer-safe i64 ct ↔ "1234.56" round-trip |
All commonly used types are re-exported at the crate root (sepa::SequenceType, sepa::DirectDebitScheme, …).
🚀 Quick start
[]
= "0.4"
📤 Building payment files
build() returns a Result: the batch is checked against the EPC rules before
any XML is produced.
use ;
let creditor = validate_iban?;
let debtor = validate_iban?;
assert_eq!; // grouped Display
assert!; // SEPA scheme area
// pain.001 — SEPA Credit Transfer (Überweisung), pain.001.001.09
let ct_xml = new
.msg_id
.add_group
.build?;
// pain.008 — a direct debit run with FRST and RCUR in ONE file.
// Sequence type lives on the group, so each needs its own.
let ci = validate_creditor_id?;
let dd_xml = new
.msg_id
.add_group
.add_group
.build?;
assert!;
assert!;
# Ok::
Each add_group becomes one PmtInf block. Sequence type, execution date,
debtor account, batch booking and category purpose all sit at that level — so
groups are what let one file mix FRST with RCUR, or carry two execution
dates, instead of forcing a separate submission per combination.
✅ Validation and character handling
The published ISO schemas are far more permissive than the banks. A file can
validate cleanly against the XSD and still be rejected — a zero amount, a
141-character name, <BICFI>NOTPROVIDED</BICFI>. This crate enforces the EPC
rules that actually decide acceptance:
use ;
let iban = validate_iban.unwrap;
// An empty message is schema-invalid (PmtInf and CdtTrfTxInf are both 1..n).
assert!;
| Rule | Enforced |
|---|---|
MsgId, EndToEndId, MndtId |
1–35 chars, no leading/trailing /, no // |
Party name Nm |
1–70 chars (XSD permits 140) |
Remittance Ustrd |
1–140 chars |
InstdAmt |
0.01 – 999,999,999.99 EUR |
| Dates | real YYYY-MM-DD calendar dates |
| Batch | at least one transaction |
| Direct debit | Creditor Identifier required |
Ultimate party Nm |
1–70 chars; only Nm and Id permitted |
| Purpose codes | 1–4 alphanumerics, in the correct code set |
Text outside the SEPA Basic Latin set is transliterated by default, since banks reject it outright:
use ;
// German style (default) — preserves the reading of the name
assert_eq!;
// The EPC217-08 table exactly as published — strictly one-to-one for Latin
assert_eq!;
The mapping is transcribed from the spreadsheet the EPC publishes, not approximated — 1011 entries. That matters beyond Latin accents, where generic transliterators diverge from the standard:
# use ;
// 26 Greek and Cyrillic letters have a published romanisation (ISO 843 / ISO 9)
assert_eq!;
assert_eq!;
// …and the ligatures do NOT expand, whatever a generic folder would do
assert_eq!;
Use .charset(CharsetPolicy::Strict) to reject non-SEPA characters instead of
rewriting them.
🧾 Corporate extras
Ultimate parties, purpose codes, structured references and mandate amendments — all XSD-validated in the right element positions:
use ;
let iban = validate_iban?;
let debtor = validate_iban?;
let xml = new
.msg_id
.add_group
.build?;
assert!;
assert!;
assert!;
# Ok::
💾 Streaming large batches (no intermediate String)
use ;
use BufWriter;
use File;
let iban = validate_iban?;
let builder = new
.msg_id
.add_group;
// `write_to` validates first, so a rejected batch leaves the file untouched
// rather than writing a truncated document.
let file = create?;
builder.write_to?;
# Ok::
📥 Parsing bank responses
Parsing is built on quick-xml, so
comments, entity references, CDATA and self-closing tags are all handled
correctly, and <ns2:Document> and <Document xmlns=…> parse identically.
<!DOCTYPE> is rejected outright as defence in depth against entity-expansion
attacks.
pain.002 — Payment Status Report
use parse_pain002;
# let xml = "";
let doc = parse_pain002?;
if doc.is_fully_accepted else
# Ok::
camt.053 — End-of-Day Statement
Handles every generation from camt.053.001.02 to .13, including the v07
changes (<Sts><Cd>BOOK</Cd></Sts>, RltdPties/Dbtr/Pty/Nm).
use parse_camt053;
# let xml = "";
let doc = parse_camt053?;
let stmt = &doc.statements;
println!;
println!;
for entry in &stmt.entries
# Ok::
camt.054 — Intraday Notifications (json feature)
= { = "0.4", = ["json"] }
#
📋 Standards
| Standard | Module | Notes |
|---|---|---|
| ISO 13616-1 + SWIFT IBAN Registry | iban |
89-country length table |
| EPC409-09 v8.0 | iban |
SEPA scheme country list (42 codes) |
| ISO 9362 | bic |
8- and 11-char BIC, SEPA pattern |
| EPC262-08 | creditor_id |
Creditor Identifier check digits |
| ISO 20022 pain.001.001.09 | pain001 |
SCT + SCT Instant — default |
| ISO 20022 pain.001.003.03 | pain001 |
Legacy DK V2.7 |
| ISO 20022 pain.008.001.08 | pain008 |
SDD CORE + B2B — default |
| ISO 20022 pain.008.003.02 | pain008 |
Legacy DK V2.7 |
| ISO 20022 pain.002 (all variants) | pain002 |
namespace-agnostic parser |
| ISO 20022 camt.053 (v02–v13) | camt053 |
namespace-agnostic parser |
| EPC217-08 | charset |
SEPA character set + conversion table |
| EPC SEPA Rulebooks 2023/2025 | all | SCT, SDD Core, SDD B2B, SCT Inst |
Schema versions
pain.001.001.09 and pain.008.001.08 are the versions the EPC 2023 rulebooks
mandated from 19 November 2023, and are the defaults here. The German DK
variants pain.001.003.03 / pain.008.003.02 reached end-of-life in November
2022 and remain available via .schema(…) for systems still pinned to them.
🔬 Testing
| Layer | What it checks |
|---|---|
| Unit + integration | 306 tests |
| XSD validation | generated documents validated against the real ISO 20022 schemas with xmllint, in CI |
| Fuzzing | cargo-fuzz targets over parsers, validators and builders (fuzz/) |
| Exhaustive sweep | all 1,114,112 Unicode code points × both transliteration styles |
Schema validation is necessary but not sufficient — <InstdAmt>0</InstdAmt>,
a 140-character name and <BICFI>NOTPROVIDED</BICFI> all pass the XSD and are
rejected by banks. Those rules live in validate and are enforced by build().
📦 Dependencies
| Crate | Role |
|---|---|
thiserror |
Ergonomic error derives |
quick-xml |
Correct, streaming XML parsing |
serde |
Serialize/Deserialize on all types (serde feature) |
serde_json |
camt054::parse_simple_json (json feature) |
📄 License
Licensed under either of MIT or Apache-2.0, at your option.