MobupayMobupay
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": "whe_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",
    "store": { "id": "sto_7Kq2mZp4nT8xB1", "code": "PAITA", "name": "Boutique de Païta" }
  }
}

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.

customerId est présent sur tous les événements payment.authorized d'un paiement rattaché à un client, créé à l'instant ou non. Stockez-le sur votre utilisateur : c'est lui qu'il faudra renvoyer à la création des sessions suivantes pour que le client retrouve ses cartes enregistrées. Voir le guide Cartes enregistrées.

Les événements payment.* et refund.* portent la boutique d'encaissement dans store, sous la forme { id, code, name }. Le code y figure, pas seulement l'identifiant : c'est lui que vos systèmes reconnaissent, et il suffit à ventiler l'encaissement sans rappeler l'API. Le champ vaut null quand le paiement n'est rattaché à aucune boutique. Voir le guide Plusieurs boutiques. Les événements de facturation et de compte n'en portent pas : une facture ou un client ne se rattache pas à un point de vente.

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.authorizedLa carte du client a été autorisée (avant capture si MANUAL).
payment.capturedLe paiement est encaissé. La commission annoncée est une projection.
payment.settledLes fonds sont reçus et la commission Mobupay est définitive.
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é.

É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 d'abonnement

Si vous prélevez des abonnements, ces événements suivent leur cycle de vie. Le plus important est subscription.mandate_revoked : c'est le seul avertissement qu'un prélèvement s'est arrêté faute de carte, et il vous parvient au moment où cela se produit, non à l'échéance suivante.

ÉvénementDescription
subscription.createdUn abonnement a été créé. Sans carte rattachée, il attend son mandat et ne prélève rien.
subscription.payment_succeededUne échéance a été prélevée. La charge utile porte l'échéance et le paiement.
subscription.payment_failedUne échéance a été refusée. Une nouvelle tentative est programmée selon votre cadence tant qu'il en reste.
subscription.past_dueLes tentatives sont épuisées : l'échéance devient un impayé, et la relance repasse au module Facturation si une facture y est liée.
subscription.mandate_revokedLa carte ne peut plus être prélevée (supprimée par le porteur ou par vous, désactivée, ou introuvable chez l'acquéreur). L'abonnement repasse en attente de mandat et cesse d'ouvrir des échéances. `reason` dit lequel de ces cas s'est produit. C'est l'événement à écouter pour redemander une carte à votre client.
subscription.mandate_restoredUne carte a été rattachée et l'abonnement reprend à sa prochaine échéance. `reason` dit qui l'a rattachée : vous, votre intégration, ou le porteur depuis son portefeuille.
subscription.cancelledL'abonnement est résilié. C'est définitif : l'échéancier ne reprend pas.

Événements de compte

Le cycle de vie de vos clients, de leurs cartes enregistrées, de vos virements et de vos litiges. Ce sont les événements qui surviennent sans que vous les ayez déclenchés : une carte que la banque du porteur cesse de reconnaître, un virement rejeté par l'interbancaire plusieurs jours après son départ, une contestation.

La charge utile est complète : elle porte l'objet tel que vous le connaissez, pour que vous puissiez la recopier sans rappeler l'API. Deux réserves, toutes deux volontaires. Un virement ne porte jamais les coordonnées bancaires du bénéficiaire, seulement son recipientId : une charge utile est poussée vers votre URL et finit dans vos journaux. Un litige ne porte pas la charge brute reçue de notre partenaire bancaire, qui contient des données du porteur de carte.

ÉvénementDescription
customer.createdUne fiche client a été créée, par l'API ou par un paiement portant l'adresse de l'acheteur.
customer.updatedUne fiche client a changé. Le champ `updatedFields` dit quoi, pour vous éviter de tout comparer.
payment_method.addedUne carte a été enregistrée pour un client, par l'API ou à l'occasion d'un paiement.
payment_method.removedUne carte ne peut plus servir. Le champ `reason` dit pourquoi : `deleted`, `disabled`, ou `unknown_at_acquirer` quand la banque du porteur ne la reconnaît plus.
payout.createdUn virement vers votre compte bancaire a été initié, manuellement ou par votre calendrier de reversement.
payout.completedLe virement a été exécuté.
payout.failedLe virement a échoué, a été rejeté par l'interbancaire ou annulé. Le champ `failureReason` porte le motif.
dispute.createdUne contestation ou un impayé est arrivé sur un de vos paiements.
dispute.updatedLe statut d'un litige a évolué. `previousStatus` donne l'état précédent.

É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 invitation a été émise pour un sous-marchand.
merchant.session.expiredUn lien d'invitation a expiré sans qu'aucun dossier n'ait été ouvert. Réémettez une invitation.
merchant.session.cancelledL'invitation d'un sous-marchand a été annulée.
merchant.onboarding.updatedL'état d'enrôlement d'un sous-marchand a changé. C'est l'événement à écouter pour suivre un dossier pas à pas : les autres n'en donnent que les bornes.
merchant.createdLe compte du sous-marchand a été créé et rattaché à votre plateforme.
merchant.openedLe compte de paiement du sous-marchand a été ouvert (KYB validé, prêt à encaisser).
merchant.rejectedUne décision défavorable est intervenue. Lisez `enrollmentStatus` pour savoir laquelle : `CORRECTION_REQUESTED` (des corrections sont attendues, le dossier vit toujours) ou `REFUSED` (fin de parcours).
merchant.linkedUn marchand existant a été rattaché à votre plateforme.
merchant.unlinkedUn sous-marchand a été détaché de votre plateforme. Son compte, lui, reste ouvert.

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.