Notifications

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

eventQuand
payment.succeededDépôt reçu : paiement réussi, portefeuille crédité (montant − frais)
payment.expiredAucun dépôt reçu dans les 15 minutes
withdrawal.succeededRetrait envoyé au bénéficiaire
withdrawal.failedRetrait en échec : portefeuille remboursé (montant + frais)
withdrawal.under_reviewPas 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émentUsage
X-Gateway-EventType d’évènement
X-Gateway-DeliveryIdentifiant unique de la notification : sert à ignorer les doublons
X-Gateway-TimestampHeure d’envoi (secondes depuis 1970, UTC) : sert à refuser les rejeux
X-Gateway-SignatureSignature HMAC-SHA256 : prouve que l’appel vient de Nebryon
X-Gateway-EnvironmentEnvironnement : LIVE ou SANDBOX (opération de test)
livemodefalse pour un évènement de test : à ignorer en production (voir SandBox)
sequenceNuméro d’ordre de la notification pour cette transaction
dataLa 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-Environment ou livemode.
  • Il ne sert pas en WebSocket : la connexion /ws/partner est déjà authentifiée par la clé API.
Bonne pratiquePourquoi
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 webhookLe 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 validesUn 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.
  1. 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.
  2. Calculez HMAC-SHA256(secret, X-Gateway-Timestamp + "." + corps brut) en hexadécimal minuscule, avec votre secret webhook whsec_….
  3. Comparez-le à X-Gateway-Signature (après sha256=) avec une comparaison à temps constant.
  4. Refusez si l’horodatage a plus de 5 minutes d’écart avec votre horloge.
  5. Si une vérification échoue, répondez 401 et ne traitez rien. Sinon, répondez 2xx rapidement, 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 "", 200

Répondre, relances, doublons, ordre

  • Répondez vite : un 2xx en 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-Delivery déjà traité.
  • Ordre : pour une transaction, les notifications arrivent une par une avec un sequence croissant ; ignorez une notification dont le sequence est déjà dépassé. data.status fait foi.