Webhook — Integrationshandbuch
Der Kern der Integration von Vivoldi Webhooks ist die Überprüfung der HTTP-Header.
Jede Anfrage enthält X-Vivoldi-Request-Id
, X-Vivoldi-Event-Id
, X-Vivoldi-Signature
und weitere. Durch die Validierung dieser Werte können Ereignisse sicher verarbeitet werden.
Dieses Handbuch bietet Erklärungen zu den Header-Feldern sowie Beispielcode, damit Sie die Webhook-Integration Schritt für Schritt schnell umsetzen können.
HTTP Header
Webhooks senden POST-Anfragen an die angegebene Callback-URL, und die Integrität sowie Zuverlässigkeit der Anfrage können über die folgenden Header überprüft werden.
HTTP Header
X-Vivoldi-Request-Id: e2ea0405b7ba4f0b9b75797179731ae0
X-Vivoldi-Event-Id: 89365c75dae740ac8500dfc48c5014b5
X-Vivoldi-Webhook-Type: GLOBAL
X-Vivoldi-Resource-Type: URL
X-Vivoldi-Comp-Idx: 50742
X-Vivoldi-Timestamp: 1758184391752
X-Content-SHA256: e040abf9ac2826bc108fce0117e49290086743733ad9db2fa379602b4db9792c
X-Vivoldi-Signature: t=1758184391752,v1=b610f699d4e7964cdb7612111f5765576920b680e7c33c649e20608406807aaf,alg=hmac-sha256
Request Parameters
- X-Vivoldi-Request-Idstring
- Eindeutige ID pro Anfrage. Für jede Anfrage wird ein neuer Wert vergeben.
- X-Vivoldi-Event-Idstring
- Eindeutige Ereignis-ID. Bleibt bei der ersten Anfrage und allen Wiederholungen gleich.
- X-Vivoldi-Webhook-Typestring
- Default:GLOBAL
- Enum:GLOBALGROUP
- Wird auf GROUP gesetzt, wenn ein Gruppen-Webhook aktiviert ist.
- X-Vivoldi-Resource-Typestring
- Enum:URLCOUPON
- URL: Kurzlink, COUPON: Coupon
- X-Vivoldi-Comp-Idxinteger
- Eindeutige Organisations-ID.
- X-Vivoldi-Timestampinteger
- Zeitpunkt der Anfrage (UNIX-Epoch-Sekunden). Empfohlene Toleranz: ±1 Minute.
- X-Content-SHA256string
- SHA-256-Hashwert des Anfrage-Payloads.
- X-Vivoldi-Signaturestring
- Signaturinformationen der Anfrage. t=Zeitstempel, v1=Signaturwert, alg=Algorithmus.
Übertragungs- · Antwort- · Wiederholungsrichtlinie
Erfolgskriterien
- Als erfolgreich gilt es, wenn der empfangende Server eine HTTP-2xx-Antwort (z. B. 200) zurückgibt.
- Nach der Signaturprüfung geben Sie sofort 200 OK zurück. Da das Timeout 5 Sekunden beträgt, sollten lange Prozesse asynchron nach der Antwort ausgeführt werden.
Wiederholung & Deaktivierung
- Bis zu 5 Wiederholungen bei Netzwerkfehlern oder Nicht-2xx-Antworten.
- Nach 5 aufeinanderfolgenden Fehlern wird das Webhook automatisch deaktiviert und eine Benachrichtigungs-E-Mail an den Administrator gesendet.
- Vermeidung von Duplikaten: Überprüfen Sie Duplikate anhand des
X-Vivoldi-Event-Id
-Werts.
Richtlinien können je nach Betriebsumgebung angepasst werden.
Kann man Webhooks ohne Header-Validierung verarbeiten?
Technisch funktioniert es möglicherweise, wenn nur der POST-Body geparst wird, aber in Produktionsumgebungen wird dies keinesfalls empfohlen. Das Überspringen der Header-Validierung bringt folgende schwerwiegende Risiken mit sich:
Wichtige Risiken:
- Gefälschte Anfragen (Spoofing): Ein Angreifer könnte gefälschte Anfragen senden, die vorgeben, von Vivoldi zu stammen, und das System könnte diese verarbeiten.
- Datenmanipulation: Wird der Payload während der Übertragung verändert, kann dies ohne Signaturprüfung nicht erkannt werden.
- Doppelte Verarbeitung: Replay-Angriffe können dasselbe Ereignis mehrfach senden und doppelte Verarbeitungen auslösen.
- Keine Nachverfolgbarkeit: Ohne Header wie Request/Event ID sind Nachverfolgung, Untersuchung und Reproduktion von Problemen sehr schwierig.
Payload
{
"linkId": "202509-event",
"domain": "https://event.com",
"compIdx": 50142,
"redirectType": 200,
"url": "https://my-event.com/books/event/202509",
"ttl": "September 2025 Event",
"description": "The 2025 National Book Festival will be held in the nation's capital at the Walter E.",
"metaImg": "https://my-event.com/storage-services/media/webcasts/2025/2509_thumbnail_00145901.jpg",
"memo": "",
"grpIdx": 0,
"grpNm": "",
"strtYmdt": "2025-09-01 00:00:00",
"endYmdt": "2025-09-30 23:59:59",
"expireYn": "Y",
"expireUrl": "https://my-event.com/books/event/closed",
"acesCnt": 17502,
"pernCnt": 16491,
"acesMaxCnt": 20000,
"referer": "https://www.google.com",
"queryString": "",
"country": "US",
"language": "en",
"regYmdt": "2025-08-31 18:10:22",
"modYmdt": "2025-08-31 18:10:22",
"payloadVersion": "v1"
}
Payload Parameters
- linkIdstring
- Link-ID.
- domainstring
- Link-Domain.
- redirectTypeinteger
- Enum:200301302
- Weiterleitungstyp. Weitere Details finden Sie auf der Seite Wichtige Begriffe.
- urlstring
- Ursprüngliche URL.
- ttlstring
- Link-Titel.
- descriptionstring
- Legt den Wert des Meta-Tags description fest, wenn
redirectType
200
ist. - metaImgstring
- Legt den Wert des Meta-Tags image fest, wenn
redirectType
200
ist. - memostring
- Notiz zur Linkverwaltung.
- grpIdxinteger
- Gruppen-IDX. Wenn eine Gruppe angegeben ist, wird das Gruppen-Webhook anstelle des globalen aufgerufen.
- grpNmstring
- Gruppenname.
- strtYmdtdatetime
- Startdatum und -zeit der Gültigkeit des Links.
- ednYmdtdatetime
- Ablaufdatum und -zeit des Links.
- expireYnstring
- Default:N
- Enum:YN
- Wird als
Y
übermittelt, wenn der Link abgelaufen ist. - expireUrlstring
- URL, auf die nach Ablauf weitergeleitet wird.
- acesCntinteger
- Gesamtanzahl der Klicks.
- pernCntinteger
- Anzahl der eindeutigen Klicks (einzigartige Nutzer).
- acesMaxCntinteger
- Maximale Anzahl erlaubter Klicks. Der Zugriff wird blockiert, wenn diese überschritten wird.
- refererstring
- URL der Seite, von der die Anfrage stammt.
- queryStringstring
- Query-String, der beim Zugriff auf die Kurz-URL enthalten ist.
- countrystring
- Ländercode des Nutzers (ISO-3166).
- languagestring
- Sprachcode des Nutzers (ISO-639).
- regYmdtdatetime
- Erstellungsdatum und -zeit des Links.
- modYmdtdatetime
- Änderungsdatum und -zeit des Links.
- payloadVersionstring
- Payload-Version. Erhöht sich bei späteren Änderungen.
Coupon Webhook will be available soon.
Signaturprüfung — Codebeispiel
Webhook-Anfragen müssen mit dem Header X-Vivoldi-Signature
und dem ausgegebenen Webhook-Secret Key überprüft werden.
Die Signatur wird mit HMAC-SHA256 basierend auf dem Zeitstempel + dem Anfrage-Body berechnet und gilt nur dann als gültig, wenn sie mit dem Header-Wert übereinstimmt.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Controller;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Map;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private static final Logger log = LoggerFactory.getLogger(WebhookController.class);
@Value("${vivoldi.webhook.secret}")
private String globalSecretKey; // global secret key
@PostMapping("/vivoldi")
public ResponseEntity<String> handleWebhook(
@RequestBody String payload,
@RequestHeader Map<String, String> headers) {
// Extracting the Vivoldi header
String requestId = headers.get("x-vivoldi-request-id");
String eventId = headers.get("x-vivoldi-event-id");
String webhookType = headers.get("x-vivoldi-webhook-type");
String resourceType = headers.get("x-vivoldi-resource-type");
String signature = headers.get("x-vivoldi-signature");
// Signature Verification
if (!verifySignature(payload, signature, webhookType)) {
return ResponseEntity.status(401).body("Invalid signature");
}
// Processing by Resource Type
switch (resourceType) {
case "URL":
handleLink(payload);
break;
case "COUPON":
handleCoupon(payload);
break;
default:
log.warn("Unknown resourceType type: {}", resourceType);
}
return ResponseEntity.ok();
}
private boolean verifySignature(String payload, String signature, String webhookType) {
try {
String timestamp = null;
String sig = null;
for (String part : signature.split(",")) {
part = part.trim();
if (part.startsWith("t=")) timestamp = part.substring(2);
if (part.startsWith("v1=")) sig = part.substring(3);
}
if (timestamp == null || sig == null) return false;
String signedPayload = timestamp + "." + payload;
String secretKey = webhookType.equals("GLOBAL") ? globalSecretKey : "";
if (secretKey.isEmpty()) {
JSONObject jsonObj = new JSONObject(payload);
int grpIdx = jsonObj.optInt("grpIdx", -1);
secretKey = loadGroupSecretKey(grpIdx); // In actual production environments, database integration
}
if (secretKey == null || secretKey.isEmpty()) return false;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
String computedSig = Hex.encodeHexString(hash);
return MessageDigest.isEqual(
sig.toLowerCase().getBytes(StandardCharsets.UTF_8),
computedSig.toLowerCase().getBytes(StandardCharsets.UTF_8)
);
} catch (Exception e) {
log.error("Signature verification failed", e);
return false;
}
}
private String loadGroupSecretKey(int grpIdx) {
switch (grpIdx) {
case 3570: return "your-group-secret-key-3570";
case 4178: return "your-group-secret-key-4178";
default: return "";
}
}
private void handleLink(String payload) {
// Link Click Event Handling Logic
log.info("Link clicked: {}", payload);
}
private void handleCoupon(String payload) {
// Coupon Usage Event Handling Logic
log.info("Coupon redeemed: {}", payload);
}
}
<?php
// Environment Settings
$globalSecretKey = $_ENV['VIVOLDI_WEBHOOK_SECRET'] ?? 'your-global-secret-key';
/**
* Main Webhook Handler Function
*/
function handleWebhook($payload) {
// Header Information Extraction
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$requestId = $headers['x-vivoldi-request-id'] ?? '';
$eventId = $headers['x-vivoldi-event-id'] ?? '';
$webhookType = $headers['x-vivoldi-webhook-type'] ?? '';
$resourceType = $headers['x-vivoldi-resource-type'] ?? '';
$signature = $headers['x-vivoldi-signature'] ?? '';
// Signature Verification
if (!verifySignature($payload, $signature, $webhookType)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
return;
}
// Processing by Resource Type
switch ($resourceType) {
case 'URL':
handleLink($payload);
break;
case 'COUPON':
handleCoupon($payload);
break;
default:
error_log('Unknown resourceType: ' . $resourceType);
}
http_response_code(200);
echo json_encode(['status' => 'success']);
}
/**
* HMAC-SHA256 Signature Verification Function
*/
function verifySignature($payload, $signature, $webhookType) {
try {
$timestamp = null;
$sig = null;
foreach (explode(',', $signature) as $part) {
$part = trim($part);
if (strpos($part, 't=') === 0) $timestamp = substr($part, 2);
if (strpos($part, 'v1=') === 0) $sig = substr($part, 3);
}
if (!$timestamp || !$sig) return false;
// Timestamp Tolerance Verification (±60 seconds)
if (abs(time() - (int)$timestamp) > 60) {
return false;
}
$signedPayload = $timestamp . '.' . $payload;
$secretKey = getSecretKey($webhookType, $payload);
if (empty($secretKey)) return false;
$computedSig = hash_hmac('sha256', $signedPayload, $secretKey);
// Safety Comparison (lowercase throughout)
return hash_equals(strtolower($sig), strtolower($computedSig));
} catch (Exception $e) {
error_log('Signature verification failed: ' . $e->getMessage());
return false;
}
}
/**
* Secret Key Return Based on Webhook Type and Group
*/
function getSecretKey($webhookType, $payload) {
global $globalSecretKey;
if ($webhookType === 'GLOBAL') {
return $globalSecretKey;
}
// Group-Specific Secret Key Configuration
$jsonData = json_decode($payload, true);
if (!isset($jsonData['grpIdx'])) {
return '';
}
$grpIdx = $jsonData['grpIdx'];
switch ($grpIdx) {
case 3570:
return 'your group secret key for 3570';
case 4178:
return 'your group secret key for 4178';
default:
return '';
}
}
/**
* Link Event Handler Function
*/
function handleLink($payload) {
error_log('Link clicked: ' . $payload);
// Processing link information by parsing JSON
$linkData = json_decode($payload, true);
if ($linkData) {
// Link Click Statistics Update
$linkId = $linkData['linkId'] ?? '';
$clickTime = $linkData['timestamp'] ?? time();
$userAgent = $linkData['userAgent'] ?? '';
// Storing click information in the database
saveClickEvent($linkId, $clickTime, $userAgent);
error_log("Link {$linkId} clicked at {$clickTime}");
}
}
/**
* Coupon Event Handling Function
*/
function handleCoupon($payload) {
error_log('Coupon redeemed: ' . $payload);
// Parsing JSON to process coupon information
$couponData = json_decode($payload, true);
if ($couponData) {
// Coupon Usage Information Processing
$couponCode = $couponData['couponCode'] ?? '';
$redeemTime = $couponData['timestamp'] ?? time();
$userId = $couponData['userId'] ?? '';
// Storing coupon usage information in the database
saveCouponRedemption($couponCode, $userId, $redeemTime);
error_log("Coupon {$couponCode} redeemed by user {$userId}");
}
}
/**
* Store click events in the database
*/
function saveClickEvent($linkId, $clickTime, $userAgent) {
// Implementation of actual database integration logic
// Example: Stored in MySQL, PostgreSQL, etc.
error_log("Saving click event - Link: {$linkId}, Time: {$clickTime}");
}
/**
* Store coupon usage information in the database
*/
function saveCouponRedemption($couponCode, $userId, $redeemTime) {
// Implementation of actual database integration logic
// Example: Updating coupon status, storing usage history, etc.
error_log("Saving coupon redemption - Code: {$couponCode}, User: {$userId}");
}
/**
* Log recording function
*/
function logWebhookEvent($eventType, $data) {
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] {$eventType}: " . json_encode($data);
error_log($logMessage);
}
// ===========================================
// Webhook Endpoint Execution Unit
// ===========================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$payload = file_get_contents('php://input');
handleWebhook($payload);
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
const express = require('express');
const crypto = require('crypto');
const app = express();
// Environment Settings
const globalSecretKey = process.env.VIVOLDI_WEBHOOK_SECRET || 'your-global-secret-key';
// Form data parser for webhook payloads
app.use(express.raw({ type: '*/*' }));
/**
* Main Webhook Handler Function
*/
function handleWebhook(headers, res, payload) {
const requestId = headers['x-vivoldi-request-id'] || '';
const eventId = headers['x-vivoldi-event-id'] || '';
const webhookType = headers['x-vivoldi-webhook-type'] || '';
const resourceType = headers['x-vivoldi-resource-type'] || '';
const signature = headers['x-vivoldi-signature'] || '';
// Signature Verification
if (!verifySignature(payload, signature, webhookType)) {
res.status(401).json({ error: 'Invalid signature' });
return;
}
// Processing by Resource Type
switch (resourceType) {
case 'URL':
handleLink(payload);
break;
case 'COUPON':
handleCoupon(payload);
break;
default:
console.error('Unknown resourceType: ' + resourceType);
}
res.status(200).json({ status: 'success' });
}
/**
* HMAC-SHA256 Signature Verification Function
*/
function verifySignature(payload, signature, webhookType) {
try {
let timestamp, sig;
for (const part of signature.split(',')) {
const p = part.trim();
if (p.startsWith('t=')) timestamp = p.slice(2);
if (p.startsWith('v1=')) sig = p.slice(3);
}
if (!timestamp || !sig) return false;
// Timestamp check (±60s)
if (Math.abs(Date.now()/1000 - Number(timestamp)) > 60) return false;
const signedPayload = `${timestamp}.${payload}`;
// Secret Key Determination
const secretKey = getSecretKey(webhookType, payload);
if (!secretKey) return false;
// HMAC-SHA256 Signature Calculation
const computedSig = crypto
.createHmac('sha256', secretKey)
.update(signedPayload)
.digest('hex');
// Timing-Safe Comparison
return crypto.timingSafeEqual(
Buffer.from(sig.toLowerCase(), 'hex'),
Buffer.from(computedSig.toLowerCase(), 'hex')
);
} catch (e) {
console.error('Signature verification failed: ' + e.message);
return false;
}
}
/**
* Secret Key Return Based on Webhook Type and Group
*/
function getSecretKey(webhookType, payload) {
if (webhookType === 'GLOBAL') {
return globalSecretKey;
}
// Group-Specific Secret Key Configuration
let jsonData;
try {
jsonData = JSON.parse(payload);
} catch (error) {
return '';
}
if (!jsonData.grpIdx) {
return '';
}
const grpIdx = jsonData.grpIdx;
switch (grpIdx) {
case 3570:
return 'your group secret key for 3570';
case 4178:
return 'your group secret key for 4178';
default:
return '';
}
}
/**
* Link Event Handler Function
*/
function handleLink(payload) {
console.error('Link clicked: ' + payload);
// Processing link information by parsing JSON
let linkData;
try {
linkData = JSON.parse(payload);
} catch (error) {
return;
}
if (linkData) {
// Link Click Statistics Update
const linkId = linkData.linkId || '';
const clickTime = linkData.timestamp || Math.floor(Date.now() / 1000);
const userAgent = linkData.userAgent || '';
// Storing click information in the database
saveClickEvent(linkId, clickTime, userAgent);
console.error(`Link ${linkId} clicked at ${clickTime}`);
}
}
/**
* Coupon Event Handling Function
*/
function handleCoupon(payload) {
console.error('Coupon redeemed: ' + payload);
// Parsing JSON to process coupon information
let couponData;
try {
couponData = JSON.parse(payload);
} catch (error) {
return;
}
if (couponData) {
// Coupon Usage Information Processing
const couponCode = couponData.couponCode || '';
const redeemTime = couponData.timestamp || Math.floor(Date.now() / 1000);
const userId = couponData.userId || '';
// Storing coupon usage information in the database
saveCouponRedemption(couponCode, userId, redeemTime);
console.error(`Coupon ${couponCode} redeemed by user ${userId}`);
}
}
/**
* Store click events in the database
*/
function saveClickEvent(linkId, clickTime, userAgent) {
// Implementation of actual database integration logic
// Example: Stored in MongoDB, MySQL, PostgreSQL, etc.
console.error(`Saving click event - Link: ${linkId}, Time: ${clickTime}`);
}
/**
* Store coupon usage information in the database
*/
function saveCouponRedemption(couponCode, userId, redeemTime) {
// Implementation of actual database integration logic
// Example: Updating coupon status, storing usage history, etc.
console.error(`Saving coupon redemption - Code: ${couponCode}, User: ${userId}`);
}
/**
* Log recording function
*/
function logWebhookEvent(eventType, data) {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
const logMessage = `[${timestamp}] ${eventType}: ${JSON.stringify(data)}`;
console.error(logMessage);
}
// ===========================================
// Webhook Endpoint Execution Unit
// ===========================================
app.post('/webhook/vivoldi', (req, res) => {
const payload = req.body.toString('utf8');
const headers = req.headers;
if (!verifySignature(payload, headers['x-vivoldi-signature'], headers['x-vivoldi-webhook-type'])) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = req.body.toString('utf8');
handleWebhook(req.headers, res, payload);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook server running on port ${PORT}`);
});
✨ Echtzeit-Integration auf Enterprise-Niveau
Webhooks verbinden Echtzeitdaten mit Ihren CRM-, Zahlungs- und Analysesystemen.
Hohe Verfügbarkeit, leistungsstarkes Queuing und Wiederholungen sowie erweiterte Sicherheitsfunktionen sind im Enterprise-Tarif verfügbar.