Skip to content

Add opt-in SEPA Instant (SCT Inst / Echtzeitüberweisung) support to credit transfers#81

Open
wolframarnold wants to merge 1 commit into
raphaelm:masterfrom
wolframarnold:feature/sepa-instant
Open

Add opt-in SEPA Instant (SCT Inst / Echtzeitüberweisung) support to credit transfers#81
wolframarnold wants to merge 1 commit into
raphaelm:masterfrom
wolframarnold:feature/sepa-instant

Conversation

@wolframarnold

Copy link
Copy Markdown

What

Adds opt-in SEPA Instant Credit Transfer (SCT Inst / Echtzeitüberweisung) support to SepaTransfer. When the config carries "instant": True, the generated pain.001 emits PmtTpInf/LclInstrm/Cd = INST — the EPC/DK mechanism for requesting an instant transfer — positioned directly after SvcLvl, per the ISO 20022 PaymentTypeInformation sequence (InstrPrty?, SvcLvl?, LclInstrm?, CtgyPurp?).

sepa = SepaTransfer({..., "instant": True}, schema="pain.001.001.03")
<PmtTpInf>
  <SvcLvl><Cd>SEPA</Cd></SvcLvl>
  <LclInstrm><Cd>INST</Cd></LclInstrm>
</PmtTpInf>

Why

Under the EU Instant Payments Regulation (2024/886), euro-area PSPs must offer instant credit transfers at price parity with standard ones, so requesting INST is becoming a routine need. sepaxml already emits LclInstrm/Cd for direct debits but had no way to do so for credit transfers. (Downstream, e.g. python-fints pins sepaxml==2.7.* for its SEPA payloads.)

Design

  • Config-level flag, not per-payment. PmtTpInf is a payment-information-block element shared by all grouped transfers; a per-payment flag on a block-level node is ambiguous for mixed batches. (This is the flaw visible in prior-art Add structured reference and instant payment support #71's per-payment approach.)
  • Mirrors the library's own direct-debit precedent. debit.py already sets LclInstrm/Cd from self._config['instrument'] (CORE/B2B); this is the symmetric credit-transfer counterpart.
  • Scoped to SEPA. The INST node is nested inside the existing non-domestic guard, since INST is a SEPA local instrument — a domestic transfer has no SvcLvl=SEPA/PmtTpInf block to attach it to.

Tests

tests/transfer/test_instant.py asserts INST is present directly after SvcLvl (batch and non-batch paths), XSD-valid, and absent without the flag. Verified XSD-valid across all four supported pain.001 schemas (03/09/10/11). No existing tests change.

Real-world verification

The exact XML this change produces (<PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl><LclInstrm><Cd>INST</Cd></LclInstrm></PmtTpInf> on a pain.001.001.03 batch, batch=True) was verified end-to-end against a live German bank on 2026-07-16, using two €0.01 transfers between real accounts at two different institutions:

  • Upload & acceptance: Nassauische Sparkasse accepted the INST-flagged file through its bulk-order file-upload channel (Sammelaufträge → Datei-Übergabe); the file was parsed completely (message ID, batch-booking flag, item count, amount, recipient), passed the Verification-of-Payee check at release, and was executed after a single TAN.
  • A/B control: an otherwise-identical transfer without LclInstrm was sent the same day. The receiving bank (Taunus Sparkasse) booked the flagged transfer as ECHTZEIT-GUTSCHRIFT (SEPA Instant credit) and the unflagged one as ONLINE-UEBERWEISUNG (standard SCT) — confirming the element alone switches the scheme, cross-bank, and is not silently downgraded.
  • Both transfers booked same-day; the instant one posted within seconds.

(The verification was done by injecting this element into sepaxml 2.7.0 output and re-validating against the bundled pain.001.001.03.xsd — i.e., precisely the XML shape this PR generates natively.)

Prior art

#71 established the same element and position (good corroboration), but bundled it with a per-payment API, structured references, six new XSDs, and a whole-file reformat (~9.4k additions across 24 files) and was self-closed unreviewed. This PR is the minimal form: one feature, ~13 lines in transfer.py, one new test file, one README line — no reformatting, no new schemas.

…ransfers

Under the EU Instant Payments Regulation (2024/886) euro-area PSPs must
offer instant credit transfers at price parity with standard ones, so
users increasingly need to flag a SEPA batch as instant. The EPC/DK
mechanism is PmtTpInf/LclInstrm/Cd=INST, which sepaxml could emit for
direct debits but not for credit transfers.

- transfer.py: when config['instant'] is set, emit
  <LclInstrm><Cd>INST</Cd></LclInstrm> directly after SvcLvl inside
  PmtTpInf, preserving the ISO 20022 PaymentTypeInformation sequence
  (InstrPrty?, SvcLvl?, LclInstrm?, CtgyPurp?). Mirrors the existing
  SepaDD precedent, which already sets LclInstrm/Cd from
  config['instrument']. Gated inside the existing non-domestic (SEPA)
  guard, since INST is a SEPA local instrument.
- The flag is config-level, not per-payment: PmtTpInf is a
  payment-information-block element shared by all grouped transfers, so a
  per-payment flag is ambiguous for mixed batches.
- tests/transfer/test_instant.py: INST present directly after SvcLvl
  (batch and non-batch), XSD-valid, and absent without the flag.
- README: document the config['instant'] flag next to 'domestic'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant