Docs/Webhooks

Webhooks

Erhalten Sie Echtzeit-Benachrichtigungen, wenn Zahlungen abgeschlossen, fehlgeschlagen oder ausstehend sind.

Übersicht

Webhooks ermöglichen es Ihrem Server, automatische HTTP-POST-Benachrichtigungen zu empfangen, wenn Ereignisse in Ihrem Fivo-Konto auftreten. Anstatt die API abzufragen, registrieren Sie eine URL und Fivo sendet Ihnen die Ereignisdaten in Echtzeit.

Jede Webhook-Anfrage wird mit HMAC-SHA256 signiert, damit Sie verifizieren können, dass sie von Fivo stammt.

Einrichtung

Konfigurieren Sie Webhooks über Ihre Seite :Dashboard → Webhooks

  1. Klicken Sie auf Webhook hinzufügen
  2. Geben Sie Ihre Endpunkt-URL ein (muss HTTPS sein)
  3. Wählen Sie die Ereignisse aus, die Sie abonnieren möchten
  4. Speichern: Fivo generiert ein Signatur-Secret für Sie
!

Anforderungen

  • URL muss HTTPS verwenden
  • URL darf nicht auf private/interne IP-Adressen verweisen (SSRF-Schutz)
  • Ihr Endpunkt muss innerhalb von 5 Sekunden mit einem 2xx-Status antworten

Ereignisse

EreignisBeschreibung
payment.completedZahlung wurde on-chain bestätigt und Ihrem Wallet gutgeschrieben
payment.failedZahlung fehlgeschlagen (Transaktion rückgängig gemacht oder abgelaufen)
refund.createdEine Rückerstattung wurde für eine Zahlung erstellt
refund.completedRückerstattung on-chain abgeschlossen und an den Kunden gesendet
refund.failedRückerstattung fehlgeschlagen (prüfen Sie failure_reason in data)

Payload

Jeder Webhook sendet einen JSON-Payload mit dem Ereignistyp, einem Zeitstempel und den Ereignisdaten:

Zahlungsereignisse

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" }
  }
}

Rückerstattungsereignisse

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

Zahlungsdatenfelder

data-Objekt (Zahlungsereignisse)

payment_idstringRequired

Fivo-Zahlungs-ID

amountstringRequired

Zahlungsbetrag (Dezimalzeichenkette)

currencystringRequired

Token-Symbol: USDC oder EURC

statusstringRequired

Zahlungsstatus: pending, completed oder failed

tx_hashstring | nullRequired

On-chain-Transaktions-Hash

from_addressstring | nullRequired

Wallet-Adresse des Zahlers

to_addressstring | nullRequired

Wallet-Adresse des Händlers

source_chainstring | nullRequired

Quell-Blockchain (z. B. ETH, BASE)

destination_chainstring | nullRequired

Ziel-Blockchain (null bei gleicher Chain)

is_cross_chainbooleanRequired

Ob die Zahlung kettenübergreifend transferiert wurde

merchant_transfer_tx_hashstringOptional

TX-Hash der Überweisung an das Händler-Wallet (nur kettenübergreifend)

customer_emailstring | nullOptional

Kunden-E-Mail, falls bei der Zahlung angegeben

referencestring | nullOptional

Händler-Referenz (z. B. Bestell-ID)

metadataobject | nullOptional

Benutzerdefinierte Metadaten der Zahlung

Anfrage-Header

Fivo fügt diese Header jeder Webhook-Anfrage hinzu:

Header

X-Fivo-SignaturestringRequired

HMAC-SHA256-Signatur mit sha256=-Präfix (siehe Signaturen verifizieren)

X-Fivo-EventstringRequired

Ereignistyp (z. B. payment.completed)

X-Fivo-TimestampstringRequired

Unix-Zeitstempel in Sekunden zum Zeitpunkt des Versands

X-Fivo-TeststringOptional

Auf „true“ gesetzt für Test-Webhooks, die über das Dashboard gesendet werden

Signaturen verifizieren

Verifizieren Sie immer den Header , um zu bestätigen, dass die Anfrage von Fivo stammt. Die Signatur wird berechnet als mit Ihrem Webhook-Secret als Schlüssel, versehen mit dem Präfix .X-Fivo-SignatureHMAC-SHA256(timestamp + "." + body)sha256=.

Ihr Webhook-Secret sieht so aus: 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

Timing-sichere Vergleiche

Verwenden Sie immer statt , um Timing-Angriffe zu verhindern.crypto.timingSafeEqual()===

Wiederholungen & automatische Deaktivierung

Wenn Ihr Endpunkt einen Nicht-2xx-Status zurückgibt oder ein Timeout auftritt, erhöht Fivo einen Fehlerzähler. Nach aufeinanderfolgenden Fehlern wird der Webhook automatisch deaktiviert, um weitere fehlgeschlagene Zustellungen zu vermeiden.10

Sie können einen deaktivierten Webhook jederzeit über das Dashboard wieder aktivieren. Der Fehlerzähler wird bei jeder erfolgreichen Zustellung zurückgesetzt.

Testen

Verwenden Sie die Schaltfläche auf Ihrer Seite , um ein Testereignis an Ihren Endpunkt zu senden. Test-Webhooks enthalten den Header , damit Ihr Server sie von echten Ereignissen unterscheiden kann.Test sendenWebhooksX-Fivo-Test: true

Zustellungsprotokolle sind auf der Webhook-Detailseite sichtbar: Sie können Payload, Antwortstatus und Antwortzeit jeder Zustellung einsehen.

Vollständiges Beispiel (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);