De gevaarlijkste knop in een betaalintegratie is niet verwijderen, maar opnieuw proberen. Een timeout kan betekenen dat de betaling, refund of ERP-boeking al is uitgevoerd, terwijl jij het antwoord niet hebt gezien. Wie dan met een nieuwe sleutel opnieuw begint, maakt van herstel een tweede financiële gebeurtenis.
Mijn stelling is eenvoudig: een financiële integratie is pas betrouwbaar wanneer herstel een volwaardige financiële gebeurtenis is. Iedere retry en replay moet dezelfde zakelijke handeling herkennen, eerst kunnen terugvallen op een eigen ledger en bij onzekerheid eindigen bij een eigenaar. Niet bij een tweede boeking.
Een financiële replay is geen nieuwe opdracht, maar een nieuwe poging om dezelfde opdracht vast te stellen of af te ronden. Dat lukt alleen wanneer de integratie de gebeurtenis, de zakelijke handeling en het eerdere resultaat uit elkaar houdt. De sleutel voor een retry hoort daarom bij de handeling, terwijl een eigen ledger bewaart wat er werkelijk is gebeurd.
De fout ontstaat tussen ontvangst en zekerheid
Een betaal- of ERP-keten bestaat uit gebeurtenissen die op elkaar lijken, maar niet hetzelfde betekenen. Een order zegt dat iemand iets heeft besteld. Een betaaltransactie zegt dat een betaalprovider een geldactie kent. Een payout bundelt transacties voor een uitbetaling. Een refund draait geld terug. Een chargeback opent een geschil. Een ERP-boeking legt vast hoe die feiten in de administratie worden geïnterpreteerd.
Dat onderscheid klinkt theoretisch totdat een API stilvalt na een POST. De aanvraag kan de andere kant al hebben bereikt. Alleen het antwoord ontbreekt nog. Een workflow die de fout als een gewone mislukking behandelt, verstuurt dezelfde opdracht opnieuw. Een workflow die de fout als succes behandelt, kan een onvolledige boeking achterlaten. De echte toestand is op dat moment onzeker.
Dat is precies waarom order, betaaltransactie, payout en boeking elk hun eigen bewijs moeten leveren. Een groen veld in de webshop is geen bewijs dat de payout is ontvangen. Een refundrecord is geen bewijs dat de terugbetaling geslaagd is. De herstelroute moet dus kunnen teruggaan naar de bron en daarna opnieuw vaststellen welke financiële gebeurtenis al bestaat.
De documentatie van Stripe beschrijft webhooklevering ook niet als een keurige rij die je blind kunt aflopen. Events kunnen dubbel binnenkomen, en Stripe probeert liveleveringen tot drie dagen opnieuw te bezorgen. De documentatie adviseert event-ID’s te registreren en niet te vertrouwen op de volgorde van gebeurtenissen. Die eigenschappen zijn onderdeel van het webhookcontract, geen uitzonderlijke storing die je pas na livegang hoeft te bedenken.
Een event-id is niet jouw financiële sleutel
De eerste ontwerpfout is een event-ID verwarren met de sleutel van de zakelijke handeling. Een event-ID identificeert één bezorging. Een payment-ID, refund-ID of payout-ID identificeert een object bij de betaalprovider. Een ERP-boeking heeft weer een eigen doel-ID. Geen van die sleutels zegt vanzelf dat twee berichten dezelfde handeling in jouw proces vertegenwoordigen.
Stel dat een betaalprovider twee keer meldt dat betaling pay_481 is geslaagd. De twee berichten zijn verschillende bezorgingen, maar ze horen bij één betaling. Vervolgens kan een payoutbericht dezelfde betaling opnieuw noemen omdat de settlementinformatie later beschikbaar komt. Een derde bericht kan een chargeback op die betaling melden. Wie op ieder event-ID een boeking zet, heeft technisch drie unieke berichten en financieel één betaling plus één geschil.
Daarom hoort een idempotente sleutel de bedoeling te beschrijven, niet alleen de bron. Bijvoorbeeld stripe:pay_481:erp-payment, stripe:refund_903:creditnota of mollie:payout_77:clearing-match. De sleutel zegt welke bronhandeling je voor welk doel uitvoert. Eenzelfde sleutel betekent: controleer of deze handeling al klaar is. Een nieuwe sleutel betekent: er is een nieuwe zakelijke beslissing genomen. Dat is een veel zwaardere grens dan “de workflow draait nog een keer”.
Deze scheiding maakt replay veilig. Je kunt een gemiste of onzekere gebeurtenis opnieuw aanbieden zonder de oorspronkelijke betaling, refund of boeking als nieuw te behandelen. Je kunt ook een nieuw besluit nemen, bijvoorbeeld een afzonderlijke creditnota voor een tweede deelrefund, zonder per ongeluk de sleutel van de eerste refund te hergebruiken.
De ledger is je geheugen, een provider-key is een vangnet
Een externe idempotentiesleutel helpt, maar hij is niet jouw financiële geheugen. Stripe bewaart het resultaat van de eerste aanvraag bij een sleutel en geeft een volgende aanvraag hetzelfde resultaat terug. Tegelijk kunnen sleutels automatisch worden verwijderd zodra ze minstens 24 uur oud zijn. Daarna kan hergebruik als een nieuwe aanvraag worden behandeld. Dat gedrag staat in de actuele Stripe-API-documentatie.
Mollie hanteert een andere bewaartermijn. De API cachet dezelfde sleutel binnen één uur, terwijl een hergebruikte sleutel daarna als een nieuwe aanvraag geldt. Mollie waarschuwt bovendien dat twee gedeeltelijke refunds naast elkaar kunnen worden uitgevoerd als een retry niet dezelfde idempotentiesleutel gebruikt. Die waarschuwing staat rechtstreeks in de uitleg over API-idempotentie.
Mijn conclusie is niet dat je providersleutels moet wantrouwen. Mijn conclusie is dat je ze moet begrenzen. Ze beschermen een API-call binnen de regels van die provider. Ze bewaren niet waarom jouw bedrijf de refund vrijgaf, welke creditnota erbij hoort, of wie een onzekere payout heeft gecontroleerd. Daarvoor heb je een eigen ledger nodig.
Die ledger hoeft geen volledig nieuw boekhoudpakket te worden. Hij moet vóór de eerste onomkeerbare nevenwerking vastleggen welke handeling je gaat uitvoeren, met welke sleutel, op basis van welke bron, voor welk doel en met welke status. Na afloop bewaart hij het doel-ID, de providerrespons, de laatste poging en de reden voor een eventueel herstel. Een verwerkingsledger vóór de eerste nevenwerking maakt dubbele webhook, timeout en replay toetsbaar.
De volgorde is hier de kern. Eerst registreren dat stripe:refund_903:creditnota wordt verwerkt. Daarna pas de creditnota aanmaken. Bestaat dezelfde sleutel al met een geslaagd doel-ID, dan is replay een no-op. Staat hij op processing, dan is een gelijktijdige retry mogelijk. Staat hij op failed, dan mag alleen het afgesproken herstelpad opnieuw proberen. Een logregel achteraf kan dat besluit niet meer betrouwbaar nemen.
Payouts, refunds en chargebacks zijn verschillende herstelgevallen
De tweede ontwerpfout is iedere financiële gebeurtenis als een generieke “transactie” te behandelen. Een payout is vaak een bundel waarin meerdere betalingen, refunds en kosten samenkomen. Stripe beschrijft het payout-reconciliatierapport daarom als een manier om bankuitbetalingen te koppelen aan de batches en categorieën die het bedrag vormen, waaronder charges, refunds en fees. De officiële rapportdocumentatie laat die uitsplitsing zien.
Een retry van een payout-import moet dus controleren of de payout al als settlement is vastgelegd. Hij mag niet opnieuw de omzet van alle onderliggende betalingen boeken. Een retry van een refund is nog gevoeliger: daar kan de geldactie zelf opnieuw worden aangeroepen. Een retry van een chargeback is weer iets anders. Het geschil kan nieuw bewijs, een deadline of een andere beslissing vragen, zonder dat er opnieuw een refund hoort te worden uitgevoerd.
Ook de ERP-kant heeft een eigen sleutel en eigen betekenis. Een payment-event kan leiden tot een openstaande post, een payout tot een clearingmatch, een refund tot een creditnota en een chargeback tot een apart onderzoeksdossier. Het zijn verbonden gebeurtenissen, geen vier varianten van dezelfde boeking. Als de koppeling dat verschil niet bewaart, wordt een herstelpoging al snel een boekingspoging.
Dat is waarom ik financiële replay niet als een foutafhandeling zie. Het is een vorm van reconciliatie. Je vergelijkt de brongebeurtenis met de bestaande ledgerregel, het providerobject, de payout of settlement en het ERP-doel. Pas wanneer die feiten bij elkaar passen, weet je of je moet afronden, opnieuw proberen of stoppen.
Om eerlijk te zijn: niet iedere koppeling heeft een eigen ledger nodig
Het tegenargument is sterk. Een eenvoudige connector tussen twee systemen moet niet veranderen in een zwaar financieel platform. Veel API’s bieden idempotentie al aan. Voor een leesactie, een tijdelijke synchronisatie of een neveneffect dat veilig opnieuw kan, is een extra tabel misschien meer beheer dan waarde. Ook een klein bedrijf wil geen architectuurproject optuigen voor één webhook.
Daar ben ik het mee eens. Ik zou geen eigen ledger bouwen voor iedere GET, iedere zoekactie of iedere status die vanzelf opnieuw kan worden opgehaald. De grens ligt bij een actie die geld, voorraad, omzet, een creditnota of een externe toezegging verandert. Vanaf dat moment is “de provider heeft waarschijnlijk al iets gedaan” geen herstelstrategie meer.
De ledger hoeft dan nog steeds niet groot te zijn. Eén duurzame tabel met een unieke handeling, bronreferentie, doelreferentie, status, poging, foutreden en eigenaar kan al het verschil maken. De waarde zit niet in de hoeveelheid velden, maar in het feit dat de tweede poging dezelfde zakelijke handeling herkent voordat hij een nieuw gevolg veroorzaakt.
Een uitzonderingsqueue is een besluitkamer
De derde ontwerpfout is de uitzonderingsqueue behandelen als een opslagplaats voor alles wat de automatisering niet begrijpt. Een queue zonder eigenaar is alleen uitgestelde onzekerheid. Bij een onduidelijke financiële uitkomst moet meteen vaststaan wie het geval beoordeelt, welk bewijs ontbreekt en welke besluiten zijn toegestaan.
Ik onderscheid vier herstelbesluiten. Opnieuw proberen met dezelfde sleutel wanneer de provider aantoonbaar niet heeft uitgevoerd of nog dezelfde aanvraag verwerkt. Reconciliëren wanneer de uitkomst onbekend is en eerst het providerobject, de payout of het ERP-doel moet worden gecontroleerd. Herstellen met een nieuwe zakelijke handeling wanneer de eigenaar bewust een correctie, reversal of tweede deelrefund vrijgeeft. Stoppen wanneer het brongegeven fout is of de financiële beslissing niet mag worden geautomatiseerd.
De eigenaar van die queue is niet automatisch de ontwikkelaar. Bij betalingen en boekingen ligt het besluit meestal bij Finance, met Operations als bronhouder en een technische eigenaar voor de koppeling. Wie dat per proces anders inricht, moet die naam of rol in de ledger bewaren. Een status failed zonder eigenaar zegt alleen dat de software klaar is met kijken.
Ik zou drie meetpunten naast de gewone fouttelling zetten: het aantal dubbele financiële neveneffecten, de waarde van posten in needs_review en het percentage onzekere gebeurtenissen dat binnen één werkdag een herstelbesluit krijgt. De eerste metric hoort nul te zijn. De tweede moet zichtbaar dalen of bewust verklaard zijn. De derde maakt duidelijk of de queue echt wordt bestuurd of alleen wordt gevuld.
Laat herstel terugwijzen naar dezelfde werkelijkheid
Mijn herstelbesluit is daarom expliciet: na een timeout op een financiële actie maak ik geen nieuwe sleutel aan. Eerst controleer ik de eigen ledger, het providerobject en het ERP-doel. Is de actie uitgevoerd, dan leg ik het gevonden doel vast en maak ik de replay een no-op. Is aantoonbaar niets uitgevoerd, dan hergebruik ik dezelfde sleutel. Blijft de uitkomst onzeker, dan gaat het geval naar de uitzonderingsqueue met een benoemde eigenaar en een deadline.
Dat besluit geldt voor payout, refund, chargeback en ERP-boeking, met een andere bron en een andere uitkomst per geval. De regel blijft hetzelfde: herstel mag de financiële werkelijkheid opnieuw vaststellen, maar niet stilzwijgend een tweede werkelijkheid maken.
Een goed financieel systeem is daarom niet het systeem dat nooit opnieuw hoeft te proberen. Het is het systeem dat na het opnieuw proberen nog precies kan vertellen welke werkelijkheid het heeft aangeraakt.
Veelgestelde vragen
Replay veilig laten landen
Ik denk mee over de financiële keten, ontwerp de ledger en uitzonderingsroute en bouw de koppeling van betaalprovider tot ERP. Zo wordt herstel een meetbaar proces dat ik van eerste ontwerp tot livegang realiseer.
Dit artikel is geproduceerd samen met het Agent Team. Meer over de redactie.
