Docs/Webhooks

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

  1. Cliquez sur Ajouter un Webhook
  2. Entrez l'URL de votre endpoint (doit être HTTPS)
  3. Sélectionnez les événements auxquels vous souhaitez vous abonner
  4. 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énementDescription
payment.completedLe paiement a été confirmé on-chain et crédité sur votre wallet
payment.failedLe paiement a échoué (transaction annulée ou expirée)
refund.createdUn remboursement a été créé pour un paiement
refund.completedRemboursement terminé on-chain et envoyé au client
refund.failedLe 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

payment.completed payload
{
  "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

refund.completed payload
{
  "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_idstringRequired

Identifiant de paiement Fivo

amountstringRequired

Montant du paiement (chaîne décimale)

currencystringRequired

Symbole du token : USDC ou EURC

statusstringRequired

Statut du paiement : pending, completed ou failed

tx_hashstring | nullRequired

Hash de transaction on-chain

from_addressstring | nullRequired

Adresse du wallet du payeur

to_addressstring | nullRequired

Adresse du wallet du marchand

source_chainstring | nullRequired

Blockchain source (ex. ETH, BASE)

destination_chainstring | nullRequired

Blockchain de destination (null pour la même chaîne)

is_cross_chainbooleanRequired

Indique si le paiement a été transféré en inter-chaînes

merchant_transfer_tx_hashstringOptional

Hash TX du transfert vers le wallet du marchand (inter-chaînes uniquement)

customer_emailstring | nullOptional

E-mail du client si fourni lors du paiement

referencestring | nullOptional

Référence du marchand (ex. numéro de commande)

metadataobject | nullOptional

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-SignaturestringRequired

Signature HMAC-SHA256 avec le préfixe sha256= (voir Vérification des signatures)

X-Fivo-EventstringRequired

Type d'événement (ex. payment.completed)

X-Fivo-TimestampstringRequired

Horodatage Unix en secondes au moment de l'envoi de la requête

X-Fivo-TeststringOptional

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...

verify-webhook.js
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);
}
i

Comparaison à temps constant

Utilisez toujours au lieu de pour prévenir les attaques par timing.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)

server.js
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);