Webhooks
CajouPay envoie un POST JSON à l'adresse de votre serveur à chaque changement de statut d'un
paiement ou d'un retrait. Ajoutez cette adresse dans Développeurs (une par environnement) et
copiez son secret whsec_…. Supprimer arrête aussitôt les envois vers une adresse, relances
en attente comprises ; les envois passés restent dans le journal.
Événements
| Type | Quand |
|---|---|
payment.request_sent | la demande est partie vers la banque du client |
payment.accepted_deferred | le client a accepté et paiera à la livraison |
payment.succeeded | le paiement est reçu |
payment.declined | le client a refusé |
payment.expired | délai dépassé |
payment.canceled | vous avez annulé le paiement |
payment.failed | erreur technique côté banque |
payment.duplicate_refunded | un paiement en double a été remboursé au client |
payout.paid, payout.failed | un retrait est versé, ou a échoué |
{
"id": "evt_3Fz9Qm2LkP",
"object": "event",
"type": "payment.succeeded",
"created_at": "2026-10-11T17:42:08Z",
"livemode": false,
"data": { "object": { "id": "pay_8Kq2xVt41N", "status": "succeeded", "amount": 32500, "…": "…" } }
}
data.object est le paiement (ou le retrait) complet, tel que l'API le renvoie.
Vérifier la signature
Chaque envoi porte l'en-tête CajouPay-Signature: t=<horodatage>,v1=<signature>, où la signature
est le HMAC-SHA256, en hexadécimal, de <horodatage>.<corps brut> avec votre secret. Refusez
(400) une signature fausse ou un horodatage à plus de 5 minutes de l'heure courante. Calculez la
signature sur le corps brut, avant tout décodage JSON.
- PHP
- Node.js
$payload = file_get_contents('php://input');
try {
$event = \CajouPay\Webhook::constructEvent(
$payload,
$_SERVER['HTTP_CAJOUPAY_SIGNATURE'] ?? null,
getenv('CAJOUPAY_WEBHOOK_SECRET'),
);
} catch (\CajouPay\Exception\SignatureVerificationException $e) {
http_response_code(400);
exit;
}
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody : le corps tel que reçu (par exemple express.raw({ type: 'application/json' })).
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.groupBy(
(header ?? '').split(',').map((part) => part.split('=')),
([key]) => key,
);
const timestamp = Number(parts.t?.[0]?.[1]);
if (!Number.isInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
return (parts.v1 ?? []).some(([, signature]) => {
const received = Buffer.from(signature ?? '', 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
});
}
Répondre
Répondez un code 2xx en moins de 10 secondes, puis traitez l'événement. Sans réponse 2xx,
CajouPay renvoie l'événement après 1 min, 5 min, 30 min, 2 h puis 12 h (six envois en tout). Les
redirections ne sont pas suivies.
Traiter sans surprise
- Une seule fois : le même événement peut arriver plusieurs fois. Retenez les
iddéjà traités, ou rendez l'action sans effet la seconde fois (une commande déjà payée le reste). - Dans le désordre : l'ordre d'arrivée n'est pas garanti. Fiez-vous au
statusdedata.object, et ne revenez jamais en arrière depuis un statut final. - Vérifiez le montant avant de livrer :
data.object.amountdoit être celui de votre commande. - En cas de doute, relisez le paiement :
GET /v1/payments/{id}.
En production, l'adresse doit être en https et joignable depuis Internet.