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ête | Description |
|---|---|
x-freegate-event-id | Identifiant unique de l'événement. |
x-freegate-signature | sha256= 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.
eventTypevous dit quel événement s'est produit (payment.payin.succeeded,payment.refund.succeeded…).data.statusvous donne l'état de la transaction après cet événement. Un remboursement réussi porte doncREFUNDEDouPARTIALLY_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.
- 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-signatureen temps constant. - Rejetez les événements trop vieux. Écartez tout événement dont
occurredAta plus de 5 minutes. L'horodatage est signé : on ne peut pas le falsifier. - Dédupliquez par
eventId. Les webhooks arrivent au moins une fois. Les rejeux sont normaux. Traitez chaqueeventIdune 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
eventIddéjà traités. Ignorez les doublons. - Répondez
2xxvite. Faites le travail lourd après. - Un
4xxde votre endpoint est un rejet. FreeGate peut réessayer.