SEPA on the Platform: SCT, SCT Instant, SDD and the pacs.008 Message

SEPA — the Single Euro Payments Area — is not one scheme but a family. When you send euros across it, the exact scheme decides how fast the money moves and whether it is a push or a pull. KibiPay's SEPA adapter speaks this family through a single ISO 20022 message type, the pacs.008, and this post walks through the three schemes and how a canonical payment becomes that message.
The three SEPA schemes
- SEPA Credit Transfer (SCT) — the workhorse push credit transfer. The debtor pushes euros to the creditor; funds clear on the scheme's normal cycle rather than instantly.
- SEPA Instant Credit Transfer (SCT Inst) — the same push, but with near-instant finality, targeting settlement in about ten seconds, around the clock, every day of the year.
- SEPA Direct Debit (SDD) — the odd one out: a pull. The creditor initiates a collection against the debtor's account under a pre-agreed mandate, rather than the debtor pushing money out.
All three appear in the platform's scheme enum as SCT, SCT_INST and SDD, and each has its own outbound workflow (outbound-sct, outbound-sct-inst, outbound-sdd).
From canonical payment to pacs.008
The adapter's job is translation. It takes a canonical payment and builds an ISO 20022 pacs.008.001.08 document — the FIToFICstmrCdtTrf (FI-to-FI Customer Credit Transfer) message that banks exchange to move a credit transfer. The structure it emits is standard pacs.008:
- A
GrpHdr(group header) with a message id derived from the payment (KP{id}, capped at the 35-character ISO limit), a creation timestamp, a transaction count of one, and a settlement method ofCLRG(clearing). - A
CdtTrfTxInf(credit-transfer transaction) carrying the end-to-end id (your reference, or the payment id), the interbank settlement amount with itsCcyattribute in euros to two decimal places, and a charge bearer ofSLEV(following the service level). - Debtor and creditor parties, each with a name, an
IBANaccount and aBICFIagent. Where a BIC isn't supplied the adapter falls back toNOTPROVIDED, the ISO-blessed placeholder. Optional unstructured remittance rides inRmtInf/Ustrd.
Submit a payment and you get back the pacs.008 XML, a wire-format label, and a scheme reference shaped like SEPA-XXXXXXXX that later drives status enquiries and reconciliation.
Deterministic outcomes for testing
Because the adapter is a self-contained simulator with no external euro network to call, it needs a way to exercise every outcome branch deterministically. It does this with a neat trick: the cents of the amount act as a marker. An amount ending in .99 forces a rejection (RJCT, invalid creditor account); .77 forces a return (RTRN, an AC04 closed-account return); .88 yields acceptance pending interbank settlement (ACCP); anything else settles cleanly (ACSC). Those scheme codes map straight onto the canonical lifecycle — ACSC to SETTLED, ACCP to ACCEPTED, RJCT to REJECTED, RTRN to RETURNED — so an integration test can drive a payment to any terminal state just by choosing the amount.
An honest note on SDD
The credit-transfer schemes (SCT and SCT Inst) are a natural fit for pacs.008 because they are credit transfers. SEPA Direct Debit is conceptually different — it is a collection, a pull against a mandate — and that pull behaviour is modeled on the platform in the direct-debit collection flow rather than as a credit message. If you want the pull mechanics, mandates, and unpaid-collection returns, read Direct Debit is a pull, not a push. The SEPA adapter here is the credit-transfer translator; it is the piece that turns "send euros" into a valid, well-formed pacs.008.
The payoff of routing all three schemes through one ISO 20022 translator is consistency: the same canonical model, the same message builder, the same reconciliation path, whether you're sending a standard transfer, an instant one, or reaching for a direct debit.
Reading a payment's fate back
Emitting the pacs.008 is only half of an adapter's job; the other half is answering "what happened to it?" The SEPA adapter keeps each submission's outcome in an in-memory store keyed by the SEPA-XXXXXXXX scheme reference, and exposes a status-enquiry endpoint at GET /v1/status/{scheme_ref}. That's the exact surface the reconciliation engine calls when a euro payment drifts past its grace window: it asks the adapter for the current acceptance code and maps it back onto the canonical lifecycle. Because the whole adapter is a deterministic, no-network simulator, that enquiry always returns the same answer for the same payment — which is precisely what makes it usable as a stand-in during continuous integration, where a real euro clearing connection would be neither available nor repeatable. Alongside the two functional endpoints sit the usual GET /healthz and GET /readyz probes, so the service participates in the same Kubernetes health model as every other rail adapter. It handles euros over IBAN-and-BIC addressing only, by design — the SEPA scheme is a euro scheme, and constraining the adapter to that keeps its validation honest.