Inloggen

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.open niet aankwam houdt payment.claimed of payment.paid niet tegen.
  • Een aanroep die wel aankwam wordt nooit herhaald.
  • We wachten maximaal tien seconden op je antwoord. Bij payment.open en payment.claimed staat 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.open is 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.open en een payment.paid op en geen payment.claimed. Ga dus niet zitten wachten op een bericht dat misschien nooit komt.
  • Eén bestelling kan meer dan één id opleveren: 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 nieuw pay_-nummer en dus een nieuwe payment.open. De description is 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 de id als sleutel en zorg dat een tweede bericht niets stuk maakt.
  • Geef pas vrij bij paid: claimed is 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.expired en payment.failed bestaan hier niet: een overschrijving die niet komt, komt gewoon niet.
  • Het middelste event heet hier payment.claimed en bij Mollie payment.pending. Bij een PSP is pending een betaling die onderweg is; hier is het de betaler die zegt dat hij betaald heeft, en dat is iets anders.
  • Mollie kent de status open ook, 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.self ontbreekt daarom.
  • De handtekening zit in X-Usecue-Signature in plaats van X-Mollie-Signature, en het event in X-Usecue-Event.
  • Wij proberen vijf keer, elk uur; Mollie tien keer in 26 uur.
  • Tussen claimed en paid zitten 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.