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
- Klicken Sie auf Webhook hinzufügen
- Geben Sie Ihre Endpunkt-URL ein (muss HTTPS sein)
- Wählen Sie die Ereignisse aus, die Sie abonnieren möchten
- 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
| Ereignis | Beschreibung |
|---|---|
payment.completed | Zahlung wurde on-chain bestätigt und Ihrem Wallet gutgeschrieben |
payment.failed | Zahlung fehlgeschlagen (Transaktion rückgängig gemacht oder abgelaufen) |
refund.created | Eine Rückerstattung wurde für eine Zahlung erstellt |
refund.completed | Rückerstattung on-chain abgeschlossen und an den Kunden gesendet |
refund.failed | Rü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
{
"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
{
"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_idstringRequiredFivo-Zahlungs-ID
amountstringRequiredZahlungsbetrag (Dezimalzeichenkette)
currencystringRequiredToken-Symbol: USDC oder EURC
statusstringRequiredZahlungsstatus: pending, completed oder failed
tx_hashstring | nullRequiredOn-chain-Transaktions-Hash
from_addressstring | nullRequiredWallet-Adresse des Zahlers
to_addressstring | nullRequiredWallet-Adresse des Händlers
source_chainstring | nullRequiredQuell-Blockchain (z. B. ETH, BASE)
destination_chainstring | nullRequiredZiel-Blockchain (null bei gleicher Chain)
is_cross_chainbooleanRequiredOb die Zahlung kettenübergreifend transferiert wurde
merchant_transfer_tx_hashstringOptionalTX-Hash der Überweisung an das Händler-Wallet (nur kettenübergreifend)
customer_emailstring | nullOptionalKunden-E-Mail, falls bei der Zahlung angegeben
referencestring | nullOptionalHändler-Referenz (z. B. Bestell-ID)
metadataobject | nullOptionalBenutzerdefinierte Metadaten der Zahlung
| Name | Type | Required | Description |
|---|---|---|---|
payment_id | string | Required | Fivo-Zahlungs-ID |
amount | string | Required | Zahlungsbetrag (Dezimalzeichenkette) |
currency | string | Required | Token-Symbol: USDC oder EURC |
status | string | Required | Zahlungsstatus: pending, completed oder failed |
tx_hash | string | null | Required | On-chain-Transaktions-Hash |
from_address | string | null | Required | Wallet-Adresse des Zahlers |
to_address | string | null | Required | Wallet-Adresse des Händlers |
source_chain | string | null | Required | Quell-Blockchain (z. B. ETH, BASE) |
destination_chain | string | null | Required | Ziel-Blockchain (null bei gleicher Chain) |
is_cross_chain | boolean | Required | Ob die Zahlung kettenübergreifend transferiert wurde |
merchant_transfer_tx_hash | string | Optional | TX-Hash der Überweisung an das Händler-Wallet (nur kettenübergreifend) |
customer_email | string | null | Optional | Kunden-E-Mail, falls bei der Zahlung angegeben |
reference | string | null | Optional | Händler-Referenz (z. B. Bestell-ID) |
metadata | object | null | Optional | Benutzerdefinierte Metadaten der Zahlung |
Anfrage-Header
Fivo fügt diese Header jeder Webhook-Anfrage hinzu:
Header
X-Fivo-SignaturestringRequiredHMAC-SHA256-Signatur mit sha256=-Präfix (siehe Signaturen verifizieren)
X-Fivo-EventstringRequiredEreignistyp (z. B. payment.completed)
X-Fivo-TimestampstringRequiredUnix-Zeitstempel in Sekunden zum Zeitpunkt des Versands
X-Fivo-TeststringOptionalAuf „true“ gesetzt für Test-Webhooks, die über das Dashboard gesendet werden
| Name | Type | Required | Description |
|---|---|---|---|
X-Fivo-Signature | string | Required | HMAC-SHA256-Signatur mit sha256=-Präfix (siehe Signaturen verifizieren) |
X-Fivo-Event | string | Required | Ereignistyp (z. B. payment.completed) |
X-Fivo-Timestamp | string | Required | Unix-Zeitstempel in Sekunden zum Zeitpunkt des Versands |
X-Fivo-Test | string | Optional | 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...
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);
}Timing-sichere Vergleiche
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)
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);