Integrations
Dubbele Mollie-refunds: de idempotency key die we misten
Op een vrijdagavond begon de refund-queue van een Nederlands modemerk dubbel te draaien. Elf uur later hadden we het door. Wat brak er, en welke key lost het op.

Het Slack-bericht van 22:47
Om 22:47 op een vrijdag in maart kregen we een berichtje van de finance-ops lead bij een Nederlands modemerk. "Hebben jullie vandaag iets uitgerold? Ons Mollie-dashboard laat twee refunds per order zien op elke retour die we verwerkt hebben." Haar screenshot was glashelder. 412 retouren. 824 refund-regels. Zelfde bedragen, zelfde IBAN's, zelfde producten, ongeveer 90 seconden uit elkaar afgevuurd.
Het merk draaide al elf maanden op een Make-scenario dat we eerder hadden gebouwd. Het volgde hun retourportaal, valideerde de RMA en riep daarna Mollie aan om de refund uit te voeren. Het had 38.000 retouren verwerkt zonder kuren. Tot die middag, toen het op elke retour dubbel begon af te vuren.
Dit is de incident-walkthrough. Wat het scenario deed, waarom het brak, hoe we het elf uur later doorhadden dan zou moeten, en de fix van vier regels die dit vanaf dag één onmogelijk had gemaakt.
De architectuur
De opzet was bewust saai. Het retourportaal POST een event naar een Make-webhook. Make haalt het originele Mollie payment ID uit een Google Sheet, valideert het refund-bedrag tegen de orderregel en roept daarna Mollie's refund endpoint aan. Bij succes schrijft het een regel naar de sheet en mailt de klant.
De refund-call zag er ongeveer zo uit:
POST https://api.mollie.com/v2/payments/tr_WDqYK6vjvE/refunds
Authorization: Bearer live_*****
Content-Type: application/json
{
"amount": { "currency": "EUR", "value": "59.95" },
"description": "Return RMA-48211"
}
Geen idempotency-header. Geen request-hash. De ingebouwde HTTP-module van Make voegt er standaard geen toe, en we hebben er nooit om gevraagd. In elf maanden schone traffic was dat nooit nodig geweest.
Hoe het misging
Op de dag van het incident had Mollie een korte vertraging op de refunds-endpoint. De mediane responstijd schoot van zo'n 180ms naar zo'n 14 seconden, gedurende ongeveer negentig minuten. De HTTP-module van Make heeft een standaard request-timeout van 40 seconden, maar dit scenario was strakker gezet op 8 seconden omdat we snel wilden falen op de klantgerichte webhook.
Make deed dus de refund-call. Mollie ontving 'm, accepteerde 'm, begon te verwerken. Na 8 seconden had Mollie nog geen 201 teruggegeven. De HTTP-module van Make gooide een timeout-error. Het scenario was ingesteld om opnieuw te proberen bij voorbijgaande HTTP-errors. Het probeerde opnieuw. Mollie behandelde de tweede call als een gloednieuw request, want niets in de payload identificeerde hem als een replay. De eerste call rondde uiteindelijk af. Daarna rondde de retry af. Twee refunds op dezelfde retour.
Dit is de simpelste failure mode in elke betaalintegratie. We wisten dat hij bestond. We hebben er voor andere klanten over geschreven. We hadden het alleen niet in dít scenario verwerkt.
Als je automatiseringstool gefaalde HTTP-calls opnieuw probeert en je betaalprovider ondersteunt idempotency keys, dan is het ontbreken van die keys geen stilistische keuze. Het is een latente bug die wacht op één trage middag bij je provider.
De elf uur die we kwijt waren
De eerste dubbele refund viel om 11:52. Finance-ops had het door om 22:47. Elf uur en een beetje. Hardop gezegd: geen enkele laag van onze monitoring zag dit.
Waarom niet?
Het Make-scenario meldde succes op elke run, want zowel de originele call als de retry kregen 201 terug. Vanuit Make gezien rondde het scenario die dag 412 keer netjes af. Onze error-rate-alert hing aan scenario-failures. Die waren er niet.
De Google Sheet zag de dubbele refund-regels wel, maar alleen via een dagelijks reconciliatie-script dat om 02:00 draaide. Dat had het incident gevangen, maar pas na nóg vier uur bloeden.
Het dashboard van Mollie liet de dubbelen direct zien. Niemand keek ernaar.
De les, los van de idempotency-fix: success-rate monitoring op de automatiseringslaag zegt je niets over correctheid aan de bestemming. We reconciliëren nu elke vijftien minuten tegen de Mollie API, niet één keer per dag.
De fix van vier regels
De API van Mollie ondersteunt al jaren een Idempotency-Key-header. Stuur dezelfde key twee keer en de tweede call geeft de originele response terug zonder een nieuwe refund aan te maken. Hun docs zijn helder: "the resource will not be created or processed more than once." Hetzelfde patroon is het standaardantwoord bij Stripe, Adyen en elke serieuze betaal-API.
De fix is een stabiele key genereren per bedoelde refund (niet per HTTP-call) en die meesturen met het request. In Make is dat één extra header op de HTTP-module, gevoed door een deterministisch veld dat retries overleeft. Wij gebruiken het RMA-nummer plus de refund-regelindex, gehasht.
POST https://api.mollie.com/v2/payments/tr_WDqYK6vjvE/refunds
Authorization: Bearer live_*****
Idempotency-Key: rma-48211-line-1-9c2f8b
Content-Type: application/json
{
"amount": { "currency": "EUR", "value": "59.95" },
"description": "Return RMA-48211"
}
Als je Mollie vanuit Node of PHP aanroept in plaats van Make, geldt hetzelfde principe. Hier is de Node-variant met de officiële client:
import createMollieClient from "@mollie/api-client";
import crypto from "node:crypto";
const mollie = createMollieClient({ apiKey: process.env.MOLLIE_KEY });
async function refundReturn(rma, paymentId, amount) {
const key = crypto
.createHash("sha1")
.update(`refund:${rma}:${amount.value}`)
.digest("hex")
.slice(0, 16);
return mollie.payment_refunds.create({
paymentId,
amount,
description: `Return ${rma}`,
idempotencyKey: key,
});
}
De key wordt afgeleid uit data die stabiel is over retries heen. Gebruik geen verse UUID die binnen het retry-blok wordt gegenereerd, dat ondergraaft het hele mechanisme. Gebruik geen timestamp. Gebruik de business-identifier van het ding dat je één keer probeert te doen.
Waarom de defaults van Make dit makkelijk maken om te missen
De HTTP-module van Make is fantastisch, en dat is een deel van het probleem. Je richt 'm op een willekeurige REST-endpoint, zet de auth, plakt de JSON-body en je bent live. De Headers-sectie staat standaard ingeklapt. Er komt geen waarschuwing als je naar een betaal-endpoint POST zonder idempotency, want Make weet niet dat die endpoint bijzonder is. De provider weet het wel. De tool niet.
Hetzelfde geldt voor Zapier, n8n en elke low-code-automatiseringsrunner. Het zijn payload-koeriers. De semantiek van "deze call mag precies één keer draaien" zit in jouw hoofd en in de docs van de provider, niet in de workflow-editor. Bouw je betaalstromen in deze tools, dan is dat semantische gat de plek waar je incidenten vandaan komen.
Waar dit zich verstopt in je stack
Mollie-refunds zijn de voor de hand liggende plek. Minder voor de hand liggende plekken waar we dezelfde bug dit jaar tegenkwamen in klantsystemen:
- Stripe-charges afgevuurd vanuit een Zapier-scenario waar de trigger een Shopify-webhook is die Shopify zelf soms opnieuw aflevert.
- WooCommerce order-completed webhooks die door een Make-scenario worden opgepikt en doorgestuurd naar een fulfilment-API van een derde partij, zonder dedupe aan de ontvangende kant.
- Transactionele mailstappen die de klant twee keer mailen wanneer de upstream-provider traag is, zonder
X-Idempotency-Keybij de mailprovider. - Interne "factuur als betaald markeren"-calls in een ERP, aangeroepen door een queue-worker die op elke 5xx opnieuw probeert.
Overal waar een retry een side-effecting POST tegenkomt zonder key, heb je deze bug. Hij blijft stil tot je provider tien trage minuten heeft.
Een audit van vijf minuten die je vandaag kunt doen
Draai je automatisering die refunds, charges, mails of berichten verstuurt, doe dan dit voor je vanavond je laptop dichtklapt:
- Lijst elke stap op in elk scenario dat POST naar een externe API met een side effect.
- Check per stap of de API een idempotency-header ondersteunt. Mollie, Stripe, Adyen, GoCardless, SendGrid, Postmark en Twilio doen het allemaal. Lees hun docs, niet je geheugen.
- Voor elke API die het ondersteunt: check of jouw scenario er ook één meestuurt. In Make is dat de Headers-sectie van de HTTP-module. In code: grep op de SDK-optie.
- Voor elke API die het ondersteunt maar waar je 'm niet meestuurt: schrijf de business-identifier op die de actie uniek benoemt. Refund van RMA-48211. Charge voor factuur 2026-0488. Welkomstmail voor user 9132.
- Voeg de header toe. Hash de identifier als hij gevoelig is. Uitrollen.
Deze audit duurt meestal langer dan vijf minuten omdat stap drie ongemakkelijke antwoorden boven tafel haalt. Dat is precies de bedoeling.
Idempotency keys zijn geen optimalisatie. Ze zijn het verschil tussen een retry die geneest en een retry die je aansprakelijkheid verdubbelt.
Wat we na het incident hebben veranderd
Het merk is volledig gecompenseerd. Het supportteam van Mollie hielp ons om alle dubbele refund-ID's uit het getroffen tijdvenster te identificeren, en we hebben de 412 extra refunds binnen twee werkdagen via hun dashboard geannuleerd. Geen enkele klant zag uiteindelijk twee refunds op zijn afschrift.
We hebben drie dingen aangepast in het scenario. Eén, elke Mollie-call draagt nu een afgeleide idempotency key die uit de RMA komt. Twee, de scenario-timeout staat weer op de door Mollie aanbevolen 40 seconden, want optimaliseren op snel falen bij een betaal-call was het verkeerde instinct. Drie, een apart Make-scenario polt Mollie elke vijftien minuten en reconciliëert refund-aantallen tegen het retourportaal. Een mismatch triggert een Slack-page, geen mail.
Toen we de retour-automatisering voor deze klant bouwden, was het gat dat we het scenario zonder idempotency-header live zetten omdat Make het makkelijk maakte om hem weg te laten. De fix was vier regels en één header. De reden dat het elf uur duurde om het te detecteren was dat niets stroomafwaarts van Make naar het echte geld keek. Draai je procesautomatisering die betalingen raakt, dan is de reconciliatielaag niet optioneel.
Het kleinste wat je vandaag kunt doen: open de HTTP-module in je duurste automatisering, vind de side-effecting POST en voeg één header toe. Kies de business-identifier, hash 'm, ship. De audit komt daarna.
Kern
Idempotency keys zijn geen optimalisatie. Ze zijn het verschil tussen een retry die geneest en een retry die je aansprakelijkheid verdubbelt.
FAQ
Wat doet een idempotency key eigenlijk?
Hij vertelt de API dat een request met dezelfde key als een eerdere een replay is, geen nieuwe actie. De provider geeft de originele response terug en maakt geen nieuwe resource aan.
Ondersteunt Make.com idempotency keys?
Ja. Voeg de idempotency-header van de provider toe (bijvoorbeeld Idempotency-Key bij Mollie en Stripe) in de Headers-sectie van de HTTP-module. De waarde moet afgeleid zijn uit een stabiele business-identifier, niet uit een verse UUID.
Wat moet ik als waarde voor de key gebruiken?
De identifier van het ding dat je één keer probeert te doen: een RMA-nummer, een factuur-ID, een user-action-ID. Hash 'm als hij gevoelig is. Gebruik nooit een timestamp of een UUID die binnen de retry wordt gegenereerd.
Hoe vind ik dubbele afvuringen die al gebeurd zijn?
Query de API van je provider voor refunds (of charges, of berichten), gegroepeerd op bedrag en source-ID binnen een strak tijdvenster. De list-refunds endpoint van Mollie plus een 5-minuten-bucket haalt ze snel boven water.