Webhook
Wenn jemand über Ihre Zahlungsseite bezahlt, kann Ihr Webshop sofort darüber benachrichtigt werden. Dazu tragen Sie einmalig eine Adresse unter Einstellungen > Zahlungsseite > Integration ein (siehe
Zahlungsseite). Ab diesem Moment erhält diese Adresse ein POST mit JSON, sobald mit einer Zahlung etwas geschieht.
Der Aufbau ist bewusst fast identisch mit dem von Mollie. Die Events heißen bis auf eines gleich, die Zahlung im Body sieht gleich aus und die Signatur funktioniert gleich. Arbeitet Ihr Webshop bereits mit einem PSP, erkennen Sie nahezu alles wieder.
Die drei Events
| Event | Wann |
|---|---|
payment.open |
Der Zahler hat Ihre Zahlungsseite vor sich |
payment.claimed |
Der Zahler sagt, dass er bezahlt hat |
payment.paid |
Die Bank bestätigt, dass das Geld eingegangen ist |
payment.open geht raus, sobald die Zahlungsseite wirklich auf dem Bildschirm steht. Das ist etwas anderes als das Öffnen des Links: Die Seite meldet sich erst mit einem Skript an, ein Linkscanner, ein Crawler oder der Prefetch eines Browsers erzeugt also keine Nachricht. Bezahlt ist zu diesem Zeitpunkt noch nichts; dies ist das Startsignal, dass jemand vor Ihrer Zahlungsseite sitzt. Ein Zahler ohne JavaScript meldet sich erst bei ‘Ich habe bezahlt’: Dann folgen payment.open und payment.claimed kurz aufeinander.
payment.claimed geht in dem Moment raus, in dem der Zahler auf ‘Ich habe bezahlt’ drückt oder von einem signierten Zahlungsauftrag bei seiner eigenen Bank zurückkehrt. Der Zahler ist zu diesem Zeitpunkt noch auf der Seite. Das ist noch keine Bestätigung: Es ist der Zahler, der es sagt, nicht die Bank.
payment.paid geht raus, sobald die Überweisung auf Ihrem Konto steht und der Zahlung zugeordnet ist. Das ist die Bestätigung, auf die Sie warten, und der einzige Status, bei dem Sie eine Bestellung freigeben sollten.
Die Liste der Online-Zahlungen in der Kasse benennt den Status einer Zahlung mit denselben drei Wörtern, sodass Sie sie neben das legen können, was Ihr Webshop erhalten hat. Zu einem Besuch, aus dem nichts weiter geworden ist, gehört keine Nachricht und kein Wort: Dort bleibt der Status leer.
Dazu gehört eine Sache, die anders ist als bei einem PSP: Die Kasse erfährt es von Ihrer Bank über Ponto, und Ponto liest Ihr Konto viermal täglich. Rechnen Sie also mit Stunden zwischen payment.claimed und payment.paid, nicht mit Sekunden. Nur Letzteres kommt von der Bank: payment.open und payment.claimed sieht die Zahlungsseite selbst geschehen, die bekommen Sie also auch ohne Bankanbindung. Haben Sie keine, sind das die beiden, die Sie hören, und payment.paid bleibt aus.
Der Aufruf
Ein Aufruf ist ein POST mit Content-Type: application/json und diesen Headern:
| Header | Inhalt |
|---|---|
X-Usecue-Event |
payment.open, payment.claimed oder payment.paid |
X-Usecue-Signature |
sha256= gefolgt vom HMAC-SHA256 des Bodys |
User-Agent |
Usecue-POS-Webhook/1.0 |
Im Body steht die Zahlung selbst, so wie sie zu diesem Zeitpunkt dasteht. Der Status im Body ist das Event ohne das vorangestellte payment., genau wie bei 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/de/docs/zahlungsseite/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/de/docs/zahlungsseite/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/de/docs/zahlungsseite/webhook/", "type": "text/html" }
}
}
Die Felder
| Feld | Bedeutung |
|---|---|
id |
Die Zahlung, pay_ gefolgt von der Nummer in der Kasse |
mode |
live, oder test, solange Sie in der Sandbox von Ponto arbeiten |
amount.value |
Der Betrag als String, mit zwei Dezimalstellen |
description |
Die Beschreibung aus dem Zahlungslink |
method |
Womit der Zahler bezahlt hat, siehe unten |
status |
open, claimed oder paid |
createdAt |
Wann der Zahler die Zahlungsseite geöffnet hat |
expiresAt |
Bis wann eine Überweisung noch zu dieser Zahlung gehört |
paidAt |
Wann die Bank das Geld gebucht hat |
expiresAt steht in einem payment.open und einem payment.claimed, paidAt nur in einem payment.paid, genau wie bei Mollie. Eine Überweisung, die nach expiresAt eingeht, wird dieser Zahlung nicht mehr zugeordnet; es folgt dann also auch kein payment.paid mehr.
Die method ist eine von diesen:
| Method | Bedeutung |
|---|---|
epc |
Der EPC-QR-Code, mit der Bank-App gescannt |
iban |
Manuell überwiesen |
ponto |
Ein Zahlungsauftrag, bei der eigenen Bank signiert |
fallback |
Ein Zahlungslink, zum Beispiel Tikkie |
noneu |
Ein Zahlungslink für außerhalb der EU, Stripe oder PayPal |
In details steht, was über den Zahler bekannt ist. Bei einem payment.open ist das so gut wie nichts: Der Zahler hat selbst noch nichts eingegeben und die Bank hat noch nichts gesagt. consumerName und consumerAccount kommen von der Bank und sind daher erst bei payment.paid gefüllt. consumerAccountDigits sind die Ziffern, die der Zahler bei einem Zahlungslink selbst eingegeben hat, remittanceInformation ist der Verwendungszweck, so wie die Bank ihn weitergegeben hat, und paymentLink der Name des Zahlungslinks, der auf dem Bildschirm stand.
Die Signatur prüfen
Beim Speichern einer Webhook-URL erzeugt die Kasse ein Webhook-Secret. Es steht auf derselben Seite. Jeder Aufruf ist damit signiert, sodass Sie wissen, dass die Nachricht von uns kommt und unterwegs nicht verändert wurde.
Berechnen Sie den HMAC-SHA256 über den Body, so wie Sie ihn erhalten haben, also nicht über das JSON, nachdem Sie es eingelesen und wieder ausgegeben haben:
<?php
$secret = 'das 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 geben Sie die Bestellung frei
}
http_response_code(200);
Vergleichen Sie mit hash_equals und nicht mit ==. Tun Sie mit einer Nachricht, deren Signatur nicht stimmt, sonst nichts.
Antworten und erneut versuchen
Antworten Sie mit einem 2xx-Status. Alles andere, und auch gar keine Antwort, gilt als nicht angekommen.
- Ein Aufruf, der nicht angekommen ist, wird erneut versucht, jede Stunde. Fünf Versuche pro Event, den ersten mitgezählt, nach etwa vier Stunden ist also Schluss.
- Das geschieht pro Event: Dass
payment.opennicht angekommen ist, hältpayment.claimedoderpayment.paidnicht auf. - Ein Aufruf, der angekommen ist, wird nie wiederholt.
- Wir warten höchstens zehn Sekunden auf Ihre Antwort. Bei
payment.openundpayment.claimedsteht der Zahler währenddessen vor Ihrer Zahlungsseite, antworten Sie also sofort und erledigen Sie die eigentliche Arbeit danach.
Was von jedem Versuch zurückkam, können Sie in der Kasse nachlesen, unter der Zahlung selbst bei Online-Zahlungen.
Worauf Sie achten sollten
payment.openist ein Besucher, noch keine Zahlung: Bei den meisten bleibt es dabei. Machen Sie daraus also keine Bestellung. Wofür es gut ist: die zugehörige Bestellung heraussuchen, sehen, dass der Zahler da ist, und später merken, dass er nicht weitergekommen ist.- Verlassen Sie sich nicht auf die Reihenfolge: Ein Zahler, der den Knopf nicht drückt, aber überweist, erzeugt ein
payment.openund einpayment.paidund keinpayment.claimed. Warten Sie also nicht auf eine Nachricht, die vielleicht nie kommt. - Eine Bestellung kann mehr als eine
iderzeugen: Wer die Zahlungsseite auf einem anderen Gerät öffnet oder noch einmal zurückkommt, nachdem er auf ‘Ich habe bezahlt’ gedrückt hat, beginnt eine neue Zahlung mit einer neuenpay_-Nummer und damit ein neuespayment.open. Diedescriptionist das, was all diese Nachrichten mit Ihrer Bestellung verbindet. - Schauen Sie auf
status, nicht auf die Anzahl der Nachrichten: Theoretisch können Sie dasselbe Event zweimal erhalten, zum Beispiel wenn unsere Antwort unterwegs verloren geht, während Sie bereits gebucht hatten. Nutzen Sie dieidals Schlüssel und sorgen Sie dafür, dass eine zweite Nachricht nichts kaputt macht. - Geben Sie erst bei
paidfrei:claimedist der Zahler, der sagt, dass er bezahlt hat. Für eine Bestellung, die Sie versenden, ist das zu wenig; für ein ‘Wir haben Ihre Bestellung erhalten’ reicht es. - Nur über eingehendes Geld: Was Sie selbst überweisen, kommt nicht in die Kasse und damit auch nicht in einen Webhook.
Unterschiede zu Mollie
Die Namen und der Aufbau sind nahezu gleich, aber dies ist kein PSP. Die Unterschiede auf einen Blick:
- Es gibt drei Events.
payment.authorized,payment.canceled,payment.expiredundpayment.failedgibt es hier nicht: Eine Überweisung, die nicht kommt, kommt einfach nicht. - Das mittlere Event heißt hier
payment.claimedund bei Molliepayment.pending. Bei einem PSP istpendingeine Zahlung, die unterwegs ist; hier ist es der Zahler, der sagt, dass er bezahlt hat, und das ist etwas anderes. - Mollie kennt den Status
openauch, ruft Sie dafür aber nicht an. Hier hören Sie ihn: Die Zahlungsseite sieht selbst, dass der Zahler da ist, und das ist der einzige Moment, in dem Sie sicher wissen, dass Ihr Link benutzt wird. - Es gibt keine API, um eine Zahlung später abzurufen. Alles, was Sie brauchen, steht im Body;
_links.selffehlt deshalb. - Die Signatur steht in
X-Usecue-Signaturestatt inX-Mollie-Signature, und das Event inX-Usecue-Event. - Wir versuchen es fünfmal, jede Stunde; Mollie zehnmal in 26 Stunden.
- Zwischen
claimedundpaidliegen Stunden, weil die Bestätigung von Ihrer eigenen Bank kommt und nicht von einem PSP.
Ausprobieren
Stellen Sie die Bankanbindung in den Sandbox-Modus und Sie erhalten dieselben Nachrichten mit "mode": "test". Bezahlen Sie danach einen Testbetrag auf Ihrer eigenen Zahlungsseite: Das payment.open kommt, sobald die Seite auf Ihrem Bildschirm steht, das payment.claimed, sobald Sie auf ‘Ich habe bezahlt’ drücken, und das payment.paid, sobald Ponto die Testbank gelesen hat.
Ohne Bankanbindung können Sie die ersten beiden genauso ausprobieren: Tragen Sie eine Webhook-URL ein, öffnen Sie Ihre eigene Zahlungsseite und ein payment.open steht sofort bereit.
Kommen Sie nicht weiter? Nehmen Sie Kontakt auf.