Webhook
Lorsque quelqu’un paie via votre page de paiement, votre boutique en ligne peut en être informée immédiatement. Pour cela, vous saisissez une adresse une seule fois dans Paramètres > Page de paiement > Intégration (voir
Page de paiement). À partir de ce moment, cette adresse reçoit un POST en JSON dès qu’il se passe quelque chose avec un paiement.
La structure est volontairement presque identique à celle de Mollie. Les events portent les mêmes noms à un près, le paiement dans le corps a la même apparence et la signature fonctionne de la même manière. Si votre boutique travaille déjà avec un PSP, vous reconnaîtrez presque tout.
Les trois events
| Event | Quand |
|---|---|
payment.open |
Le payeur a votre page de paiement devant lui |
payment.claimed |
Le payeur dit qu’il a payé |
payment.paid |
La banque confirme que l’argent est arrivé |
payment.open part dès que la page de paiement est réellement à l’écran. Ce n’est pas la même chose que l’ouverture du lien : la page ne s’annonce qu’avec un script, un scanner de liens, un crawler ou le prefetch d’un navigateur ne produit donc aucun message. À cet instant, rien n’est encore payé ; c’est le signal de départ indiquant que quelqu’un se trouve devant votre page de paiement. Un payeur sans JavaScript ne s’annonce qu’au moment du « J’ai payé » : payment.open et payment.claimed se suivent alors de près.
payment.claimed part au moment où le payeur appuie sur « J’ai payé », ou revient d’une demande de paiement signée auprès de sa propre banque. À cet instant, le payeur est encore sur la page. Ce n’est pas encore une confirmation : c’est le payeur qui le dit, pas la banque.
payment.paid part dès que le virement figure sur votre compte et a été rattaché au paiement. C’est la confirmation que vous attendez, et le seul statut sur lequel vous devriez libérer une commande.
La liste des paiements en ligne dans la caisse nomme le statut d’un paiement avec ces mêmes trois mots, pour que vous puissiez la mettre en regard de ce que votre boutique a reçu. Une visite dont rien d’autre n’est sorti n’a ni message ni mot : son statut y reste vide.
Il y a là une chose qui diffère d’un PSP : la caisse l’apprend de votre banque via Ponto, et Ponto lit votre compte quatre fois par jour. Comptez donc des heures entre payment.claimed et payment.paid, pas des secondes. Seul ce dernier vient de la banque : payment.open et payment.claimed, la page de paiement les voit se produire elle-même, vous les recevez donc aussi sans connexion bancaire. Si vous n’en avez pas, ce sont ces deux-là que vous recevez et payment.paid n’arrive pas.
L’appel
Un appel est un POST avec Content-Type: application/json et ces en-têtes :
| En-tête | Contenu |
|---|---|
X-Usecue-Event |
payment.open, payment.claimed ou payment.paid |
X-Usecue-Signature |
sha256= suivi du HMAC-SHA256 du corps |
User-Agent |
Usecue-POS-Webhook/1.0 |
Le corps contient le paiement lui-même, tel qu’il se présente à ce moment-là. Le statut dans le corps est l’event sans le payment. devant, exactement comme chez 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/fr/docs/page-de-paiement/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/fr/docs/page-de-paiement/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/fr/docs/page-de-paiement/webhook/", "type": "text/html" }
}
}
Les champs
| Champ | Signification |
|---|---|
id |
Le paiement, pay_ suivi du numéro dans la caisse |
mode |
live, ou test tant que vous travaillez dans le bac à sable de Ponto |
amount.value |
Le montant sous forme de chaîne, avec deux décimales |
description |
La description issue du lien de paiement |
method |
Avec quoi le payeur a payé, voir ci-dessous |
status |
open, claimed ou paid |
createdAt |
Quand le payeur a ouvert la page de paiement |
expiresAt |
Jusqu’à quand un virement appartient encore à ce paiement |
paidAt |
Quand la banque a comptabilisé l’argent |
expiresAt figure dans un payment.open et dans un payment.claimed, paidAt uniquement dans un payment.paid, comme chez Mollie. Un virement qui arrive après expiresAt n’est plus rattaché à ce paiement ; aucun payment.paid ne suit donc non plus.
La method est l’une de celles-ci :
| Method | Signification |
|---|---|
epc |
Le QR-code EPC, scanné avec l’application bancaire |
iban |
Viré manuellement |
ponto |
Une demande de paiement, signée auprès de sa propre banque |
fallback |
Un lien de paiement, par exemple Tikkie |
noneu |
Un lien de paiement pour hors UE, Stripe ou PayPal |
Dans details figure ce que l’on sait du payeur. Lors d’un payment.open, ce n’est pour ainsi dire rien : le payeur n’a encore rien saisi et la banque n’a encore rien dit. consumerName et consumerAccount viennent de la banque et ne sont donc remplis qu’avec payment.paid. consumerAccountDigits sont les chiffres que le payeur a saisis lui-même lors d’un lien de paiement, remittanceInformation est la communication telle que la banque l’a transmise, et paymentLink le nom du lien de paiement affiché à l’écran.
Vérifier la signature
Lors de l’enregistrement d’une URL de webhook, la caisse crée un secret de webhook. Il figure sur la même page. Chaque appel en est signé, afin que vous sachiez que le message vient de nous et n’a pas été modifié en chemin.
Calculez le HMAC-SHA256 sur le corps tel que vous l’avez reçu, donc pas sur le JSON après l’avoir lu puis réécrit :
<?php
$secret = 'le secret du webhook';
$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') {
// c'est ici que vous libérez la commande
}
http_response_code(200);
Comparez avec hash_equals et non avec ==. Ne faites rien d’autre d’un message dont la signature ne correspond pas.
Répondre et réessayer
Répondez avec un statut 2xx. Tout le reste, y compris l’absence de réponse, est considéré comme non reçu.
- Un appel qui n’est pas arrivé est réessayé, chaque heure. Cinq tentatives par event, la première comprise, donc au bout de quatre heures environ cela s’arrête.
- Cela se fait par event : le fait qu’un
payment.openne soit pas arrivé ne retient paspayment.claimednipayment.paid. - Un appel qui est bien arrivé n’est jamais répété.
- Nous attendons au maximum dix secondes votre réponse. Sur
payment.openetpayment.claimed, le payeur se trouve entre-temps devant votre page de paiement, répondez donc immédiatement et faites le vrai travail ensuite.
Ce que chaque tentative a renvoyé est consultable dans la caisse, sous le paiement lui-même, à Paiements en ligne.
Ce à quoi il faut faire attention
payment.openest un visiteur, pas encore un paiement : la plupart en restent là. N’en faites donc pas une commande. Ce à quoi il sert : retrouver la commande correspondante, voir que le payeur est là, et constater plus tard qu’il n’est pas allé plus loin.- Ne présumez pas de l’ordre : un payeur qui n’appuie pas sur le bouton mais qui vire l’argent produit un
payment.openet unpayment.paid, et pas depayment.claimed. N’attendez donc pas un message qui ne viendra peut-être jamais. - Une commande peut produire plus d’un
id: celui qui ouvre la page de paiement sur un autre appareil, ou qui revient après avoir appuyé sur « J’ai payé », commence un nouveau paiement avec un nouveau numéropay_et donc un nouveaupayment.open. C’est ladescriptionqui relie tous ces messages à votre commande. - Regardez
status, pas le nombre de messages : en théorie, vous pouvez recevoir deux fois le même event, par exemple si notre réponse se perd en chemin alors que vous aviez déjà comptabilisé. Utilisez l’idcomme clé et faites en sorte qu’un second message ne casse rien. - Ne libérez qu’avec
paid:claimed, c’est le payeur qui dit avoir payé. Pour une commande que vous expédiez, c’est insuffisant ; pour un « nous avons bien reçu votre commande », cela suffit. - Uniquement l’argent entrant : ce que vous virez vous-même n’arrive pas dans la caisse et donc pas non plus dans un webhook.
Différences avec Mollie
Les noms et la structure sont presque identiques, mais il ne s’agit pas d’un PSP. Les différences en bref :
- Il y a trois events.
payment.authorized,payment.canceled,payment.expiredetpayment.failedn’existent pas ici : un virement qui ne vient pas, ne vient tout simplement pas. - L’event du milieu s’appelle ici
payment.claimedetpayment.pendingchez Mollie. Chez un PSP,pendingest un paiement en cours d’acheminement ; ici, c’est le payeur qui dit avoir payé, ce qui est autre chose. - Mollie connaît lui aussi le statut
open, mais ne vous appelle pas pour autant. Ici, vous l’entendez : la page de paiement voit elle-même que le payeur est là, et c’est le seul moment où vous savez avec certitude que votre lien est utilisé. - Il n’y a pas d’API pour consulter un paiement plus tard. Tout ce dont vous avez besoin est dans le corps ;
_links.selfest donc absent. - La signature se trouve dans
X-Usecue-Signatureau lieu deX-Mollie-Signature, et l’event dansX-Usecue-Event. - Nous essayons cinq fois, chaque heure ; Mollie dix fois en 26 heures.
- Il s’écoule des heures entre
claimedetpaid, parce que la confirmation vient de votre propre banque et non d’un PSP.
Essayer
Mettez la connexion bancaire en mode bac à sable et vous recevrez les mêmes messages avec "mode": "test". Payez ensuite un montant de test sur votre propre page de paiement : le payment.open arrive dès que la page est à votre écran, le payment.claimed dès que vous appuyez sur « J’ai payé », et le payment.paid dès que Ponto a lu la banque fictive.
Sans connexion bancaire, vous pouvez tout aussi bien essayer les deux premiers : saisissez une URL de webhook, ouvrez votre propre page de paiement et un payment.open vous attend aussitôt.
Vous êtes bloqué ? Prenez contact.