Webhooks

Plusieurs destinations par compte, chacune avec son secret et ses événements. C'est le seul canal qui fait foi.

Qu'est-ce qu'un webhook

Un webhook est une requête POST que NguePay envoie à votre serveur lorsqu'un événement se produit. C'est le seul canal qui fait foi : ce que le navigateur du client affiche n'est pas une preuve d'encaissement.

Plusieurs destinations, pas une seule

Un compte NguePay peut déclarer jusqu'à cinq destinations. Chacune reçoit sa propre copie de l'événement, signée avec son propre secret, avec son propre cycle de tentatives.

Les conséquences pratiques comptent : votre ERP en panne ne retarde pas la notification de votre back-office ; révoquer le secret de votre outil de monitoring ne casse pas votre boutique ; et vous branchez un nouvel outil sans toucher à l'existant.

ChampRôle
labelNom lisible : « Back-office », « ERP », « Monitoring ».
urlL'endpoint HTTPS qui reçoit les POST.
eventsListe blanche d'événements. Vide ou absente = tous les événements.
is_activeSuspendre sans supprimer — secret et historique conservés.
secretClé HMAC propre à cette destination (wh_…). Renvoyée uniquement par la création et la rotation.

En lecture, une destination expose aussi uuid, description, created_at et son état de livraison : consecutive_failures (livraisons définitivement échouées depuis le dernier succès, remis à 0 au premier succès — la destination n'est jamais désactivée automatiquement), last_success_at et last_failure_at. events vaut null quand la destination reçoit tout. Format complet : objet Destination webhook.

URL acceptées

Une URL de webhook est une adresse que les serveurs NguePay vont appeler. Elle est contrôlée à l'enregistrement, puis à chaque livraison :

RègleDétail
Schémahttps:// obligatoire en production.
PortPort standard uniquement (443 en HTTPS) — pas de :8080.
IdentifiantsRefusés dans l'URL (https://user:mdp@…).
HôteDoit résoudre vers une adresse publique. Adresses privées, locales ou réservées (10.x, 192.168.x, 127.0.0.1, noms .local…) refusées.
RedirectionsNon suivies. Une réponse 3xx fait échouer la livraison, sans nouvelle tentative.

Ces règles valent aussi pour la webhook_url posée sur un paiement. Déclarez l'URL finale : si votre serveur redirige http vers https, ou ajoute un / final, la livraison n'arrivera jamais.

Gérer les destinations par API

# Lister (renvoie aussi le catalogue d'événements et le quota)
curl https://api.live.nguepay.com/api/v1/webhook-endpoints \
  -H "Authorization: Bearer sk_live_VOTRE_CLE"

# Créer — le secret n'est renvoyé QUE dans cette réponse
curl -X POST https://api.live.nguepay.com/api/v1/webhook-endpoints \
  -H "Authorization: Bearer sk_live_VOTRE_CLE" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "ERP",
    "url": "https://erp.exemple.ci/nguepay",
    "events": ["payment.succeeded", "refund.succeeded"]
  }'

# Modifier (partiel), supprimer
curl -X PUT    https://api.live.nguepay.com/api/v1/webhook-endpoints/{uuid} -d '{"is_active": false}'
curl -X DELETE https://api.live.nguepay.com/api/v1/webhook-endpoints/{uuid}

# Régénérer le secret / envoyer une livraison de test
curl -X POST https://api.live.nguepay.com/api/v1/webhook-endpoints/{uuid}/rotate-secret
curl -X POST https://api.live.nguepay.com/api/v1/webhook-endpoints/{uuid}/test
À la rotation, l'ancien secret cesse immédiatement de signer. Déployez le nouveau avant de faire tourner : entre la rotation et votre déploiement, toute livraison sera rejetée par votre propre vérification.

Quelle destination reçoit quoi

NguePay résout les destinations dans cet ordre, et s'arrête au premier cas qui s'applique :

  1. Une webhook_url posée sur la transaction à sa création : elle devient la destination unique de cette transaction — elle remplace vos destinations déclarées, elle ne s'y ajoute pas.
  2. Sinon, toutes les destinations actives abonnées à l'événement.
  3. Sinon, l'URL unique historique du compte. Dès qu'une destination est déclarée, ce repli n'est plus utilisé.

Événements

Le catalogue complet, avec la charge utile de chacun, est sur la page Référence des événements.

Vérifier la signature

En-têteContenu
X-NguePay-SignatureHMAC-SHA256 hexadécimal de "{timestamp}.{corps brut}".
X-NguePay-TimestampHorodatage Unix de l'envoi.
X-NguePay-EventNom de l'événement.
X-NguePay-DeliveryIdentifiant unique de la livraison — votre clé d'idempotence. Identique d'une relance à l'autre, propre à chaque destination ; c'est l'uuid du journal des livraisons.
Content-Typeapplication/json.
La signature couvre l'horodatage et le corps, pas le corps seul. Signer le corps seul rendrait toute livraison interceptée rejouable indéfiniment ; avec l'horodatage, vous refusez tout ce qui a plus de cinq minutes.

Calculez le HMAC sur le corps brut, jamais sur un JSON ré-encodé : un espace ou un ordre de clés différent produit une signature différente.

<?php

$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_NGUEPAY_TIMESTAMP'] ?? '';
$received  = $_SERVER['HTTP_X_NGUEPAY_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $timestamp . '.' . $body, getenv('NGUEPAY_WEBHOOK_SECRET'));

if (! hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}

if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(401); // rejeu trop ancien
    exit;
}

http_response_code(200);
traiter_en_arriere_plan(json_decode($body, true));
const crypto = require('crypto');

// Le corps BRUT est indispensable : express.json() le reparse et
// la signature ne correspondrait plus.
app.post('/webhooks/nguepay',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const timestamp = req.get('X-NguePay-Timestamp') || '';
    const received  = req.get('X-NguePay-Signature') || '';

    const expected = crypto
      .createHmac('sha256', process.env.NGUEPAY_WEBHOOK_SECRET)
      .update(timestamp + '.' + req.body.toString('utf8'))
      .digest('hex');

    if (expected !== received) return res.sendStatus(401);
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);

    res.sendStatus(200);
    queue.push(JSON.parse(req.body.toString('utf8')));
  });
import hmac, hashlib, time, os
from flask import request, abort

SECRET = os.environ['NGUEPAY_WEBHOOK_SECRET'].encode()

@app.post('/webhooks/nguepay')
def nguepay():
    raw = request.get_data()  # corps brut, surtout pas request.json
    timestamp = request.headers.get('X-NguePay-Timestamp', '')
    received = request.headers.get('X-NguePay-Signature', '')

    expected = hmac.new(
        SECRET, timestamp.encode() + b'.' + raw, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, received):
        abort(401)
    if abs(time.time() - int(timestamp or 0)) > 300:
        abort(401)

    enqueue(request.get_json())
    return '', 200

Ce que doit faire votre endpoint

  1. Vérifier la signature. Sinon 401, et on s'arrête là.
  2. Vérifier l'horodatage (tolérance 5 minutes).
  3. Vérifier que X-NguePay-Delivery n'a pas déjà été traité.
  4. Répondre 200 immédiatement.
  5. Faire le travail métier en arrière-plan.

L'ordre compte. Un endpoint qui fait le travail avant de répondre finit par dépasser le délai de 10 secondes, se voit rejouer, et traite deux fois la même commande.

Tentatives et échecs

Réponse de votre serveurCe que fait NguePay
2xxLivraison marquée livrée. Terminé.
3xxÉchec définitif : les redirections ne sont pas suivies.
4xxÉchec définitif, aucun rejeu : rejouer une requête refusée à l'identique ne la rendra pas acceptable.
5xx, timeout, réseau3 tentatives au total : immédiate, puis 1 et 5 minutes plus tard.
Piège classique : si votre vérification de signature répond 401, la livraison est abandonnée sans rejeu. Un secret mal déployé se traduit donc par une perte silencieuse — d'où la livraison de test avant mise en service.
# Journal des livraisons, filtrable par destination
curl "https://api.live.nguepay.com/api/v1/webhooks?endpoint={uuid}&status=failed" \
  -H "Authorization: Bearer sk_live_VOTRE_CLE"

# Rejouer une livraison échouée — elle repart vers la MÊME destination
curl -X POST https://api.live.nguepay.com/api/v1/webhooks/{delivery_uuid}/retry \
  -H "Authorization: Bearer sk_live_VOTRE_CLE"

Chaque livraison du journal expose status (pending, delivered, failed), attempts, response_status (dernier code HTTP de votre serveur, null sans réponse), last_attempt_at, next_retry_at, l'URL appelée, la destination (endpoint) et la transaction concernée. Le corps envoyé et la réponse de votre serveur ne sont pas exposés. Format complet : objet Livraison webhook.

Bonnes pratiques

Besoin d'aide ?