Integrations
Adyen migration playbook: keeping SEPA mandates alive
A Dutch government department, 47,000 active SEPA mandates, and a procurement clock. The playbook for swapping recurring billing from Stripe to Adyen without losing a mandate.

It's a Tuesday morning in March. The finance lead at a Dutch government department opens a ticket from procurement: switch the recurring-billing SaaS from Stripe to Adyen before the fiscal year closes. Forty-seven thousand active SEPA mandates. Citizen-payers, not corporates. A hard deadline tied to the framework agreement. Nobody on the team has migrated a payment processor before, and procurement's assumption (based on a single LinkedIn post) is that it's "swapping the API keys."
It isn't. The reason it isn't shows up the moment you read the EPC's SEPA Direct Debit Core Rulebook, or the day the first Adyen test charge comes back with reason code MD07. Mandates are not data. They are a chain of authorization that belongs to the creditor and is operated by the PSP, and once you change PSPs you are touching every link in that chain.
Every PSP migration write-up we have read makes the same mistake: it treats the API as the hard part. The API is two days of work. The mandate chain is the rest of the quarter. The engineering actually lives in the authorization that sits between a debtor's bank, your Creditor Identifier, and whichever PSP is operating the file delivery on the day the file goes out.
What the SEPA mandate chain actually is
A SEPA Direct Debit mandate is a signed authorization from a debtor allowing a specific creditor, identified by a Creditor Identifier (the SCI), to pull funds via direct debit. Each mandate carries a Unique Mandate Reference (UMR), an IBAN, signing data (date, place, signature method), and a sequence type: FRST for the first collection, RCUR for recurring ones, FNAL for the last, OOFF for one-offs.
When you use Stripe Billing, Stripe is both the SCI of record (their creditor ID is used unless you negotiated otherwise) and the holder of the mandate proof. The signed mandate text and the authentication metadata sit on their side. From your application's view you have a payment_method and a mandate object. You do not, in most setups, have the raw signed mandate ready to hand to another PSP.
Adyen, like every regulated SEPA PSP, will not accept a recurring debit instruction without evidence the mandate exists, who the creditor is, and what sequence type to send. If you switch without coordinating, the next collection goes out under Adyen's creditor ID against a mandate the debtor signed for Stripe's creditor ID. The bank returns it. Reason code MD07: incorrect creditor.
The pre-migration audit
Before any code, you do an audit. Three days of read-only work that tells you what you actually have.
Inventory every active subscription with an attached SEPA payment method. For each one, you record the UMR, the IBAN (last four), the signing date, the signature method (Stripe Checkout signed-up-front, the Billing Portal, a custom flow on your own domain), the current sequence (was the last charge FRST or RCUR?), and the mandate reference URL Stripe gives you. Stripe exposes most of this through the SEPA Direct Debit API on payment_method.sepa_debit and the associated 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();
The Creditor Identifier decision
This is the fork. There are two paths and they are not interchangeable.
Path A: you kept your own Creditor Identifier across PSPs. This is only possible if you set up Stripe with your own SCI from day one (Stripe calls this "use my own Creditor Identifier" in their SEPA setup). If you did, the mandate chain travels with you. Adyen registers your SCI on their platform, you re-upload the mandate metadata, and recurring collections continue under RCUR.
Path B: you used Stripe's SCI. The mandates legally point at Stripe. You cannot move them. You re-collect mandate consent from every debtor before Adyen's first collection. For 47,000 mandates that is a six-week communications project: email, SMS for the bounces, mailed letters for the high-value ones, and a web form that captures a fresh signature and an IP-timestamped audit trail.
If you cannot answer "whose Creditor Identifier is on the mandate?" in the first hour of the project, stop. The answer changes a four-week engineering job into a six-month operational one.
Importing into Adyen
Assume Path A. You have your SCI, you have the UMRs, you have the signing data. Adyen accepts these through a recurring contract import in the Customer Area, with a CSV that maps to their shopperReference (your stable customer ID) and a SEPA mandate block per row.
The fields you need per row: shopperReference, IBAN, account holder name, mandate reference (your UMR, kept identical, do not re-mint), date of signature, mandate type (Core or B2B), and the sequence type for the next collection (RCUR if you migrated mid-cycle). Adyen's SEPA Direct Debit documentation is honest about the constraint: you need contractual confirmation from their onboarding team that they will honour the imported mandates under your SCI. Get that in writing before you import a single row.
The dual-run
You do not flip 47,000 subscriptions on a Friday. You dual-run for at least one full billing cycle.
Pick a cohort: 1% of debtors, low-value, with email addresses that actually resolve. Move them to Adyen. Charge them on Adyen. Watch the R-transactions for two weeks. SEPA returns can arrive up to five business days for technical reasons (AC04, AC06) and up to eight weeks for unauthorized claims (MD06). Reconcile against your billing ledger.
Ramp by cohort: 1%, 5%, 25%, 100%. At each stage, freeze the corresponding subscriptions on Stripe (cancel at period end, auto-collection off) so you cannot double-charge if a webhook race fires.
Webhook reconciliation
During dual-run, your billing system listens to invoice.payment_succeeded from Stripe and AUTHORISATION plus REPORT_AVAILABLE from Adyen. The trap is treating them as equivalent. They are not.
Stripe fires invoice.payment_succeeded optimistically at submission, then a separate charge.failed if the SEPA debit comes back later. Adyen separates AUTHORISATION (we accepted the instruction) from the settlement report that lands two to three business days later. Your reconciliation cannot mark a subscription "paid" until you have settlement confirmation, otherwise you ship access on a debit that gets returned a week later.
// 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-notification and the 14-day rule
SEPA Core requires the creditor notify the debtor at least 14 calendar days before a collection, unless the mandate text shortens this window. Most well-drafted mandates shorten it to one or two days for recurring same-amount charges.
The migration itself is not a "first" collection from the debtor's point of view, as long as your SCI and UMR are preserved. Collections continue under RCUR and the existing notice window stands. If you went Path B and re-collected consent, the next collection is FRST and the 14-day notice resets unless the new mandate shortens it. Send the pre-notification on the day Adyen submits the file. Keep proof of send for the seller-side audit.
What actually breaks
Three things, in order of how often they have broken on us.
R-transactions arriving at the old PSP. Stripe receives a return for a collection submitted on Stripe four days earlier, and your reconciler, already pointing at Adyen, drops it. Keep Stripe webhooks live for at least 90 days after the last Stripe collection. Adyen is the same in reverse.
Disputes opened in the eight-week unauthorized window. A debtor who paid Stripe in March can dispute via their bank in early May. The dispute hits Stripe's account. You need a small refunds-only operator playbook for the residual Stripe account, and budget for the dispute fees in the migration cost.
Subscriptions modified on Stripe after migration. A customer-support agent updates a Stripe subscription in May because the customer record still exists. Stripe attempts a charge against a mandate you have already moved. Disable write access to Stripe subscriptions the day you finish the ramp, and remove the live API key from any operator tool.
The hard part of a PSP migration is not the API. It is the 90-day window where two systems hold partial truths about the same mandate, and one of them is wrong.
The cutover
On the day of the final ramp: pause new Stripe collections via subscription updates (auto-advance off, pause collection set), confirm the last Stripe settlement report, flip your application's recurring-charge writer to Adyen, send the SEPA pre-notification batch, submit Adyen's first batch under RCUR with the imported UMRs, and watch the AUTHORISATION webhooks for 24 hours. The first returns arrive day two to day five. Anything past a 1.5% return rate in week one means a mandate mapping is wrong. Pull the file and diff it against the Stripe export.
The boring part is the calendar
Most migrations we see fail on dates, not code. The framework agreement renewal is fixed. Adyen's onboarding takes six to ten weeks with KYC and SCI registration. The 1%/5%/25%/100% ramp eats another six to eight weeks. The 90-day return window on the old PSP means Stripe stays partially open until roughly five months after the first migration batch. Plot it on a Gantt before you cut a single ticket, and back-solve what the procurement deadline actually requires you to start.
When we built the recurring-billing migration for a Dutch public-sector client, the trap was assuming Adyen's import format and Stripe's mandate export shared a UMR convention. They don't, and the translator we wrote to bridge the two envelopes turned out to be the bulk of the work in that kind of payments integration.
Today's smallest useful step: run the Stripe export script above against a read-only key on staging, then count how many of your mandates carry a Creditor Identifier you control versus Stripe's. The answer tells you whether your next quarter is a sprint or a project.
Key takeaway
A PSP migration is not an API swap. It is custody of the mandate chain, and the real work lives in the 90-day window where two systems disagree about the same debtor.
FAQ
Can I keep Stripe's Creditor Identifier when moving to Adyen?
No. The SCI belongs to the PSP unless you registered your own from day one. If Stripe owns it, you have to re-collect mandates from every debtor under a new SCI before Adyen can collect.
How long should the dual-run last?
At least one full billing cycle plus eight weeks. The eight-week unauthorized-dispute window is when the trickiest R-transactions land, and both PSPs need to be live to absorb them.
Does the migration trigger SEPA pre-notification?
If SCI and UMR are preserved, collections continue as RCUR and the existing notice window stands. If you re-collected consent, the next charge is FRST and the 14-day rule applies unless the new mandate shortens it.
What is the most common Adyen first-charge failure?
Mismatched Creditor Identifier. The bank returns reason MD07. Confirm in writing that Adyen will honour your SCI on imported mandates before you submit a single instruction.