Webhook
Wanneer iemand betaalt via jouw betaalpagina kan je webshop daar direct bericht van krijgen. Je vult daarvoor één keer een adres in bij Instellingen > Betaalpagina > Integratie (zie
Betaalpagina). Vanaf dat moment krijgt dat adres een POST met JSON zodra er iets met een betaling gebeurt.
De opzet is bewust bijna gelijk aan die van Mollie. De events heten op één na hetzelfde, de betaling in de body ziet er hetzelfde uit en de handtekening werkt hetzelfde. Werkt je webshop al met een PSP, dan herken je vrijwel alles.
De drie events
| Event | Wanneer |
|---|---|
payment.open |
De betaler heeft je betaalpagina voor zich |
payment.claimed |
De betaler zegt dat hij betaald heeft |
payment.paid |
De bank bevestigt dat het geld binnen is |
payment.open gaat de deur uit zodra de betaalpagina echt op het scherm staat. Dat is iets anders dan het openen van de link: de pagina meldt zichzelf pas aan met een script, dus een linkscanner, een crawler of de prefetch van een browser levert geen bericht op. Er is op dat moment nog niets betaald; dit is het startsein dat er iemand voor je betaalpagina zit. Een betaler zonder JavaScript meldt zich pas bij ‘Ik heb betaald’: dan komen payment.open en payment.claimed vlak na elkaar.
payment.claimed gaat de deur uit op het moment dat de betaler op ‘Ik heb betaald’ drukt, of terugkomt van een ondertekend betaalverzoek bij zijn eigen bank. De betaler staat op dat moment nog op de pagina. Dit is nog geen bevestiging: het is de betaler die het zegt, niet de bank.
payment.paid gaat de deur uit zodra de overschrijving op je rekening staat en aan de betaling gekoppeld is. Dit is de bevestiging waar je op wacht en de enige status waarop je een bestelling zou moeten vrijgeven.
De lijst met online betalingen in de kassa noemt de status van een betaling met deze zelfde drie woorden, zodat je hem naast kunt leggen wat je webshop binnenkreeg. Bij een bezoek waar verder niets van terechtkwam hoort geen bericht en geen woord: daar blijft de status leeg.
Daar hoort één ding bij dat anders is dan bij een PSP: de kassa hoort van je bank via Ponto, en Ponto leest je rekening vier keer per dag. Reken dus op uren tussen payment.claimed en payment.paid, niet op seconden. Alleen die laatste komt van de bank: payment.open en payment.claimed ziet de betaalpagina zelf gebeuren en die krijg je dus ook zonder bankkoppeling. Heb je er geen, dan zijn dat de twee die je hoort en blijft payment.paid uit.
De aanroep
Een aanroep is een POST met Content-Type: application/json en deze headers:
| Header | Inhoud |
|---|---|
X-Usecue-Event |
payment.open, payment.claimed of payment.paid |
X-Usecue-Signature |
sha256= gevolgd door de HMAC-SHA256 van de body |
User-Agent |
Usecue-POS-Webhook/1.0 |
In de body staat de betaling zelf, zoals hij er op dat moment bij staat. De status in de body is het event zonder payment. ervoor, precies zoals bij Mollie.
payment.open
{
"resource": "payment",
"id": "pay_123",
"mode": "live",
"amount": { "currency": "EUR", "value": "10.00" },
"description": "Order 12345",
"method": "epc",
"status": "open",
"createdAt": "2026-09-11T09:13:37+02:00",
"expiresAt": "2026-09-25T09:13:37+02:00",
"details": {
"transferReference": "Order 12345",
"consumerName": null,
"consumerAccount": null,
"consumerAccountDigits": null,
"remittanceInformation": null,
"paymentLink": null
},
"_links": {
"dashboard": { "href": "https://pos.usecue.com/pos/payments/details/123", "type": "text/html" },
"documentation": { "href": "https://pos.usecue.com/p/nl/docs/betaalpagina/webhook/", "type": "text/html" }
}
}
payment.claimed
{
"resource": "payment",
"id": "pay_123",
"mode": "live",
"amount": { "currency": "EUR", "value": "10.00" },
"description": "Order 12345",
"method": "epc",
"status": "claimed",
"createdAt": "2026-09-11T09:13:37+02:00",
"expiresAt": "2026-09-25T09:13:37+02:00",
"details": {
"transferReference": "Order 12345",
"consumerName": null,
"consumerAccount": null,
"consumerAccountDigits": "4300",
"remittanceInformation": null,
"paymentLink": null
},
"_links": {
"dashboard": { "href": "https://pos.usecue.com/pos/payments/details/123", "type": "text/html" },
"documentation": { "href": "https://pos.usecue.com/p/nl/docs/betaalpagina/webhook/", "type": "text/html" }
}
}
payment.paid
{
"resource": "payment",
"id": "pay_123",
"mode": "live",
"amount": { "currency": "EUR", "value": "10.00" },
"description": "Order 12345",
"method": "epc",
"status": "paid",
"createdAt": "2026-09-11T09:13:37+02:00",
"paidAt": "2026-09-11T14:02:00+02:00",
"details": {
"transferReference": "Order 12345",
"consumerName": "J. Jansen",
"consumerAccount": "NL91ABNA0417164300",
"consumerAccountDigits": "4300",
"remittanceInformation": "Order 12345",
"paymentLink": null
},
"_links": {
"dashboard": { "href": "https://pos.usecue.com/pos/payments/details/123", "type": "text/html" },
"documentation": { "href": "https://pos.usecue.com/p/nl/docs/betaalpagina/webhook/", "type": "text/html" }
}
}
De velden
| Veld | Betekenis |
|---|---|
id |
De betaling, pay_ gevolgd door het nummer in de kassa |
mode |
live, of test zolang je in de sandbox van Ponto werkt |
amount.value |
Het bedrag als string, met twee decimalen |
description |
De omschrijving uit de betaallink |
method |
Waarmee de betaler betaalde, zie hieronder |
status |
open, claimed of paid |
createdAt |
Wanneer de betaler de betaalpagina opende |
expiresAt |
Tot wanneer een overschrijving nog bij deze betaling hoort |
paidAt |
Wanneer de bank het geld boekte |
expiresAt staat in een payment.open en een payment.claimed, paidAt alleen in een payment.paid, net als bij Mollie. Een overschrijving die na expiresAt binnenkomt wordt niet meer aan deze betaling gekoppeld; er komt dan dus ook geen payment.paid meer.
De method is er één van deze:
| Method | Betekenis |
|---|---|
epc |
De EPC QR-code, gescand met de bankapp |
iban |
Handmatig overgeschreven |
ponto |
Een betaalverzoek, ondertekend bij de eigen bank |
fallback |
Een betaallink, bijvoorbeeld Tikkie |
noneu |
Een betaallink voor buiten de EU, Stripe of PayPal |
In details staat wat er over de betaler bekend is. Bij een payment.open is dat vrijwel niets: de betaler heeft zelf nog niets ingevuld en de bank heeft nog niets gezegd. consumerName en consumerAccount komen van de bank en zijn dus pas gevuld bij payment.paid. consumerAccountDigits zijn de cijfers die de betaler zelf invulde bij een betaallink, remittanceInformation is de omschrijving zoals de bank hem doorgaf en paymentLink de naam van de betaallink die op het scherm stond.
De handtekening controleren
Bij het opslaan van een webhook URL maakt de kassa een webhook secret. Dat staat op dezelfde pagina. Elke aanroep is daarmee ondertekend, zodat je weet dat het bericht van ons komt en onderweg niet is aangepast.
Reken de HMAC-SHA256 uit over de body zoals je hem binnenkreeg, dus niet over de JSON nadat je hem hebt ingelezen en weer weggeschreven:
<?php
$secret = 'het webhook secret';
$body = file_get_contents('php://input');
$signature = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($signature, $_SERVER['HTTP_X_USECUE_SIGNATURE'] ?? '')) {
http_response_code(403);
exit;
}
$payment = json_decode($body, true);
if ($payment['status'] == 'paid') {
// hier geef je de bestelling vrij
}
http_response_code(200);
Vergelijk met hash_equals en niet met ==. Doe verder niets met een bericht waarvan de handtekening niet klopt.
Antwoorden en opnieuw proberen
Antwoord met een 2xx status. Alles daarbuiten, en ook helemaal geen antwoord, geldt als niet aangekomen.
- Een aanroep die niet aankwam wordt opnieuw geprobeerd, elk uur. Vijf pogingen per event, de eerste meegeteld, dus na een uur of vier houdt het op.
- Dat gaat per event: dat
payment.openniet aankwam houdtpayment.claimedofpayment.paidniet tegen. - Een aanroep die wel aankwam wordt nooit herhaald.
- We wachten maximaal tien seconden op je antwoord. Bij
payment.openenpayment.claimedstaat de betaler ondertussen voor je betaalpagina, dus antwoord meteen en doe het echte werk daarna.
Wat er van elke poging terugkwam kun je nalezen in de kassa, onder de betaling zelf bij Online betalingen.
Waar je op moet letten
payment.openis een bezoeker, nog geen betaling: de meeste blijven daarbij steken. Maak er dus geen bestelling van. Waar het wel voor is: de bijbehorende bestelling opzoeken, zien dat de betaler er is, en later merken dat hij niet verder kwam.- Ga niet uit van de volgorde: een betaler die de knop niet indrukt maar wel overmaakt, levert een
payment.openen eenpayment.paidop en geenpayment.claimed. Ga dus niet zitten wachten op een bericht dat misschien nooit komt. - Eén bestelling kan meer dan één
idopleveren: wie de betaalpagina op een ander apparaat opent, of nog eens terugkomt nadat hij op ‘Ik heb betaald’ heeft gedrukt, begint een nieuwe betaling met een nieuwpay_-nummer en dus een nieuwepayment.open. Dedescriptionis wat al die berichten aan jouw bestelling koppelt. - Kijk naar
status, niet naar het aantal berichten: je kunt hetzelfde event in theorie twee keer krijgen, bijvoorbeeld als ons antwoord onderweg verdwijnt terwijl jij al geboekt had. Gebruik deidals sleutel en zorg dat een tweede bericht niets stuk maakt. - Geef pas vrij bij
paid:claimedis de betaler die zegt dat hij betaald heeft. Voor een bestelling die je verstuurt is dat te weinig; voor een ‘we hebben je bestelling ontvangen’ is het genoeg. - Alleen over binnenkomend geld: wat jij zelf overmaakt komt niet in de kassa en dus ook niet in een webhook.
Verschillen met Mollie
De namen en de opbouw zijn vrijwel gelijk, maar dit is geen PSP. De verschillen op een rij:
- Er zijn drie events.
payment.authorized,payment.canceled,payment.expiredenpayment.failedbestaan hier niet: een overschrijving die niet komt, komt gewoon niet. - Het middelste event heet hier
payment.claimeden bij Molliepayment.pending. Bij een PSP ispendingeen betaling die onderweg is; hier is het de betaler die zegt dat hij betaald heeft, en dat is iets anders. - Mollie kent de status
openook, maar belt je er niet voor. Hier hoor je hem wel: de betaalpagina ziet zelf dat de betaler er is, en dat is het enige moment waarop je zeker weet dat je link gebruikt wordt. - Er is geen API om een betaling later op te vragen. Alles wat je nodig hebt staat in de body;
_links.selfontbreekt daarom. - De handtekening zit in
X-Usecue-Signaturein plaats vanX-Mollie-Signature, en het event inX-Usecue-Event. - Wij proberen vijf keer, elk uur; Mollie tien keer in 26 uur.
- Tussen
claimedenpaidzitten uren, omdat de bevestiging van je eigen bank komt en niet van een PSP.
Uitproberen
Zet de bankkoppeling in sandboxmodus en je krijgt dezelfde berichten met "mode": "test". Betaal daarna een testbedrag op je eigen betaalpagina: de payment.open komt zodra de pagina op je scherm staat, de payment.claimed zodra je op ‘Ik heb betaald’ drukt en de payment.paid zodra Ponto de nepbank heeft gelezen.
Zonder bankkoppeling kun je de eerste twee net zo goed uitproberen: vul een webhook URL in, open je eigen betaalpagina en er staat meteen een payment.open klaar.
Loop je vast? Neem contact op.