Webhooks
Recevoir les événements Mobupay en temps réel.
Format d'un événement
Mobupay envoie une requête POST à votre endpoint avec un body JSON :
{
"id": "evt_2NjK8mLk0093xY",
"type": "payment.succeeded",
"createdAt": "2026-05-14T12:34:56Z",
"data": {
"paymentId": "pay_xxxxxxxx",
"amount": 1000,
"currency": "XPF",
"status": "transit",
"reference": "SPR-xxxxxxxx",
"customerId": "cus_xxxxxxxx",
"customerCreated": true,
"paymentMethodId": "pm_xxxxxxxx",
"cardBrand": "CB",
"cardLast4": "0007"
}
}Quand un client est créé pendant le paiement (paramètre saveCustomer), le champ customerId contient l'identifiant créé et customerCreated vaut true. Si une carte est enregistrée au passage, paymentMethodId est fourni.
Répondez 2xx dans les 10 secondes pour acquitter la réception. Tout autre code (4xx/5xx) ou timeout déclenchera un retry.
Liste des événements
| Événement | Description |
|---|---|
| payment.created | Un paiement vient d'être initié. |
| payment.authorized | La carte du client a été autorisée (avant capture si MANUAL). |
| payment.captured | Le paiement est encaissé (les fonds sont transférés). |
| payment.failed | Le paiement a échoué (refus carte). |
| payment.expired | La session / le lien de paiement a expiré sans paiement. |
| payment.refunded | Un remboursement total ou partiel a été effectué. |
| payment.partially_refunded | Un remboursement partiel a été effectué. |
| payment.cancelled | Un paiement autorisé non capturé a été annulé. |
| customer.created | Un nouveau client a été créé via l'API. |
| customer.updated | Les informations d'un client ont été modifiées. |
| payment_method.added | Une nouvelle carte a été tokenisée pour un client. |
| payment_method.removed | Une carte tokenisée a été supprimée. |
| payout.created | Un virement de fonds vers votre compte bancaire a été initié. |
| payout.completed | Le virement a été exécuté. |
| payout.failed | Le virement a échoué. |
| dispute.created | Un litige (contestation) a été ouvert sur un paiement. |
| dispute.updated | Le statut d'un litige a évolué. |
Événements plateforme
Si vous êtes une plateforme (marketplace), abonnez-vous à ces événements pour suivre le cycle de vie de vos sous-marchands affiliés : invitation, onboarding KYB, ouverture du compte de paiement et rattachement.
| Événement | Description |
|---|---|
| merchant.session.created | Une session d'invitation a été créée pour un sous-marchand. |
| merchant.session.expired | Le lien d'invitation d'un sous-marchand a expiré. |
| merchant.session.cancelled | L'invitation d'un sous-marchand a été annulée. |
| merchant.created | Le sous-marchand a démarré son onboarding et a été créé (rattaché à votre plateforme). |
| merchant.opened | Le compte de paiement du sous-marchand a été ouvert (KYB validé, prêt à encaisser). |
| merchant.rejected | L'onboarding du sous-marchand a été refusé. |
| merchant.updated | Les informations du sous-marchand ont été modifiées. |
| merchant.linked | Un marchand existant a été rattaché à votre plateforme. |
| merchant.unlinked | Un sous-marchand a été détaché de votre plateforme. |
Vérification de signature
Chaque requête webhook contient un header X-Mobupay-Signature avec une signature HMAC-SHA256 du body. Vérifiez-la avant de traiter l'événement pour vous protéger des requêtes forgées.
Exemple Node.js :
import crypto from "node:crypto";
const SECRET = process.env.MOBUPAY_WEBHOOK_SECRET;
function verifySignature(rawBody, signature) {
const expected = crypto
.createHmac("sha256", SECRET)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post("/webhook", (req, res) => {
const signature = req.headers["x-mobupay-signature"];
if (!verifySignature(req.rawBody, signature)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.rawBody);
// ... traiter l'event
res.sendStatus(200);
});Important : utilisez le body brut (non-parsé) pour calculer la signature. Un body re-serialisé par JSON.stringify peut produire un hash différent.
Signature V2 (anti-rejeu, recommandée)
En plus de X-Mobupay-Signature (V1, HMAC du corps), chaque livraison porte X-Mobupay-Timestamp (epoch secondes) et X-Mobupay-Signature-V2 = HMAC-SHA256 de {timestamp}.{corps}. Le timestamp étant signé, une requête interceptée ne peut pas être rejouée plus tard : rejetez-la si l'horodatage dépasse votre fenêtre de tolérance (5 min recommandé).
import crypto from "node:crypto";
function verifyV2(rawBody, headers, secret, toleranceSec = 300) {
const ts = headers["x-mobupay-timestamp"];
const sig = headers["x-mobupay-signature-v2"];
if (!ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false; // anti-rejeu
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}V1 reste émise pour compatibilité. Les nouveaux intégrateurs (connecteurs e-commerce) doivent vérifier V2 + la fraîcheur du timestamp.
Politique de retry
Si votre endpoint ne répond pas 2xx dans les 10 secondes, Mobupay retry :
- 1ère retry : après 1 min
- 2ème : 5 min
- 3ème : 30 min
- 4ème : 2 h
- 5ème : 12 h
- Après 5 échecs : abandon (votre endpoint est considéré comme cassé). Vous pouvez voir l'historique dans le BO.
Idempotence côté receiver
À cause des retry, un même événement peut être livré plusieurs fois. Utilisez le champ id (préfixe evt_) pour dédupliquer côté serveur : stockez les IDs déjà traités et ignorez les doublons.