Webhooks
Recevez des notifications en temps réel lorsqu'une transaction change de statut. Chaque notification est signée HMAC-SHA256 pour garantir son authenticité.
Configuration
Configurez votre URL webhook lors de la génération de votre clé API ou via le dashboard marchand. Votre endpoint doit :
- Répondre en HTTPS
- Retourner HTTP 200 en moins de 10 secondes
- Vérifier la signature
X-Signature
Webhook secret obligatoire
Pour recevoir des webhooks signés et bénéficier des retries automatiques, votre clé API doit avoir un webhook_secret. Sans celui-ci, les retries seront abandonnés et vous ne pourrez pas vérifier l'authenticité des notifications.
Types d'événements
| Événement | Déclencheur |
|---|---|
payin.success | Paiement entrant confirmé |
payin.failed | Paiement entrant refusé ou timeout |
payin.pending | Paiement entrant en attente de confirmation |
payout.success | Virement sortant confirmé |
payout.failed | Virement sortant échoué — wallet recrédité automatiquement |
Format du payload
{
"event": "payin.success",
"transaction_id": "PAY_XYZABC123456_1719100000",
"provider": "envoifacile",
"provider_ref": "d22c81f3-ee04-4ec5-8bd2-cd8af5dabcfc",
"status": "success",
"message": "Payment confirmed.",
"action_url": null,
"fee_percent": 2.5,
"fee_amount": 125,
"net_amount": 4875,
"timestamp": "2024-06-23T12:00:00+00:00"
}
Champs financiers
Les champs fee_percent, fee_amount et net_amount sont désormais inclus dans tous les événements de paiement pour faciliter votre réconciliation.
Vérification de la signature
Chaque requête webhook inclut le header X-Signature avec une signature HMAC-SHA256 du payload brut (raw body). Vérifiez la signature sur le corps brut de la requête, pas sur un JSON ré-encodé.
X-Signature: sha256=a1b2c3d4e5f6...
X-Event: payin.success
Algorithme
X-Signature = "sha256=" + HMAC-SHA256(raw_request_body, webhook_secret)
Tester votre implémentation
Utilisez l'endpoint de test pour générer un payload signé avec votre propre webhook_secret :
curl -X POST https://www.envoifacile.com/api/v1/merchant/webhooks/test-signature \
-H "X-API-KEY: ef_live_xxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"test": true}'
Vérification en PHP
public function handleWebhook(Request $request): JsonResponse
{
$secret = env('ENVOIFACILE_WEBHOOK_SECRET');
$payload = $request->getContent();
$signature = $request->header('X-Signature');
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature ?? '')) {
return response()->json(['error' => 'Invalid signature'], 401);
}
$data = $request->json()->all();
$event = $data['event'];
$trxId = $data['transaction_id'];
match ($event) {
'payin.success' => $this->onPayinSuccess($trxId, $data),
'payout.success' => $this->onPayoutSuccess($trxId, $data),
'payin.failed', 'payout.failed' => $this->onPaymentFailed($trxId, $data),
default => null,
};
return response()->json(['received' => true]);
}
Vérification en Node.js
const crypto = require('crypto');
app.post('/webhook/envoifacile', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.ENVOIFACILE_WEBHOOK_SECRET;
const signature = req.headers['x-signature'];
const payload = req.body;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = JSON.parse(payload);
console.log(`Event: ${data.event} | Trx: ${data.transaction_id}`);
res.json({ received: true });
});
Vérification en Python
import hmac
import hashlib
from flask import request, abort
@app.route('/webhook', methods=['POST'])
def webhook():
raw_body = request.get_data(as_text=True)
signature = request.headers.get('X-Signature', '')
secret = 'votre_webhook_secret'.encode()
expected = 'sha256=' + hmac.new(secret, raw_body.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
return 'OK'
Utilisez toujours hash_equals, timingSafeEqual ou compare_digest
Ne comparez jamais les signatures avec == ou ===. Ces opérateurs sont vulnérables aux attaques timing. Utilisez hash_equals() (PHP), crypto.timingSafeEqual() (Node.js) ou hmac.compare_digest() (Python).
Politique de retry
Si votre endpoint ne répond pas HTTP 200 dans les 10 secondes, nous retentons automatiquement jusqu'à 3 fois. Un jitter aléatoire de ±20% est appliqué sur chaque délai pour éviter les pics simultanés.
| Tentative | Délai (base) | Avec jitter | Statut webhook |
|---|---|---|---|
| 1ère | immédiate | 0s | pending |
| 2ème | +60s | ~48s à ~72s | pending |
| 3ème (finale) | +5min | ~4min à ~6min | abandoned si échec |
Conditions d'abandon des retries
Les retries sont abandonnés si votre clé API est désactivée ou si votre webhook_secret est vide. Assurez-vous donc que les deux sont configurés correctement.
Idempotence de votre endpoint
Votre endpoint peut recevoir le même événement plusieurs fois (retries). Utilisez le transaction_id comme clé d'idempotence côté réception.