Integrations
Mollie, Adyen, Buckaroo webhooks: veertien valkuilen
Een Rotterdams reisbureau wilde z'n maandagochtenden terug. We bouwden een reconciliatie-agent voor drie Nederlandse PSP's en vonden veertien webhook-valkuilen die stil onder een 200 OK liegen.

De boekhoudster van een Rotterdams reisbureau met 22 mensen had een Google Sheet van 1.180 rijen, en de kolom die ze het meest haatte was ‘PSP-referentie’. Elke refund, gedeeltelijke refund, chargeback en hernieuwde poging kwam als een nieuwe rij binnen, en ze plakte ze met de hand aan elkaar. De zomerdrukte was net voorbij. Ze wilde haar maandagochtenden terug.
Wij werden ingehuurd om de reconciliatie-agent te bouwen die dat sheet moest vervangen. Die leest de webhooks van de drie PSP's die het reisbureau gebruikt — Mollie, Adyen, Buckaroo — koppelt elk event aan een boeking en een passagier, en schrijft het resultaat weg in Exact Online. Loodgieterswerk. Het soort werk waarvan we dachten dat het in drie weken klaar zou zijn.
Na zes weken hadden we veertien openstaande tickets, allemaal van dezelfde vorm: ‘Refund binnen, agent kijkt ernaar, agent kiest de verkeerde order, agent meldt het aan Exact.’ Geen van de providers loog. Allemaal antwoordden ze met 200 OK en stuurden ze geldige, ondertekende payloads. Ze waren alleen in veertien verschillende vormen geknutseld die niet bij elkaar pasten, en in sommige gevallen niet eens bij zichzelf.
Dit is het spiekbriefje, gerangschikt van ‘gaat stilletjes geld kosten’ bovenaan tot ‘irritant’ onderaan. Als je een Nederlandse PSP integreert en je geeft om reconciliatie, bewaar 'm.
Onze rangschikking
Twee faalmodussen zijn erger dan de rest. De eerste is order_id-substitutie: de webhook verwijst naar de verkeerde boeking, de agent boekt op de verkeerde klant, en je komt er drie maanden later achter via een telefoontje. De tweede is stil veldverlies bij retry: de webhook vuurt twee keer af met dezelfde status, de tweede mist het IBAN van de consument, en de bankreconciliatie loopt vast omdat niemand de centen meer terug kan koppelen aan de klant. Beide falen onder een 200 OK. Beide zien er goed uit in je logs. Die wegen we het zwaarst.
De veertien, ergste eerst
1. Adyen CHARGEBACK gebruikt originalReference, niet merchantReference
De CHARGEBACK-notificatie komt binnen met een pspReference voor de chargeback zelf en een originalReference die wijst naar de oorspronkelijke autorisatie. Als je je boeking sleutelt op merchantReference, moet je de originele auth ophalen om die te vinden — de chargeback-notificatie draagt de boeking-ID niet op de voor de hand liggende plek. We hadden een geval waarbij twee boekingen dezelfde merchantReference-prefix deelden (een UI-bug van een vorige developer), en de agent kende een chargeback van €1.840 toe aan een klant die voor een citytrip van €290 had betaald. Zie de Adyen chargeback notification reference.
2. Mollie refund-webhook laat het consumer-IBAN vallen bij retry
Een refund-webhook vuurt af op het moment dat een refund aangemaakt wordt. Als de refund in pending blijft staan (en bij SEPA-routed iDEAL-refunds is dat altijd zo), vuurt Mollie dezelfde webhook nog een keer af zodra de status omdraait naar refunded. De payload van de eerste schiet bevat het IBAN van de consument onder details.consumerAccount. De tweede vaak niet — de docs beloven het niet, en in de praktijk is het veld bij retries ongeveer een derde van de tijd null. Als je agent de tweede schiet als bron van waarheid behandelt, ben je net je bankreferentie kwijt.
3. Buckaroo partial refunds vervangen de transactiesleutel
De push van Buckaroo voor een gedeeltelijke refund komt binnen met BRQ_TRANSACTIONS gevuld met de eigen transactie-GUID van die partial refund, en de originele betaal-GUID staat verstopt onder BRQ_RELATEDTRANSACTION_PARTIALPAYMENT. Naïeve handlers (en er staan er meerdere open source in PHP die dit zo doen) sleutelen op BRQ_TRANSACTIONS. De agent opent dan een nieuwe grootboekregel in plaats van de originele boeking te crediteren.
4. Mollie chargeback-webhook sleutelt op chargeback-ID
De chargeback-webhook van Mollie post de chargeback-ID naar je URL, niet de payment-ID. Je moet eerst de chargeback ophalen, dan de bijbehorende payment, dan de metadata van die payment. Drie round-trips voordat je weet welke boeking geraakt is. Als één van die round-trips een 429 geeft — en tijdens een massa-chargeback-event op het kaartnetwerk in maart 2026 zagen we precies dat — komt de agent stil te staan en veroudert de chargeback in je dead-letter queue.
5. Adyen notification-batching kan succes-states door elkaar gooien
Adyen batcht notificaties in één POST. Binnen die batch kun je een AUTHORISATION met success=false ontvangen, gevolgd door een AUTHORISATION met success=true voor dezelfde merchantReference, in dezelfde envelope. De volgorde is bij retries niet gegarandeerd. Als je ze op de retry in array-volgorde verwerkt, draai je de boeking terug naar ‘failed’ nadat 'ie al ‘paid’ was.
6. Buckaroo BRQ_STATUSCODE 491 verbergt chargeback-intentie
Statuscode 491 betekent ‘pending input’. Voor chargebacks blijft Buckaroo daar tot 48 uur op zitten terwijl het op de acquirer wacht. In dat raam komt er geen vervolg-push. Wij hadden agents die boekingen op ‘betwist, nog niet doorberekend’ zetten — nuttig, maar alleen omdat we het zelf zo hadden gebouwd. De default is stilte.
7. Mollie consumer-details worden pas gevuld na settlement
Voor iDEAL specifiek worden details.consumerName, details.consumerAccount en details.consumerBic pas gevuld nadat de payment gesettled is, en dat kan uren na status=paid zijn. Schrijf je naar je grootboek op paid, dan schrijf je drie null-velden weg. Wacht je tot details gevuld is, dan wacht je een ongedefinieerd lange tijd. Er is hiervoor geen settlement-webhook — je polt.
8. Adyen AUTHORISATION_ADJUSTMENT houdt de pspReference
Als een hotelpartner het bedrag op een vastgehouden kaart aanpast (Adyens AUTHORISATION_ADJUSTMENT), verandert de pspReference niet. Het bedrag in additionalData.acquirerAmount wel. Als je agent dedupliceert op pspReference, ziet de tweede notificatie eruit als een duplicaat van de eerste en wordt 'ie weggegooid. We betrapten dit op een hotelaanpassing van €640 naar €890 die negen dagen lang uit het grootboek verdwenen was.
9. Buckaroo-signature breekt zodra ze een nieuwe push-parameter toevoegen
De push-signature van Buckaroo is een SHA-1 (of SHA-512, afhankelijk van je shop-config) van de gesorteerde, aaneengeplakte push-parameters plus je secret. Voegen zij aan hun kant een nieuw veld toe — ze voegden BRQ_PAYMENT_METHOD_SCHEME eind 2025 toe — dan weigert je oude signature-verificatiecode geldige pushes, omdat jouw sortering niet meer overeenkomt met die van hen. Er is geen versie-header.
10. Mollie webhook-URL is per payment, vastgevroren bij aanmaak
De webhook-URL zet je op het moment dat je de payment aanmaakt. Zes maanden later, nadat je je reconciliatie-service naar een nieuw domein hebt verhuisd, vuurt een vertraagde chargeback af tegen een payment die in de oude wereld is gemaakt. Die gaat naar de oude URL. Geeft de oude URL een 404, dan retryt Mollie 24 uur lang en geeft het dan op. Wij houden nu op elke oude URL een 410 Gone-handler die doorstuurt naar het nieuwe endpoint en de overdracht logt.
11. Adyen-retries veranderen de eventDate niet
Retryt Adyen een notificatie, dan blijft de eventDate gelijk aan de eerste poging. Is je dedupe-logica (merchantReference, eventCode, eventDate), prima. Is het (merchantReference, eventCode, receivedAt) — en een paar populaire libraries kiezen die laatste als default — dan verwerk je dezelfde notificatie twee keer.
12. Buckaroo refund-pushes dragen geen consumer-IBAN
De refund-push van Buckaroo verwijst naar de consumententransactie maar bevat zelf geen IBAN. Je moet het BRQ_SERVICE_IDEAL_CONSUMERIBAN-veld van de oorspronkelijke transactie opzoeken, en dat heb je alleen opgeslagen als je de originele push goed hebt afgehandeld. De helft van de oude PHP-integraties die we hebben gered, sloeg het nooit op.
13. Mollie test-webhooks komen uit een andere IP-range
Zet je de webhook-bron van Mollie op je IP-allowlist (we hebben één klant die daar op blijft hameren), dan komen testmode-pushes uit Mollie's webhook-sandbox uit een andere range dan live-pushes. Je staging slaagt, productie blokkeert dezelfde webhook. Er gaat nergens hardop iets stuk.
14. Adyen wil ‘[accepted]’ als response body
Een 200 OK met een lege body is niet genoeg. De standaard Adyen-webhook verwacht letterlijk de body [accepted]. Antwoord je met {"ok":true} en een 200, dan behandelt Adyen dat als een failure en retryt. De retries slagen uiteindelijk, je grootboek krijgt duplicaten, en je dedupe-logica uit valkuil #11 redt je alleen als je 'm goed had ingericht.
Het skelet waar we op uitkwamen
Na veertien herschrijvingen bleef er van de webhook-handler van de reconciliatie-agent één normalisatiestap over die afvuurt vóór alle business-logica:
def normalise(provider, payload):
# Always resolve to (booking_id, event_type, amount_cents, party_iban)
booking = resolve_booking(provider, payload) # may fetch
event = canonical_event(provider, payload) # 6 types, not 40
amount = money_in_cents(provider, payload)
iban = consumer_iban(provider, payload) or \
lookup_original_iban(booking, event) # always fall back
return Event(booking, event, amount, iban)
De fallback op iban is de regel die zichzelf heeft terugverdiend. Elke webhook krijgt een kans 'm uit de push te vullen; zwijgt de push (Buckaroo refund, Mollie retry), dan lopen we terug naar de oorspronkelijke transactie die we hebben opgeslagen. Dat is de regel die voorkomt dat de bankreconciliatie stukloopt.
Dedupliceert je reconciliatie-agent op ‘received timestamp’ of ‘PSP-referentie alleen’, dan verlies je stilletjes chargebacks, partial refunds en aangepaste autorisaties. Dedupliceer op (provider, event_type, primary_reference, event_date_from_payload) — nooit op kloktijd.
Het kleinste wat je vandaag kunt doen
Open je webhook-handler. Zoek de regel waar je beslist bij welke boeking een binnenkomend event hoort. Leest die regel één veld uit — merchantReference, BRQ_TRANSACTIONS, een URL-parameter, wat dan ook — dan heb je een kandidaat voor valkuil #1, #3 of #4. Print de laatste vijftig binnengekomen payloads waar je alleen naar dat veld hebt gekeken, en controleer met de hand of er een paar bij een andere boeking horen dan de agent dacht. Een half uur. Je vindt er minstens één.
Toen we deze betalingsreconciliatie-agent voor het Rotterdamse reisbureau bouwden, was de verrassing niet dat webhooks liegen. Het was hoeveel ervan stilletjes liegen. We hebben de handler herbouwd rond het normaliseer-dan-resolve-patroon hierboven, en dat is dezelfde vorm die we nu gebruiken voor elke betaal-zijdige AI-agent die we uitrollen.
Kern
Webhook-reconciliatie breekt onder een 200 OK. Normaliseer elk PSP-event naar (boeking, type, bedrag, IBAN) en dedupliceer op de event-datum uit de payload, nooit op kloktijd.
FAQ
Heb ik een aparte webhook-handler per PSP nodig?
Praktisch wel. De wire-formats en retry-semantiek verschillen genoeg dat één handler met branches zelf de bug wordt. Splits vroeg aan de rand, en deel daarna één normalisatiestap voordat business-logica draait.
Kan ik voor deduplicatie vertrouwen op de signature van de PSP alleen?
Nee. Adyen en Buckaroo retryen geldige signatures; door Mollie's URL-per-payment kunnen duplicaten maanden uit elkaar binnenkomen. Dedupliceer op (provider, event_type, primary_reference, event_date_from_payload).
Welke valkuil slaat het hardst toe als je er niks aan doet?
Adyens CHARGEBACK die originalReference gebruikt in plaats van merchantReference. Die kan een chargeback aan de verkeerde klant toekennen, stil, onder een 200 OK, en je komt er pas maanden later via een telefoontje achter.
Waarom niet gewoon een externe reconciliatie-tool gebruiken?
De meeste kant-en-klare tools gaan uit van één PSP, één valuta, één boekingsmodel. Op het moment dat je Mollie iDEAL met Adyen-kaart en Buckaroo-SEPA mixt op dezelfde boeking, schrijf je de merge-laag alsnog zelf.