Webhooks
Recevez des notifications en temps réel lorsque des paiements sont terminés, échoués ou en attente.
Aperçu
Les Webhooks permettent à votre serveur de recevoir automatiquement des notifications HTTP POST lorsque des événements se produisent dans votre compte Fivo. Au lieu d'interroger l'API, vous enregistrez une URL et Fivo vous envoie les données de l'événement en temps réel.
Chaque requête webhook est signée avec HMAC-SHA256 afin que vous puissiez vérifier qu'elle provient de Fivo.
Configuration
Configurez les Webhooks depuis votre page :Tableau de bord → Webhooks
- Cliquez sur Ajouter un Webhook
- Entrez l'URL de votre endpoint (doit être HTTPS)
- Sélectionnez les événements auxquels vous souhaitez vous abonner
- Enregistrez : Fivo génère un secret de signature pour vous
Exigences
- L'URL doit utiliser HTTPS
- L'URL ne peut pas pointer vers des adresses IP privées/internes (protection SSRF)
- Votre endpoint doit répondre avec un statut 2xx dans les 5 secondes
Événements
| Événement | Description |
|---|---|
payment.completed | Le paiement a été confirmé on-chain et crédité sur votre wallet |
payment.failed | Le paiement a échoué (transaction annulée ou expirée) |
refund.created | Un remboursement a été créé pour un paiement |
refund.completed | Remboursement terminé on-chain et envoyé au client |
refund.failed | Le remboursement a échoué (vérifiez failure_reason dans data) |
Contenu
Chaque webhook envoie un contenu JSON avec le type d'événement, un horodatage et les données de l'événement :
Événements de paiement
{
"event": "payment.completed",
"timestamp": "2026-03-04T10:30:45.123Z",
"data": {
"payment_id": "fivo_live_abc123",
"amount": "100.00",
"currency": "USDC",
"status": "completed",
"tx_hash": "0xabc...def",
"from_address": "0x1234...5678",
"to_address": "0xabcd...ef01",
"source_chain": "ETH",
"destination_chain": "BASE",
"is_cross_chain": true,
"merchant_transfer_tx_hash": "0x9876...5432",
"customer_email": "customer@example.com",
"reference": "ORD-2026-001",
"metadata": { "product_id": "plan_pro" }
}
}Événements de remboursement
{
"event": "refund.completed",
"timestamp": "2026-03-04T10:35:20.456Z",
"data": {
"refund_id": "refund-uuid",
"payment_id": "payment-uuid",
"amount": "49.99",
"currency": "USDC",
"status": "completed",
"destination_address": "0xCustomer...",
"blockchain": "BASE",
"reason": "requested_by_customer",
"tx_hash": "0xabc..."
}
}Champs des données de paiement
Objet data (événements de paiement)
payment_idstringRequiredIdentifiant de paiement Fivo
amountstringRequiredMontant du paiement (chaîne décimale)
currencystringRequiredSymbole du token : USDC ou EURC
statusstringRequiredStatut du paiement : pending, completed ou failed
tx_hashstring | nullRequiredHash de transaction on-chain
from_addressstring | nullRequiredAdresse du wallet du payeur
to_addressstring | nullRequiredAdresse du wallet du marchand
source_chainstring | nullRequiredBlockchain source (ex. ETH, BASE)
destination_chainstring | nullRequiredBlockchain de destination (null pour la même chaîne)
is_cross_chainbooleanRequiredIndique si le paiement a été transféré en inter-chaînes
merchant_transfer_tx_hashstringOptionalHash TX du transfert vers le wallet du marchand (inter-chaînes uniquement)
customer_emailstring | nullOptionalE-mail du client si fourni lors du paiement
referencestring | nullOptionalRéférence du marchand (ex. numéro de commande)
metadataobject | nullOptionalMétadonnées personnalisées attachées au paiement
| Name | Type | Required | Description |
|---|---|---|---|
payment_id | string | Required | Identifiant de paiement Fivo |
amount | string | Required | Montant du paiement (chaîne décimale) |
currency | string | Required | Symbole du token : USDC ou EURC |
status | string | Required | Statut du paiement : pending, completed ou failed |
tx_hash | string | null | Required | Hash de transaction on-chain |
from_address | string | null | Required | Adresse du wallet du payeur |
to_address | string | null | Required | Adresse du wallet du marchand |
source_chain | string | null | Required | Blockchain source (ex. ETH, BASE) |
destination_chain | string | null | Required | Blockchain de destination (null pour la même chaîne) |
is_cross_chain | boolean | Required | Indique si le paiement a été transféré en inter-chaînes |
merchant_transfer_tx_hash | string | Optional | Hash TX du transfert vers le wallet du marchand (inter-chaînes uniquement) |
customer_email | string | null | Optional | E-mail du client si fourni lors du paiement |
reference | string | null | Optional | Référence du marchand (ex. numéro de commande) |
metadata | object | null | Optional | Métadonnées personnalisées attachées au paiement |
En-têtes de requête
Fivo inclut ces en-têtes avec chaque requête webhook :
En-têtes
X-Fivo-SignaturestringRequiredSignature HMAC-SHA256 avec le préfixe sha256= (voir Vérification des signatures)
X-Fivo-EventstringRequiredType d'événement (ex. payment.completed)
X-Fivo-TimestampstringRequiredHorodatage Unix en secondes au moment de l'envoi de la requête
X-Fivo-TeststringOptionalDéfini à "true" pour les webhooks de test envoyés depuis le tableau de bord
| Name | Type | Required | Description |
|---|---|---|---|
X-Fivo-Signature | string | Required | Signature HMAC-SHA256 avec le préfixe sha256= (voir Vérification des signatures) |
X-Fivo-Event | string | Required | Type d'événement (ex. payment.completed) |
X-Fivo-Timestamp | string | Required | Horodatage Unix en secondes au moment de l'envoi de la requête |
X-Fivo-Test | string | Optional | Défini à "true" pour les webhooks de test envoyés depuis le tableau de bord |
Vérification des signatures
Vérifiez toujours l'en-tête pour confirmer que la requête provient de Fivo. La signature est calculée comme en utilisant votre secret webhook comme clé, préfixée avec .X-Fivo-SignatureHMAC-SHA256(timestamp + "." + body)sha256=.
Votre secret webhook ressemble à : whsec_a1b2c3d4e5f6...
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-fivo-signature'];
const timestamp = req.headers['x-fivo-timestamp'];
const body = JSON.stringify(req.body);
// 1. Reject if timestamp is older than 5 minutes (replay protection)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 5 * 60) {
throw new Error('Webhook timestamp too old');
}
// 2. Compute expected signature: HMAC-SHA256(timestamp + "." + body)
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + body)
.digest('hex');
// 3. Compare using timing-safe comparison
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(body);
}Comparaison à temps constant
crypto.timingSafeEqual()===Tentatives et désactivation automatique
Si votre endpoint retourne un statut non-2xx ou expire, Fivo incrémente un compteur d'échecs. Après échecs consécutifs, le webhook est automatiquement désactivé pour éviter d'autres livraisons échouées.10
Vous pouvez réactiver un webhook désactivé depuis le tableau de bord à tout moment. Le compteur d'échecs est réinitialisé à chaque livraison réussie.
Tests
Utilisez le bouton sur votre page pour envoyer un événement de test à votre endpoint. Les webhooks de test incluent l'en-tête afin que votre serveur puisse les distinguer des événements réels.Envoyer un testWebhooksX-Fivo-Test: true
Les journaux de livraison sont visibles sur la page de détail du webhook : vous pouvez inspecter le contenu, le statut de la réponse et le temps de réponse pour chaque livraison.
Exemple complet (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.FIVO_WEBHOOK_SECRET;
app.post('/webhooks/fivo', (req, res) => {
try {
// 1. Verify signature
const signature = req.headers['x-fivo-signature'];
const timestamp = req.headers['x-fivo-timestamp'];
const body = JSON.stringify(req.body);
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(timestamp + '.' + body)
.digest('hex');
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Replay protection
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 5 * 60) {
return res.status(401).json({ error: 'Timestamp too old' });
}
// 3. Handle event
const { event, data } = req.body;
switch (event) {
case 'payment.completed':
console.log('Payment received:', data.amount, data.currency);
console.log('From:', data.from_address, 'on', data.source_chain);
console.log('Customer:', data.customer_email);
console.log('Reference:', data.reference);
// Update your order status, send confirmation, etc.
break;
case 'payment.failed':
console.log('Payment failed:', data.payment_id);
break;
case 'refund.created':
console.log('Refund created:', data.refund_id);
break;
case 'refund.completed':
console.log('Refund completed:', data.refund_id);
break;
case 'refund.failed':
console.log('Refund failed:', data.refund_id);
break;
}
res.status(200).json({ received: true });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.listen(3000);