Que fait Nomba webhooks ?
Un webhook est la manière dont Nomba informe votre serveur que de l'argent a effectivement été transféré. N'importe qui sur Internet peut envoyer une requête POST à votre URL de webhook, donc le gestionnaire doit prouver que chaque requête provient bien de Nomba avant de marquer une commande comme payée ou de rembourser quiconque. Il doit également gérer les tentatives de relivraison de Nomba.
Utilisez le vérificateur du SDK officiel ; n'implémentez pas le HMAC vous-même. La signature de Nomba couvre des champs spécifiques joints par :, et les SDK bloquent également une astuce de rejeu qu'une implémentation naïve manque.
| Langage | Vérification |
|---|---|
Node.js / TS (@nombaoss/nomba-node) | verifyWebhook(rawBody, headers, signingKey) |
Python (nomba) | from nomba.webhooks import verify_signature → verify_signature(raw_body, headers, signing_key) |
PHP (nombaoss/nomba-php) | Nomba\Webhook::verify($rawBody, $headers, $signingKey) |
Les imports exacts et les types d'arguments pour chaque langage se trouvent dans références/sdk-setup.md. Chacun retourne l'événement analysé ou lève une exception (WebhookVerificationError / WebhookVerificationException). Aucun d'eux n'a besoin d'identifiants API, seulement la clé de signature du webhook depuis le tableau de bord Nomba (Développeur → Configuration du Webhook). Lisez-la depuis une variable d'environnement telle que NOMBA_WEBHOOK_SIGNING_KEY. C'est un secret différent du secret client de l'API.
Le gestionnaire, étape par étape
- Obtenez le corps brut. Vérifiez les octets exacts que Nomba a envoyés. Si un analyseur de corps JSON s'exécute en premier et que vous resérialisez l'objet, la signature peut cesser de correspondre, ou pire, vous finissez par vérifier autre chose que ce sur quoi vous agissez. Chaque framework a une manière différente d'obtenir le corps brut ; voir références/frameworks.md pour la bonne méthode, et excluez la route de la protection CSRF là où le framework en dispose.
- Vérifiez, puis analysez. Utilisez l'événement retourné par le vérificateur. En cas d'échec, répondez
401(ou400) et arrêtez-vous. Ne journalisez pas le corps complet ou les en-têtes des requêtes rejetées au niveau info. - Dédupliquez sur
requestId. Nomba relivre les événements, donc le même événement peut arriver plusieurs fois. Enregistrez chaquerequestIdtraité avec une contrainte d'unicité et ignorez ceux déjà vus. Retournez200également pour les doublons, sinon Nomba continue de relivrer. - Accusez réception rapidement. Retournez
200rapidement et effectuez les travaux lents (emails, exécution des commandes, mises à jour de comptabilité) dans une tâche en arrière-plan indexée parrequestId. Des gestionnaires lents provoquent des dépassements de délai et des relivraisons. - Agissez sur l'événement en utilisant le tableau ci-dessous. Faites correspondre l'événement à votre propre enregistrement par référence, et vérifiez que le montant et la devise correspondent à ce que vous attendiez avant d'exécuter.
Ce que signifie chaque événement
event_type | Correspondance sur | Action |
|---|---|---|
payment_success | checkout : data.order.orderReference ; compte virtuel / virement entrant : data.transaction.aliasAccountReference ou transactionId | Marquez comme payé après avoir vérifié data.transaction.transactionAmount (et la devise) par rapport à votre commande. Pour les checkouts tokenisés, stockez data.tokenizedCardData.tokenKey (s'il est présent et non "N/A") avec le client, côté serveur uniquement. |
payment_failed | identique à ci-dessus | Marquez la tentative comme échouée ; laissez le client réessayer. Rien n'a été débité. |
payment_reversal | identique à ci-dessus | L'argent que vous avez reçu a été reversé. Annulez l'exécution ou signalez pour révision. |
payout_success | data.transaction.merchantTxRef | Marquez le virement ou le paiement de facture comme effectué. |
payout_failed | data.transaction.merchantTxRef | Ne remboursez pas encore. Nomba peut réessayer par une autre voie. Marquez-le comme échoué et attendez. |
payout_refund | data.transaction.merchantTxRef | L'argent est de retour dans votre portefeuille Nomba. Maintenant, il est sûr de rembourser ou de créditer votre client et de marquer le virement comme échoué. |
Rembourser sur payout_failed (ou sur un dépassement de délai) au lieu de payout_refund est la manière classique pour les marchands de payer deux fois leurs clients. Pour tout événement que vous ne reconnaissez pas, journalisez-le et retournez 200.
Confirmez les décisions financières avec l'API. La signature de Nomba ne couvre que event_type, requestId, data.merchant.userId, data.merchant.walletId, data.transaction.transactionId, type, time, responseCode et le timestamp. Le montant, orderReference, merchantTxRef et le tokenKey de la carte ne sont pas signés. Par conséquent, avant d'exécuter, rembourser ou stocker un jeton de carte, réinterrogez avec le SDK et agissez sur la réponse de l'API : transactions.get({ orderReference }) pour les paiements par checkout (également checkout.getTransaction en production) et transactions.get({ merchantTxRef }) pour les virements. La déduplication sur le requestId signé empêche une copie modifiée d'un événement déjà traité, et la réinterrogation couvre le reste.
Protection anti-rejeu
La déduplication sur requestId est la défense principale : une relivraison rejouée ou retentée d'un événement déjà traité ne change rien.
Les vérificateurs peuvent également rejeter les timestamps anciens (toleranceSeconds / tolerance_seconds / le quatrième argument en PHP), mais laissez-le désactivé par défaut. Nomba documente nomba-timestamp comme l'heure à laquelle la charge utile a été envoyée, sans préciser si les nouvelles tentatives automatiques en reçoivent un nouveau. S'ils conservent l'original, une tolérance plus courte que la fenêtre de relivraison de Nomba rejetterait les relivraisons légitimes après une panne, et les commandes payées ne seraient jamais confirmées. Activez-le seulement après avoir confirmé dans le bac à sable que les relivraisons portent un nouveau timestamp, puis conservez-le généreux (par exemple une heure) avec les horloges des serveurs synchronisées.
Exemple (Node.js + Express)
import express from "express";
import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";
const app = express();
// Corps brut pour cette route uniquement ; enregistrez-la avant tout app-wide express.json().
app.post("/webhooks/nomba", express.raw({ type: "*/*" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!);
} catch {
return res.sendStatus(401);
}
// Contrainte d'unicité sur request_id ; retourne false si déjà vu.
const isNew = await webhookEvents.recordOnce(event.requestId, event.event_type);
if (isNew) await jobs.enqueue("nomba-webhook", event); // traiter en arrière-plan
res.sendStatus(200);
});
Python (Flask/FastAPI/Django), PHP (Laravel/plain) et autres frameworks Node sont dans références/frameworks.md.
Test
- Testez unitairement le gestionnaire avec le vecteur de test documenté du SDK : corps depuis
webhook-fixture.jsondu SDK, cléSampleSignatureKey123, en-têtenomba-timestamp: 2025-09-29T10:51:44Z,nomba-signature: q30yFZDyykoymBf9+mt1wz5UAuLk57raPGPHlGzIv00=. Vérifiez qu'une mauvaise clé, un corps modifié et des en-têtes manquants sont rejetés, et qu'unrequestIden double est traité une seule fois. - De bout en bout : définissez une URL de webhook de test et une clé de signature dans le tableau de bord, exposez votre serveur local avec un tunnel, et effectuez des paiements dans le bac à sable. Le tableau de bord peut repousser ou déboguer les livraisons (https://developer.nomba.com/docs/api-basics/troubleshoot-webhooks).
Checklist
- Le corps brut est vérifié avec le SDK, avant tout analyse ou logique métier.
- La clé de signature provient de l'environnement, pas du code.
- Les mauvaises signatures obtiennent un 401/400 ; rien d'autre ne se produit.
-
requestIdest dédupliqué avec une contrainte d'unicité ; les doublons obtiennent toujours 200. - Les travaux lourds s'exécutent en arrière-plan ; le gestionnaire retourne 200 rapidement.
- Le montant et la devise sont vérifiés par rapport à la commande avant exécution.
- Les remboursements ont lieu uniquement sur
payout_refund(ou une réinterrogation retournantREFUND). - La route est exclue de la protection CSRF et utilise HTTPS.