A Practical Guide to SEPA Integration for International Fintechs
Run Formance Ledger locally
Clone Formance Ledger from GitHub, run it locally, and post a pending instant payment against your account tree.
Run Formance Ledger locally
Clone Formance Ledger from GitHub, run it locally, and post a pending instant payment against your account tree.
Fintechs moving euros across Europe often face a first bank statement (a camt.053 file) with entries the ledger can't match. Then, they face a direct debit refund landing six weeks after settlement, or find €250,000 stuck mid-flight when the 10-second instant payment confirmation never arrives.
SEPA (Single Euro Payments Area) is the set of euro payment rails covering 41 countries under one rulebook. A fintech needs a sponsor bank or licensed payment provider, a core ledger that tracks payment state end to end, and support for the three schemes that run on the SEPA payment rails.
SEPA is the standardized euro payment zone for moving euros across countries under one set of rules, one message format, and one account number format.
The rules are written by the European Payments Council (EPC), which publishes a rulebook for each payment type:
Fintechs don't connect to SEPA directly because they must go through a sponsoring bank or a licensed payment provider that gives them API or file access to the payment rail.
Every rulebook took effect on 5 October 2025, earlier than the usual November date, so any integration built against older rulebooks is already out of date.
Fintechs need a core ledger to hold SEPA payment state (pending, settled, restored, refund, and reconciliation) across every scheme, without scattering it across payment services to meet those rulebooks. Formance offers an open-source, programmable core ledger that unifies fiat and digital assets with regulatory-grade traceability.
SEPA has four payment types, SCT, SCT Inst, SDD Core, and SDD B2B, each with its own settlement speed, error handling, and refund window.
| Payment type | Settlement speed | Error handling | Message type |
| SCT | Batches, not real-time | Per the EPC SCT guidance on error codes | pain.001 |
| SCT Inst | Within 10 seconds of the payer's bank getting the order | Rejects and recalls, including sender-initiated recall requests | pain.001 |
| SDD Core | Payment date set in advance; refundable for 8 weeks, 13 months if unauthorized | Refusals, rejects, returns, refunds, reversals | Direct Debit Initiation (pain.008) |
| SDD B2B | Payment date; final after 3 business days | Same five types, no refund on authorized payments | pain.008 |
A money movement architecture has to handle each one separately. A business direct debit is final under SDD B2B after three business days, but a consumer direct debit can still be refunded eight weeks later.
Whichever rail a payment runs on, the fintech needs a core ledger to hold its state. The rail determines settlement speed and refund windows. Without a ledger, each rail's timing and reversibility rules end up scattered across payment services.
A core ledger is required for fintechs to meet SEPA requirements across four areas: pooled funds and virtual accounts (mapping one omnibus balance to individual user wallets), settlement versus posting states (tracking payments), automated reconciliation (matching bank feeds to internal events at scale), and AML, KYC, and audit evidence (producing the immutable trail regulators require).
Fintechs rarely hold a bank account per user. Customer funds sit pooled in one master account (the omnibus account) at a sponsor bank. The sponsor sees a single balance, and the ledger maps that balance to individual user wallets in euros, in real time.
SEPA notification, clearing, and final settlement rarely happen at the same moment. A ledger records pending, authorized, and settled states so a user cannot spend funds that are still clearing or subject to a return.
SEPA generates high volumes of statement lines. A double-entry ledger matches bank feeds against internal transaction events and flags discrepancies, failed transfers, and reversals as they arrive.
The European Central Bank (ECB) and national regulators require fintechs and electronic money institutions to trace every euro. Immutable, double-entry journal entries produce a chronological audit trail for compliance reporting and financial audits, and back the Anti-Money Laundering (AML) and Know Your Customer (KYC) controls regulators require.
Fintechs turn SEPA rules into ledger entries in seven steps, grouped into two categories: steps that apply to every SEPA rail and steps that apply only to specific rails.
These four steps cover idempotent submission, Verification of Payee, R-transaction bookings, and end-of-day reconciliation. They apply whether the payment runs on SCT, SCT Inst, SDD Core, or SDD B2B.
Idempotent means safe to send twice: if the file gets sent again, the second send doesn't create a duplicate payment. Use the ledger's transaction ID to build the message ID and payment info ID in your pain.001 and pain.008 files, and never timestamp or retry the transaction.
SEPA only handles euros, and it follows the TARGET calendar (closed on weekends and major holidays), so a resend after an outage might cross a bank holiday and use the wrong value date. Four controls make idempotent submission safe:
Recovery queues that replay old files after an outage have been known to duplicate payments at scale, moving significant sums into accounts that shouldn't receive them and creating clawback work that can drag on for weeks.
The payer's bank must check that the beneficiary name matches the IBAN before the payer confirms an SCT or SCT Inst, on any channel. Handle the three possible outcomes as separate states:
If the check fails or the responding bank doesn't answer, hold the payment in a waiting account instead of sending the file. The check is dispute evidence and a prerequisite for the instant payment window.
An R-transaction is any message that reverses or rejects a payment: reject, refusal, return, refund, or reversal for direct debits, plus rejects and recall requests for instant payments.
Each type needs its own ledger entry because they differ in who starts them and whether the payment has settled yet. Before settlement, release the hold; after settlement, reverse the booked credit.
Post the reversal with bi-temporal metadata (when it was entered and when it took effect) so it points back to the original payment.
| R-transaction | Who starts it and when | Ledger entry |
| SDD reject | Debtor's bank or clearing system, before settlement | Release the expected-funds hold, but nothing is settled |
| SDD refusal | Debtor, before settlement | Release the hold, and save the reason |
| SDD return | Debtor's bank, after settlement | Reverse the credit against @platform:sdd:returns:suspense at the R-transaction value date |
| SDD refund | Debtor via their bank, after settlement, within the refund window | Reverse against the creditor's account, and dated back to the original payment |
| SDD reversal | Creditor (you), after settlement | New outbound entry from the creditor's account, and referencing the original payment |
| SCT Inst reject | Beneficiary's bank or clearing system, within the window | Move the amount from pending back to available |
| SCT Inst recall or recall request | Sender's bank, after settlement | Book the request against @platform:sepaInst:recalls:pending; move funds only if the answer is positive |
At end of day, the sum of customer balances in the ledger should equal the camt.053 closing balance for the omnibus IBAN.
If it doesn't, the difference posts to @platform:omnibus:reconciliation:variance with the statement date attached.
A zero variance balance is the reconciliation evidence auditors and operations teams need.
These three steps cover the SDD mandate lifecycle (SDD Core and SDD B2B), the SCT Inst 10-second state machine (SCT Inst), and inbound credit attribution (SCT and SCT Inst). Each applies only to the rails noted on its header.
The mandate (the customer's permission to pull money) has to exist as a proper record in the ledger before any direct debit file goes out. Save the mandate reference, signature date, creditor ID, debtor name and IBAN, and sequence type (first, recurring, one-off, or final) so every payment can be traced back to the permission behind it.
Changes to a mandate (a new debtor IBAN, a new creditor ID, or a migrated reference) update the existing mandate with an "amendment" flag and the old values. A canceled mandate can't back a later payment, even if the debtor's account is still open.
Track pre-notification timing per mandate. The SDD rulebooks require the creditor to tell the debtor at least 14 calendar days before the first collection, unless the mandate agrees to a shorter period.
Sending a file too early breaks the rulebook and is a common cause of refunds. Copy the mandate reference, sequence type, and pre-notification date onto every payment so returns and refunds can be traced back to the mandate that authorized them.
Hold instant payments in a pending state so a missing confirmation inside the 10-second window doesn't trigger an automatic failure. Move to settled when confirmation arrives; return the funds to the customer if the window expires. The clock runs per payment, so a file with many payments is many separate windows.
A delayed confirmation is not the same as a failed one. Real outages can delay instant payment confirmations while settlement keeps running underneath, meaning a ledger that treats silence as failure would return money that had already settled. Always wait for an explicit result before reversing.
In the example below, written in Numscript (Formance's ledger DSL), @users:4821:available holds spendable funds and @users:4821:sepaInst:pending reserves €250,000 while transfer trf250000 waits on the result:
// SCT_INST_SUBMIT
// Event: reserve EURO 250,000 for transfer trf250000 while SCT Inst is pending
send [EUR/2 25000000] (
source = @users:4821:available
destination = @users:4821:sepaInst:pending
)
set_tx_meta("event_type", "sct_inst_submit")
set_tx_meta("transfer_id", "trf250000")
A confirmation moves the amount from @users:4821:sepaInst:pending to @platform:banks:sponsor:out:settled. A timeout returns it to @users:4821:available. Save the payment ID and reason code on both moves.
An incoming SCT or SCT Inst lands in your shared IBAN (the omnibus account) with a reference in the payment description.
The ledger needs three things:
The sponsor bank sees one balance, and the fintech owes each customer their share.
Post incoming credits to @platform:omnibus:unattributed first, then move them to @users:{id}:available once the reference resolves.
Anything that can't be matched stays in the unattributed account for ops to review, so the ledger never books a guess.
A SEPA-ready ledger has to prove five things: message IDs come from ledger transactions, instant payments go through a pending state before settling or restoring, each error type has a named booking rule with a reason code, beneficiary addresses are in the structured format, and the ledger reconciles to the camt.053 every day.
Run these five checks:
With the Formance Ledger holding pending, settled, restored, refund, and reconciliation states, teams can centralize booking rules instead of spreading ledger state across payment services.