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.
| Champ | Rôle |
|---|---|
label | Nom lisible : « Back-office », « ERP », « Monitoring ». |
url | L'endpoint HTTPS qui reçoit les POST. |
events | Liste blanche d'événements. Vide ou absente = tous les événements. |
is_active | Suspendre sans supprimer — secret et historique conservés. |
secret | Clé 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ègle | Détail |
|---|---|
| Schéma | https:// obligatoire en production. |
| Port | Port standard uniquement (443 en HTTPS) — pas de :8080. |
| Identifiants | Refusés dans l'URL (https://user:mdp@…). |
| Hôte | Doit 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. |
| Redirections | Non 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}/testQuelle destination reçoit quoi
NguePay résout les destinations dans cet ordre, et s'arrête au premier cas qui s'applique :
- Une
webhook_urlposé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. - Sinon, toutes les destinations actives abonnées à l'événement.
- 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.
payment.succeeded— paiement encaissépayment.failed— paiement échouépayment.canceled— paiement annulérefund.succeeded— remboursement effectuépayout.succeeded,payout.failed,payout.canceled— virement sortant concluwebhook.test— livraison de test déclenchée depuis le dashboard
Vérifier la signature
| En-tête | Contenu |
|---|---|
X-NguePay-Signature | HMAC-SHA256 hexadécimal de "{timestamp}.{corps brut}". |
X-NguePay-Timestamp | Horodatage Unix de l'envoi. |
X-NguePay-Event | Nom de l'événement. |
X-NguePay-Delivery | Identifiant 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-Type | application/json. |
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 '', 200Ce que doit faire votre endpoint
- Vérifier la signature. Sinon
401, et on s'arrête là. - Vérifier l'horodatage (tolérance 5 minutes).
- Vérifier que
X-NguePay-Deliveryn'a pas déjà été traité. - Répondre
200immédiatement. - 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 serveur | Ce que fait NguePay |
|---|---|
2xx | Livraison 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éseau | 3 tentatives au total : immédiate, puis 1 et 5 minutes plus tard. |
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
- Un secret distinct par destination — c'est le défaut, ne le contournez pas
- Endpoint HTTPS, joignable publiquement, sans authentification préalable ni redirection
- Idempotence sur
X-NguePay-Delivery - Abonner chaque destination aux seuls événements qu'elle traite
- Surveiller le compteur d'échecs consécutifs de chaque destination
- Pour les montants importants, relire la transaction par API avant de livrer