Webhooks
Nebryon prévient votre serveur par un appel HTTP quand un paiement ou un retrait se termine. L’URL se règle dans votre espace (Intégration).
Configurer l’URL
- HTTPS obligatoire, adresse publique (pas de
localhost, d’adresse privée ni de nom en.local). - Pas d’identifiants dans l’URL, et pas de redirection : votre URL doit répondre directement en
2xx. - En local, exposez votre serveur par un tunnel HTTPS public (ngrok, Cloudflare Tunnel…).
Évènements
| event | Quand |
|---|---|
payment.succeeded | Dépôt reçu : paiement réussi, portefeuille crédité (montant − frais) |
payment.expired | Aucun dépôt reçu dans les 15 minutes |
withdrawal.succeeded | Retrait envoyé au bénéficiaire |
withdrawal.failed | Retrait en échec : portefeuille remboursé (montant + frais) |
withdrawal.under_review | Pas de confirmation de l’opérateur : vérification manuelle ; succeeded ou failed suivra |
Il n’y a pas de webhook à la création : la réponse de votre appel en tient lieu.
Format de l’appel
| Élément | Usage |
|---|---|
X-Gateway-Event | Type d’évènement |
X-Gateway-Delivery | Identifiant unique de la notification : sert à ignorer les doublons |
X-Gateway-Timestamp | Heure d’envoi (secondes depuis 1970, UTC) : sert à refuser les rejeux |
X-Gateway-Signature | Signature HMAC-SHA256 : prouve que l’appel vient de Nebryon |
X-Gateway-Environment | Environnement : LIVE ou SANDBOX (opération de test) |
livemode | false pour un évènement de test : à ignorer en production (voir SandBox) |
sequence | Numéro d’ordre de la notification pour cette transaction |
data | La transaction à jour ; retrouvez votre commande par data.externalReference |
Requête reçue par votre serveur
POST https://votre-site.com/webhooks/gateway
Content-Type: application/json
X-Gateway-Event: payment.succeeded
X-Gateway-Delivery: a0912fcc-07bc-48c8-8d70-0ef54dcbfe2f
X-Gateway-Timestamp: 1791213506
X-Gateway-Signature: sha256=5f1c0e…
X-Gateway-Environment: LIVE
{
"id": "a0912fcc-07bc-48c8-8d70-0ef54dcbfe2f",
"event": "payment.succeeded",
"sequence": 1,
"livemode": true,
"createdAt": "2026-10-05T15:24:27.740809",
"data": {
"reference": "PAY261005DE7PIJWDCW",
"externalReference": "CMD-1042",
"type": "PAYMENT",
"status": "SUCCESS",
"amount": 5000.00,
"fee": 75.00,
"currency": "XOF",
"network": "ORANGE_MONEY",
"phoneNumber": "+22676112233",
"operatorReference": "CI241005.1234.A12345",
"completedAt": "2026-10-05T15:24:27.73765"
}
}Secret webhook : sécuriser la communication
Votre URL de webhook est publique : sans vérification, n’importe qui pourrait y envoyer un faux payment.succeeded et vous faire livrer une commande impayée. Le secret webhook (whsec_…) prouve que l’appel vient bien de Nebryon et que son contenu n’a pas été modifié en route.
- Il est connu seulement de Nebryon et de vous, et ne circule jamais dans les requêtes : seule la signature calculée avec lui est envoyée (
X-Gateway-Signature). - Il est affiché une seule fois, à la création du service, avec vos clés API.
- Le même secret signe les webhooks de production et de SandBox : distinguez-les par
X-Gateway-Environmentoulivemode. - Il ne sert pas en WebSocket : la connexion
/ws/partnerest déjà authentifiée par la clé API.
| Bonne pratique | Pourquoi |
|---|---|
| Stockez-le côté serveur (variable d’environnement, coffre de secrets) | Jamais dans une application mobile, du code front ou un dépôt de code |
| En cas de fuite, renouvelez-le : espace partenaire › Intégration › Nouveau secret webhook | Le nouveau secret est affiché une fois et signe immédiatement les envois suivants : installez-le aussitôt sur votre serveur |
| Refusez tout webhook dont la signature ou l’horodatage ne sont pas valides | Un message falsifié ou rejoué ne doit jamais être traité |
Calcul de la signature
signature = HMAC-SHA256(secret, timestamp + "." + corps)
X-Gateway-Timestamp: 1791213506
X-Gateway-Signature: sha256=<signature en hexadécimal>Vérifier la signature (obligatoire)
N’importe qui peut appeler votre URL : ne validez jamais une commande sans avoir vérifié la signature.
- Lisez le corps brut de la requête, tel que reçu : ne le re-sérialisez pas après un
JSON.parse, la signature ne correspondrait plus. - Calculez
HMAC-SHA256(secret, X-Gateway-Timestamp + "." + corps brut)en hexadécimal minuscule, avec votre secret webhookwhsec_…. - Comparez-le à
X-Gateway-Signature(aprèssha256=) avec une comparaison à temps constant. - Refusez si l’horodatage a plus de 5 minutes d’écart avec votre horloge.
- Si une vérification échoue, répondez
401et ne traitez rien. Sinon, répondez2xxrapidement, puis traitez.
Node.js (Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
app.post("/webhooks/gateway", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Gateway-Timestamp");
const received = (req.get("X-Gateway-Signature") || "").replace(/^sha256=/, "");
const expected = crypto
.createHmac("sha256", process.env.GATEWAY_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`) // req.body : Buffer brut
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(401);
const notification = JSON.parse(req.body);
res.sendStatus(200); // répondre vite…
handleNotification(notification); // …puis traiter (idempotent)
});PHP
$body = file_get_contents('php://input'); // corps brut
$timestamp = $_SERVER['HTTP_X_GATEWAY_TIMESTAMP'] ?? '';
$received = preg_replace('/^sha256=/', '', $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '');
$expected = hash_hmac('sha256', $timestamp . '.' . $body, getenv('GATEWAY_WEBHOOK_SECRET'));
if (abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$notification = json_decode($body, true);
http_response_code(200);
// traiter $notification (idempotent)Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/gateway")
def gateway_webhook():
body = request.get_data() # corps brut (bytes)
timestamp = request.headers.get("X-Gateway-Timestamp", "")
received = request.headers.get("X-Gateway-Signature", "").removeprefix("sha256=")
expected = hmac.new(os.environ["GATEWAY_WEBHOOK_SECRET"].encode(),
f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(timestamp or 0)) > 300 or not hmac.compare_digest(expected, received):
abort(401)
notification = request.get_json()
# traiter notification (idempotent)
return "", 200Répondre, relances, doublons, ordre
- Répondez vite : un
2xxen moins de 10 secondes, puis traitez en arrière-plan. - Relances : sans
2xx, Nebryon réessaie environ 30 s, 1, 2, 4, 8, 16 et 32 min après — 8 tentatives sur une heure. Ensuite, vous pouvez la renvoyer depuis votre espace (Intégration). - Doublons : une notification peut arriver plusieurs fois. Ignorez un
X-Gateway-Deliverydéjà traité. - Ordre : pour une transaction, les notifications arrivent une par une avec un
sequencecroissant ; ignorez une notification dont lesequenceest déjà dépassé.data.statusfait foi.