Developer Portal Webhooks

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énementDéclencheur
payin.successPaiement entrant confirmé
payin.failedPaiement entrant refusé ou timeout
payin.pendingPaiement entrant en attente de confirmation
payout.successVirement sortant confirmé
payout.failedVirement sortant échoué — wallet recrédité automatiquement

Format du payload

json Exemple payin.success
{
  "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é.

text Header X-Signature
X-Signature: sha256=a1b2c3d4e5f6...
X-Event: payin.success

Algorithme

text Formule
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 :

bash Endpoint de test signature
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

php Laravel — Vérification HMAC
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

javascript Express.js — Vérification HMAC
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

python Flask — Vérification HMAC
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.

TentativeDélai (base)Avec jitterStatut webhook
1èreimmédiate0spending
2ème+60s~48s à ~72spending
3ème (finale)+5min~4min à ~6minabandoned 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.