Chargement de vos clés API…

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.authorized",
  "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.

Détail des commissions dans l'événement

Les événements payment.authorized, payment.captured, payment.refunded, payment.partially_refunded et payment.cancelled portent un bloc settlement : la commission Mobupay, le net, et la composition ligne par ligne. Vous n'avez pas besoin de rappeler l'API pour savoir ce qui vous a été facturé.

"settlement": {
  "capturedAmount": 9479,
  "mobupayCommission": 165,
  "netAmount": 9314,
  "currency": "XPF",
  "settled": false,
  "detailedFees": [
    {
      "id": "spirit_commission",
      "label": "Commission sur encaissement",
      "type": "PERCENTAGE",
      "rate": 140,
      "amount": 132, "amountEur": 111,
      "taxAmount": 8, "taxAmountEur": 7,
      "taxes": [{ "id": "TGC6", "type": "PERCENTAGE", "value": 600, "amount": 8, "amountEur": 7 }],
      "totalAmount": 141, "totalAmountEur": 118
    },
    { "id": "spirit_fixed_fee", "label": "Frais fixe par transaction", "totalAmount": 16, "totalAmountEur": 13 },
    { "id": "authorization_result", "label": "Autorisation (capture différée)", "totalAmount": 8, "totalAmountEur": 7 }
  ]
}

Les montants sont dans la devise d'origine du paiement ; les champs amountEur portent la valeur exacte en centimes d'euro. La grille tarifaire étant en euros, chaque ligne est convertie puis arrondie au franc : la somme des totalAmount peut donc dépasser mobupayCommission d'un franc. Pour un rapprochement comptable exact, additionnez les totalAmountEur.

settled: false signifie que le règlement de l'acquéreur n'est pas encore parvenu : les lignes décrivent la facturation engagée. Les frais déjà prélevés ne sont pas restitués par un remboursement, qui ajoute sa propre ligne. Sur un paiement de place de marché, chaque entrée de platformDistribution porte de la même façon son detailedCommission.

Liste des événements

ÉvénementDescription
payment.createdUn paiement vient d'être initié.
payment.authorizedLa carte du client a été autorisée (avant capture si MANUAL).
payment.capturedLe paiement est encaissé (les fonds sont transférés).
payment.failedLe paiement a échoué (refus carte).
payment.expiredLa session / le lien de paiement a expiré sans paiement.
payment.refundedUn remboursement total ou partiel a été effectué.
payment.partially_refundedUn remboursement partiel a été effectué.
payment.cancelledUn paiement autorisé non capturé a été annulé.
customer.createdUn nouveau client a été créé via l'API.
customer.updatedLes informations d'un client ont été modifiées.
payment_method.addedUne nouvelle carte a été tokenisée pour un client.
payment_method.removedUne carte tokenisée a été supprimée.
payout.createdUn virement de fonds vers votre compte bancaire a été initié.
payout.completedLe virement a été exécuté.
payout.failedLe virement a échoué.
dispute.createdUn litige (contestation) a été ouvert sur un paiement.
dispute.updatedLe statut d'un litige a évolué.

Événements de facturation

Émis par le module Facturation, pour les factures dont vous êtes l'émetteur. Ils évitent d'interroger l'API en boucle pour savoir si une facture a été réglée.

La charge utile porte les identifiants, le statut, les montants et le reste dû, mais jamais les lignes ni l'identité de votre client : ces données ne traversent pas le réseau sans nécessité. Pour le détail complet, appelez GET /billing/invoices/{id}, qui sert toujours l'état courant, alors qu'un webhook peut arriver dans le désordre.

ÉvénementDescription
invoice.issuedUne facture a été émise : elle porte son numéro définitif et son contenu est figé.
invoice.sentLa facture a été transmise à son destinataire, avec son lien de règlement.
invoice.paidLa facture est intégralement réglée.
invoice.partially_paidUn règlement partiel a été imputé ; le reste dû figure dans la charge utile.
invoice.overdueL'échéance est dépassée sans règlement complet.
invoice.disputedUn règlement encaissé a été contesté : la facture est rouverte en litige.
invoice.creditedLa facture a été annulée par un avoir du montant total.
invoice.uncollectibleLa créance a été déclarée irrécouvrable.

É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énementDescription
merchant.session.createdUne session d'invitation a été créée pour un sous-marchand.
merchant.session.expiredLe lien d'invitation d'un sous-marchand a expiré.
merchant.session.cancelledL'invitation d'un sous-marchand a été annulée.
merchant.createdLe sous-marchand a démarré son onboarding et a été créé (rattaché à votre plateforme).
merchant.openedLe compte de paiement du sous-marchand a été ouvert (KYB validé, prêt à encaisser).
merchant.rejectedL'onboarding du sous-marchand a été refusé.
merchant.updatedLes informations du sous-marchand ont été modifiées.
merchant.linkedUn marchand existant a été rattaché à votre plateforme.
merchant.unlinkedUn 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.