Se connecter

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.open ne soit pas arrivé ne retient pas payment.claimed ni payment.paid.
  • Un appel qui est bien arrivé n’est jamais répété.
  • Nous attendons au maximum dix secondes votre réponse. Sur payment.open et payment.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.open est 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.open et un payment.paid, et pas de payment.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éro pay_ et donc un nouveau payment.open. C’est la description qui 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’id comme 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.expired et payment.failed n’existent pas ici : un virement qui ne vient pas, ne vient tout simplement pas.
  • L’event du milieu s’appelle ici payment.claimed et payment.pending chez Mollie. Chez un PSP, pending est 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.self est donc absent.
  • La signature se trouve dans X-Usecue-Signature au lieu de X-Mollie-Signature, et l’event dans X-Usecue-Event.
  • Nous essayons cinq fois, chaque heure ; Mollie dix fois en 26 heures.
  • Il s’écoule des heures entre claimed et paid, 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.