FreeGate Docs

Webhooks

Signature HMAC, rejeu, idempotence et déduplication.

Un webhook est la façon dont FreeGate vous prévient du résultat d'un paiement. Chaque événement est signé. Vous devez le vérifier avant d'y faire confiance.

Vous déclarez votre endpoint webhook et récupérez son secret dans la console d'administration. Le secret ne s'affiche qu'une seule fois : gardez-le comme un secret.

En-têtes

Chaque livraison porte deux en-têtes :

En-têteDescription
x-freegate-event-idIdentifiant unique de l'événement.
x-freegate-signaturesha256= suivi du HMAC-SHA256 (hex) du corps brut de la requête.

Corps de l'événement

Voici un événement réel de payin réussi :

{
  "eventId": "evt_fwn_txn_bd43a004b88e48528cec30d6b8daac9d_succeeded",
  "eventType": "payment.payin.succeeded",
  "occurredAt": "2026-07-07T18:19:38.114Z",
  "data": {
    "transactionId": "fwn_txn_bd43a004b88e48528cec30d6b8daac9d",
    "type": "PAYIN",
    "status": "SUCCESS",
    "amount": "100.00",
    "currency": "XOF",
    "countryIso2": "CI",
    "operatorCode": "WAVE",
    "fee": "1.50",
    "netAmount": "98.50",
    "customerPhone": "+2250100000000"
  }
}

data reprend le détail complet de la transaction (montant, devise, opérateur, frais, net…), identique à GET /v1/transactions/{id}.

Événements courants : payment.payin.succeeded, payment.payin.failed, payment.refund.succeeded, payment.refund.failed, et les mêmes événements pour les payouts. Un événement *.expired arrive comme un échec (status: "FAILED" avec un failureReason en *_EXPIRED).

Le statut

data.status utilise le même vocabulaire que la ressource transaction (GET /v1/transactions/{id}) : PENDING, SUCCESS, FAILED, REFUNDED ou PARTIALLY_REFUNDED. Un seul jeu de valeurs à gérer, partout.

  • eventType vous dit quel événement s'est produit (payment.payin.succeeded, payment.refund.succeeded…).
  • data.status vous donne l'état de la transaction après cet événement. Un remboursement réussi porte donc REFUNDED ou PARTIALLY_REFUNDED, pas un simple « succès ».

Les échecs et les expirations arrivent tous les deux avec status: "FAILED" et un failureReason (par exemple PAYMENT_FAILED ou PAYMENT_EXPIRED).

Vérifier une signature

Vérifiez chaque webhook. Faites-le en trois étapes.

  1. Contrôlez la signature. Calculez le HMAC-SHA256 du corps brut avec votre secret webhook. Encodez-le en hexadécimal. Préfixez-le de sha256=. Comparez-le à x-freegate-signature en temps constant.
  2. Rejetez les événements trop vieux. Écartez tout événement dont occurredAt a plus de 5 minutes. L'horodatage est signé : on ne peut pas le falsifier.
  3. Dédupliquez par eventId. Les webhooks arrivent au moins une fois. Les rejeux sont normaux. Traitez chaque eventId une seule fois.
import crypto from 'node:crypto'

function isValidSignature(rawBody, header, secret) {
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(header)
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Signez sur le corps brut exact reçu (les octets), pas sur un JSON re-sérialisé. La moindre différence de format invalide la signature.

Idempotence côté réception

Comme les rejeux sont attendus, votre endpoint doit être idempotent.

  • Notez les eventId déjà traités. Ignorez les doublons.
  • Répondez 2xx vite. Faites le travail lourd après.
  • Un 4xx de votre endpoint est un rejet. FreeGate peut réessayer.

Sur cette page