Integrations
Migreren naar Adyen zonder je SEPA-mandaten te breken
Een Nederlands overheidsdepartement, 47.000 actieve SEPA-mandaten, een aanbestedingsklok. Hoe je recurring billing van Stripe naar Adyen migreert zonder mandaat te verliezen.

Het is dinsdagochtend in maart. De finance lead van een Nederlands overheidsdepartement opent een ticket van inkoop: schakel de recurring-billing SaaS over van Stripe naar Adyen vóór het boekjaar sluit. Zevenenveertigduizend actieve SEPA-mandaten. Burgers als betalers, geen bedrijven. Een harde deadline gekoppeld aan de raamovereenkomst. Niemand in het team heeft eerder een payment processor gemigreerd, en inkoop denkt (op basis van één LinkedIn-post) dat het neerkomt op 'de API-keys omwisselen'.
Dat is niet zo. De reden waarom niet, zie je zodra je het SEPA Direct Debit Core Rulebook van de EPC opent, of op de dag dat de eerste Adyen-testincasso terugkomt met reden MD07. Mandaten zijn geen data. Het is een autorisatieketen die toebehoort aan de incassant en die wordt uitgevoerd door de PSP. Zodra je van PSP wisselt, raak je elke schakel in die keten aan.
Elk PSP-migratieverhaal dat we lezen maakt dezelfde fout: de API zou het zware werk zijn. De API is twee dagen werk. De mandaatketen kost de rest van het kwartaal. Het echte engineeringswerk zit in de autorisatie tussen de bank van de betaler, jouw Creditor Identifier en de PSP die op de dag van indienen de file aanlevert.
Wat de SEPA-mandaatketen werkelijk is
Een SEPA-incassomandaat is een ondertekende machtiging van een betaler waarmee een specifieke incassant, geïdentificeerd door een Creditor Identifier (de SCI), bedragen mag afschrijven via direct debit. Elk mandaat draagt een Unique Mandate Reference (UMR), een IBAN, ondertekeningsgegevens (datum, plaats, methode van ondertekening) en een sequence type: FRST voor de eerste incasso, RCUR voor terugkerende, FNAL voor de laatste, OOFF voor eenmalige.
Als je Stripe Billing gebruikt, is Stripe zowel de officiële SCI (hun creditor ID wordt gebruikt, tenzij je iets anders bent overeengekomen) als de houder van het mandaatbewijs. De ondertekende mandaattekst en de authenticatiemetadata staan bij hen. Vanuit je applicatie zie je een payment_method en een mandate object. In de meeste setups heb je het ruwe ondertekende mandaat niet klaarliggen om aan een andere PSP te overhandigen.
Adyen accepteert, net als elke gereguleerde SEPA PSP, geen terugkerende incasso-instructie zonder bewijs dat het mandaat bestaat, wie de incassant is, en welk sequence type meegestuurd moet worden. Wissel je zonder coördinatie, dan gaat de volgende incasso uit onder de creditor ID van Adyen tegen een mandaat dat de betaler heeft getekend voor de creditor ID van Stripe. De bank stuurt het terug. Reden MD07: onjuiste incassant.
De audit vooraf
Voordat je ook maar één regel code schrijft, doe je een audit. Drie dagen read-only werk die je vertellen wat je daadwerkelijk hebt.
Inventariseer elke actieve subscription met een gekoppelde SEPA-betaalmethode. Per subscription leg je vast: de UMR, de IBAN (laatste vier), de datum van ondertekening, de methode (Stripe Checkout vooraf getekend, de Billing Portal, een eigen flow op je eigen domein), de huidige sequence (was de laatste incasso FRST of RCUR?) en de mandate reference URL die Stripe je geeft. Stripe maakt het meeste hiervan beschikbaar via de SEPA Direct Debit API op payment_method.sepa_debit en de bijbehorende mandate resource.
// Stripe export. Run against a read-only key, write to a flat file,
// never log mandate URLs to stdout.
const stripe = require('stripe')(process.env.STRIPE_LIVE);
const fs = require('fs');
const out = fs.createWriteStream('mandates.ndjson');
for await (const sub of stripe.subscriptions.list({
status: 'active',
expand: ['data.default_payment_method'],
limit: 100,
})) {
const pm = sub.default_payment_method;
if (pm?.type !== 'sepa_debit') continue;
const mandate = pm.sepa_debit.mandate
? await stripe.mandates.retrieve(pm.sepa_debit.mandate)
: null;
const sd = mandate?.payment_method_details?.sepa_debit;
out.write(JSON.stringify({
customer: sub.customer,
subscription: sub.id,
iban_last4: pm.sepa_debit.last4,
holder: pm.billing_details.name,
umr: sd?.reference,
signed_at: mandate?.created,
mandate_url: sd?.url,
creditor_id: sd?.creditor_identifier,
}) + '\n');
}
out.end();
De Creditor Identifier-beslissing
Hier ligt de splitsing. Er zijn twee paden en ze zijn niet inwisselbaar.
Pad A: je hebt je eigen Creditor Identifier behouden over PSPs heen. Dit kan alleen als je Stripe vanaf dag één hebt opgezet met je eigen SCI (Stripe noemt dit "use my own Creditor Identifier" in hun SEPA-setup). Als je dat hebt gedaan, reist de mandaatketen met je mee. Adyen registreert je SCI op hun platform, je uploadt de mandaatmetadata opnieuw, en terugkerende incasso's blijven doorlopen onder RCUR.
Pad B: je gebruikte de SCI van Stripe. De mandaten wijzen juridisch naar Stripe. Je kunt ze niet verplaatsen. Je verzamelt opnieuw mandaatconsent van elke betaler vóór de eerste incasso van Adyen. Voor 47.000 mandaten is dat een communicatieproject van zes weken: e-mail, SMS voor de bouncebacks, brieven per post voor de hoogwaardige relaties, en een webformulier dat een verse handtekening vastlegt met een IP-gestempelde audit trail.
Als je in het eerste uur van het project niet kunt beantwoorden 'wiens Creditor Identifier staat op het mandaat?', stop. Het antwoord verandert een engineeringklus van vier weken in een operationeel traject van zes maanden.
Importeren in Adyen
Stel: Pad A. Je hebt je SCI, je hebt de UMRs, je hebt de ondertekeningsgegevens. Adyen accepteert deze via een recurring contract import in de Customer Area, met een CSV die mapt op hun shopperReference (jouw stabiele klant-ID) en een SEPA-mandaatblok per regel.
De velden die je per regel nodig hebt: shopperReference, IBAN, naam van de rekeninghouder, mandate reference (jouw UMR, identiek houden, niet opnieuw genereren), datum van ondertekening, mandaattype (Core of B2B) en het sequence type voor de volgende incasso (RCUR als je midden in een cycle migreert). De SEPA Direct Debit-documentatie van Adyen is eerlijk over de voorwaarde: je hebt een contractuele bevestiging nodig van hun onboarding team dat ze de geïmporteerde mandaten honoreren onder jouw SCI. Krijg dat zwart op wit voordat je één regel importeert.
De dual-run
Je zet geen 47.000 subscriptions op een vrijdag in één keer om. Je draait dual-run voor minstens één volledige billing cycle.
Kies een cohort: 1% van de betalers, lage bedragen, met e-mailadressen die daadwerkelijk werken. Verhuis ze naar Adyen. Schrijf af via Adyen. Volg de R-transacties twee weken lang. SEPA-returns kunnen tot vijf werkdagen aankomen om technische redenen (AC04, AC06) en tot acht weken voor ongeautoriseerde claims (MD06). Reconcileer tegen je billing ledger.
Schaal per cohort: 1%, 5%, 25%, 100%. In elke fase bevries je de bijbehorende subscriptions op Stripe (cancel at period end, auto-collection uit) zodat je niet dubbel kunt incasseren als er een webhook-race optreedt.
Webhook-reconciliatie
Tijdens dual-run luistert je billing-systeem naar invoice.payment_succeeded van Stripe en AUTHORISATION plus REPORT_AVAILABLE van Adyen. De valkuil is ze als gelijkwaardig behandelen. Dat zijn ze niet.
Stripe stuurt invoice.payment_succeeded optimistisch bij indienen, en pas later een aparte charge.failed als de SEPA-incasso teruggestuurd wordt. Adyen scheidt AUTHORISATION (we hebben de instructie geaccepteerd) van het settlement report dat twee à drie werkdagen later binnenkomt. Je reconciliatie mag een subscription pas op 'betaald' zetten als je settlement-bevestiging hebt, anders lever je toegang uit op een incasso die een week later teruggeboekt wordt.
// Reconciler: a charge is "paid" only after settlement confirms.
function onWebhook(event) {
if (event.source === 'stripe' && event.type === 'charge.succeeded') {
db.markPending(event.data.id, { psp: 'stripe', amount: event.data.amount });
}
if (event.source === 'adyen' && event.eventCode === 'AUTHORISATION') {
db.markPending(event.pspReference, { psp: 'adyen', amount: event.amount.value });
}
if (event.type === 'charge.failed' || event.eventCode === 'NOTIFICATION_OF_CHARGEBACK') {
db.markReturned(event.reference, { reason: event.reason });
}
if (event.eventCode === 'REPORT_AVAILABLE') {
settlementReport.apply(event); // promotes pending -> paid
}
}
Pre-notificatie en de 14-dagenregel
SEPA Core vereist dat de incassant de betaler minstens 14 kalenderdagen vóór een incasso informeert, tenzij de mandaattekst dit venster verkort. De meeste goed opgestelde mandaten verkorten het naar één of twee dagen voor terugkerende incasso's met een vast bedrag.
De migratie zelf is geen 'eerste' incasso vanuit het perspectief van de betaler, zolang je SCI en UMR behouden blijven. Incasso's lopen door onder RCUR en het bestaande notificatievenster blijft staan. Heb je Pad B gevolgd en opnieuw consent verzameld, dan is de volgende incasso FRST en reset de 14-dagen-notificatie, tenzij het nieuwe mandaat hem verkort. Stuur de pre-notificatie op de dag dat Adyen de file indient. Bewaar verzendbewijs voor de audit aan verkoperszijde.
Wat er echt stuk gaat
Drie dingen, in volgorde van hoe vaak ze bij ons stuk gingen.
R-transacties die bij de oude PSP binnenkomen. Stripe ontvangt een return voor een incasso die vier dagen eerder via Stripe is ingediend, en je reconciler, die al naar Adyen wijst, laat hem vallen. Houd Stripe-webhooks minstens 90 dagen levend na de laatste Stripe-incasso. Voor Adyen geldt hetzelfde, andersom.
Geschillen die binnen het acht-wekenvenster voor onbevoegd worden geopend. Een betaler die in maart via Stripe heeft betaald, kan begin mei via zijn bank een geschil openen. Het geschil komt binnen op het Stripe-account. Je hebt een klein refunds-only operator playbook nodig voor het restant-Stripe-account, en je budgetteert de dispute fees in de migratiekosten.
Subscriptions die na de migratie nog op Stripe worden aangepast. Een customer-support medewerker werkt in mei een Stripe-subscription bij omdat de klant nog in het systeem staat. Stripe probeert een incasso tegen een mandaat dat je al hebt verhuisd. Schakel schrijftoegang tot Stripe-subscriptions uit op de dag dat je de ramp afrondt, en verwijder de live API-key uit elke operator tool.
Het moeilijke aan een PSP-migratie is niet de API. Het is het venster van 90 dagen waarin twee systemen elk een deel van de waarheid over hetzelfde mandaat houden, en één van de twee zit ernaast.
De cutover
Op de dag van de laatste ramp: pauzeer nieuwe Stripe-incasso's via subscription updates (auto-advance uit, pause collection aan), bevestig het laatste Stripe settlement report, schakel de recurring-charge writer van je applicatie om naar Adyen, stuur de SEPA pre-notificatie batch, dien de eerste batch van Adyen in onder RCUR met de geïmporteerde UMRs, en volg de AUTHORISATION-webhooks 24 uur lang. De eerste returns komen binnen op dag twee tot dag vijf. Een returnpercentage boven 1,5% in week één betekent dat een mandate mapping verkeerd staat. Trek de file en diff hem tegen de Stripe-export.
Het saaie werk zit in de kalender
De meeste migraties die we zien lopen vast op data, niet op code. De verlenging van de raamovereenkomst ligt vast. De onboarding van Adyen kost zes tot tien weken met KYC en SCI-registratie. De ramp van 1%/5%/25%/100% kost nog eens zes tot acht weken. Het 90-dagen return-venster op de oude PSP betekent dat Stripe gedeeltelijk open blijft tot ongeveer vijf maanden na de eerste migratiebatch. Zet het op een Gantt voordat je één ticket aanmaakt, en reken terug wat de inkoopdeadline werkelijk vereist dat je start.
Toen we de recurring-billing migratie bouwden voor een Nederlandse publieke-sector klant, was de valkuil aannemen dat het importformaat van Adyen en de mandate-export van Stripe een UMR-conventie deelden. Dat doen ze niet, en de translator die we schreven om de twee enveloppen te overbruggen bleek het leeuwendeel van het werk in zo'n payments integration.
De kleinste nuttige stap voor vandaag: draai het Stripe-exportscript hierboven met een read-only key op staging, en tel hoeveel van je mandaten een Creditor Identifier dragen die jij beheert tegenover die van Stripe. Het antwoord vertelt je of je volgende kwartaal een sprint of een project wordt.
Kern
Een PSP-migratie is geen API-uitwisseling. Het is het beheer van de mandaatketen, en het echte werk zit in het venster van 90 dagen waarin twee systemen het oneens zijn over dezelfde betaler.
FAQ
Kan ik de Creditor Identifier van Stripe behouden bij de overstap naar Adyen?
Nee. De SCI hoort bij de PSP, tenzij je vanaf dag één je eigen SCI hebt geregistreerd. Bezit Stripe hem, dan moet je opnieuw mandaten verzamelen bij elke betaler onder een nieuwe SCI voordat Adyen kan incasseren.
Hoe lang moet de dual-run duren?
Minstens één volledige billing cycle plus acht weken. Het acht-wekenvenster voor ongeautoriseerde geschillen is wanneer de lastigste R-transacties binnenkomen, en beide PSPs moeten live zijn om ze op te vangen.
Activeert de migratie een SEPA-pre-notificatie?
Als SCI en UMR behouden blijven, lopen incasso's door als RCUR en blijft het bestaande notificatievenster staan. Heb je opnieuw consent verzameld, dan is de volgende incasso FRST en geldt de 14-dagenregel, tenzij het nieuwe mandaat hem verkort.
Wat is de meest voorkomende fout bij de eerste Adyen-incasso?
Een mismatch in de Creditor Identifier. De bank stuurt terug met reden MD07. Bevestig schriftelijk dat Adyen jouw SCI honoreert op geïmporteerde mandaten voordat je één instructie indient.