Vivoldi Webhook API & HMAC-Signaturprüfung
Eine sichere Webhook-Integration beginnt mit der Signaturprüfung über HTTP-Header.
Jede Vivoldi Webhook-Anfrage enthält Header wie X-Vivoldi-Request-Id, X-Vivoldi-Event-Id, X-Vivoldi-Signature.
Durch die Validierung dieser Header lassen sich manipulierte Anfragen erkennen und Link-, Coupon- sowie Stempel-Events sicher verarbeiten.
Diese Anleitung erklärt Schritt für Schritt die Funktion der einzelnen Header, den Ablauf der HMAC-Signaturprüfung sowie Implementierungsbeispiele für Java, PHP und Node.js.
HTTP Header
Vivoldi Webhooks senden HTTP-POST-Anfragen an die registrierte Callback-URL.
Jede Anfrage enthält spezielle Header mit Signaturen, Zeitstempeln und Event-IDs, sodass sich die Herkunft der Anfrage und die Integrität der Payload zuverlässig überprüfen lassen.
HTTP Header
X-Vivoldi-Request-Id: e2ea0405b7ba4f0b9b75797179731ae0
X-Vivoldi-Event-Id: 89365c75dae740ac8500dfc48c5014b5
X-Vivoldi-Webhook-Type: GLOBAL
X-Vivoldi-Resource-Type: URL
X-Vivoldi-Action-Type: NONE
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-Id string
- Eindeutige Anfrage-ID. Wird für jede Anfrage neu generiert und dient zur eindeutigen Identifizierung des Vorgangs.
- X-Vivoldi-Event-Id string
-
Eindeutige Ereignis-ID.
Wenn die erste Anfrage fehlschlägt und wiederholt wird, bleibt dieselbe Event-Id erhalten, um eine doppelte Verarbeitung desselben Ereignisses zu verhindern. - X-Vivoldi-Webhook-Type string
- Default:GLOBAL
-
Enum:
GLOBALGROUP
-
Wenn ein GROUP-Webhook aktiviert ist, wird dieser Wert auf GROUP gesetzt.
Stempel-Events verwenden immer GROUP, da sie pro Stempelkarte arbeiten.
Link- und Coupon-Events werden als GLOBAL gesendet, wenn kein Gruppen-Webhook konfiguriert ist. - X-Vivoldi-Resource-Type string
-
Enum:
URLCOUPONSTAMP
- URL: Kurzlink, COUPON: Coupon, STAMP: Stempel.
- X-Vivoldi-Action-Type string
-
Enum:
NONEADDREMOVEUSE
-
NONE: Wird für Link-Klick- oder Coupon-Nutzungsereignisse verwendet; keine zusätzliche Aktion.
ADD: Stempel hinzufügen
REMOVE: Stempel entfernen
USE: Stempelbelohnung einlösenWenn künftig neue Aktionen für Link- oder Coupon-Events hinzugefügt werden, kann dieser Header-Wert (
X-Vivoldi-Action-Type) erweitert werden. - X-Vivoldi-Comp-Idx integer
-
Organisations-ID (IDX).
Kann auf der Seite [Einstellungen → Organisationseinstellungen] eingesehen werden. - X-Vivoldi-Timestamp integer
- Zeitpunkt der Anfrage (UNIX-Epoch-Sekunden). Empfohlene Toleranz: ±5 Minuten.
- X-Content-SHA256 string
- SHA-256-Hashwert des Anfrage-Payloads.
- X-Vivoldi-Signature string
- Signaturinformationen der Anfrage. Format: t=Zeitstempel, v1=Signaturwert, alg=Algorithmus.
Webhook-Zustellung, Antworten & Retry-Richtlinien
Vivoldi Webhooks definieren klare Regeln für erfolgreiche Antworten, automatische Wiederholungsversuche und die Deaktivierung von Endpoints, um eine zuverlässige Event-Zustellung sicherzustellen.
Das Verständnis dieser Richtlinien hilft dabei, doppelte Verarbeitungen und den Verlust von Events zu vermeiden.
Erfolgskriterien
Dies definiert die Kriterien, anhand derer Vivoldi erkennt, ob Ihr Server eine Webhook-Anfrage erfolgreich empfangen hat.
- Die Zustellung gilt als erfolgreich, wenn der Server einen HTTP-Status 2xx (z. B. 200) zurückgibt.
- Geben Sie nach der Signaturprüfung sofort 200 OK zurück. Da das Webhook-Timeout 5 Sekunden beträgt, sollten lang laufende Prozesse asynchron nach dem Senden von
200 OKverarbeitet werden.
Wiederholung & Deaktivierung
Bei fehlgeschlagener Zustellung führt Vivoldi automatische Wiederholungsversuche durch und deaktiviert das Webhook bei wiederholten Fehlern, um unnötigen Traffic zu vermeiden.
- Automatische Wiederholungsversuche bis zu 5 Mal bei Netzwerkfehlern oder Nicht-2xx-Antworten.
- Das Webhook wird nach 5 aufeinanderfolgenden Fehlern automatisch deaktiviert, und der Administrator erhält eine Benachrichtigungs-E-Mail.
- Vermeidung doppelter Events: Verwenden Sie den Wert
X-Vivoldi-Event-Id, um doppelte Events zu erkennen.
Die Richtlinien können je nach Betriebsumgebung angepasst werden.
Ist die Verarbeitung von Webhooks ohne Header-Signaturprüfung sicher?
Technisch gesehen können Webhooks auch nur anhand des POST-Body (Payload) verarbeitet werden. In Produktionsumgebungen sollte jedoch immer eine Header-Validierung durchgeführt werden.
Wird die Header-Prüfung ausgelassen, kann dies zu schwerwiegenden Sicherheitsrisiken wie gefälschten Anfragen, manipulierten Payloads, doppelter Verarbeitung und fehlender Nachvollziehbarkeit führen.
Wichtige Risiken:
-
Gefälschte Anfragen (Spoofing): Angreifer können sich als Vivoldi-Server ausgeben und gefälschte Webhook-Anfragen senden.
Ohne Header-Validierung könnte das System diese irrtümlich als legitime Anfragen behandeln. - Datenmanipulation: Wird die Payload während der Übertragung verändert, kann dies ohne Signaturprüfung nicht erkannt werden.
- Doppelte Verarbeitung: Replay-Angriffe können dazu führen, dass dasselbe Event mehrfach empfangen wird, was doppelte Verarbeitung oder doppelte Gutschriften verursachen kann.
- Fehlende Nachvollziehbarkeit: Ohne Request-Id- oder Event-Id-Header werden Request-Tracking, Fehleranalyse und Problemreproduktion deutlich erschwert.
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
- linkId string
- Link-ID.
- domain string
- Link-Domain.
- redirectType integer
-
Enum:
200301302
- Weiterleitungstyp. Weitere Details finden Sie auf der Seite Wichtige Begriffe.
- url string
- Ursprüngliche URL.
- ttl string
- Link-Titel.
- description string
-
Legt den Wert des Meta-Tags description fest, wenn
redirectType200ist. - metaImg string
-
Legt den Wert des Meta-Tags image fest, wenn
redirectType200ist. - memo string
- Notiz zur Linkverwaltung.
- grpIdx integer
- Gruppen-IDX. Wenn eine Gruppe angegeben ist, wird das Gruppen-Webhook anstelle des globalen aufgerufen.
- grpNm string
- Gruppenname.
- strtYmdt datetime
- Startdatum und -zeit der Gültigkeit des Links.
- ednYmdt datetime
- Ablaufdatum und -zeit des Links.
- expireYn string
- Default:N
-
Enum:
YN
-
Wird als
Yübermittelt, wenn der Link abgelaufen ist. - expireUrl string
- URL, auf die nach Ablauf weitergeleitet wird.
- acesCnt integer
- Gesamtanzahl der Klicks.
- pernCnt integer
- Anzahl der eindeutigen Klicks (einzigartige Nutzer).
- acesMaxCnt integer
- Maximale Anzahl erlaubter Klicks. Der Zugriff wird blockiert, wenn diese überschritten wird.
- referer string
- URL der Seite, von der die Anfrage stammt.
- queryString string
- Query-String, der beim Zugriff auf die Kurz-URL enthalten ist.
- country string
- Ländercode des Nutzers (ISO-3166).
- language string
- Sprachcode des Nutzers (ISO-639).
- regYmdt datetime
- Erstellungsdatum und -zeit des Links.
- modYmdt datetime
- Änderungsdatum und -zeit des Links.
- payloadVersion string
- Payload-Version. Erhöht sich bei späteren Änderungen.
{
"cpnNo": "ZJLF0399WQBEQZJM",
"domain": "https://vvd.bz",
"nm": "$10 off cake coupon",
"grpIdx": 574,
"grpNm": "Event coupons",
"discTypeIdx": 457,
"discCurrency": "USD",
"formatDiscCurrency": "$10"
"disc": 10.0,
"strtYmd": "2025-01-01",
"endYmd": "2025-12-31",
"useLimit": 1,
"imgUrl": "https://file.vivoldi.com/coupon/2024/11/08/lmTFkqLQdCzeBuPdONKG.webp",
"onsiteYn": "Y",
"onsitePwd": "123456",
"memo": "$10 off cake with coupon at the venue",
"url": "",
"userId": "user08",
"userNm": "Emily",
"userPhnno": "202-555-0173",
"userEml": "test@gmail.com",
"userEtc1": "",
"userEtc2": "",
"useCnt": 0,
"regYmdt": "2025-08-31 18:10:22",
"payloadVersion": "v1"
}
Payload Parameters
- cpnNo string
- Coupon-Nummer.
- domain string
- Coupon-Domain.
- nm string
- Coupon-Name.
- grpIdx integer
- Gruppen-Index. Wenn eine Gruppe angegeben ist, wird das Gruppen-Webhook anstelle des globalen Webhooks ausgelöst.
- grpNm string
- Gruppenname.
- discTypeIdx integer
- Standard:457
-
Enum:
457458
- Rabatt-Typ. (457: Prozentualer Rabatt %, 458: Betrag-Rabatt)
- discCurrency string
- Standard:KRW
-
Enum:
KRWCADCNYEURGBPIDRJPYMURRUBSGDUSD
- Währungseinheit. Pflichtfeld, wenn Betrag-Rabatt (discTypeIdx:458) verwendet wird.
- formatDiscCurrency string
- Währungssymbol.
- disc double
- Standard:0
- Bei Prozent-Rabatt (457) 1–100%, bei Betrag-Rabatt (458) ein Geldbetrag.
- imgUrl string
- Coupon-Bild-URL.
- onsiteYn string
- Standard:N
-
Enum:
YN
-
Vor-Ort-Coupon. Legt fest, ob die Schaltfläche
„Coupon verwenden“auf der Coupon-Seite angezeigt wird.
Erforderlich, wenn Coupons im stationären Geschäft durch Mitarbeiter eingelöst werden. - onsitePwd string
-
Passwort für Vor-Ort-Coupon.
Wird benötigt, um den Coupon einzulösen. - memo string
- Interne Notizen.
- url string
-
Bei Eingabe einer URL wird auf der Coupon-Seite die Schaltfläche
„Zur Coupon-Nutzung“angezeigt.
Beim Klicken auf die Schaltfläche oder das Coupon-Bild erfolgt eine Weiterleitung zu dieser URL. - userId string
-
Wird zur Verwaltung der Coupon-Zielnutzer verwendet.
Wenn die Coupon-Nutzungsanzahl auf 2–5 gesetzt ist, muss dieses Feld angegeben werden.
Üblicherweise wird die Login-ID oder der englische Name des Website-Mitglieds eingetragen. - userNm string
- Name des Coupon-Nutzers (intern).
- userPhnno string
- Telefonnummer des Coupon-Nutzers (intern).
- userEml string
- E-Mail-Adresse des Coupon-Nutzers (intern).
- userEtc1 string
- Zusätzliches internes Feld.
- userEtc2 string
- Zusätzliches internes Feld.
- useCnt integer
- Anzahl der Coupon-Nutzungen.
- regYmdt datetime
- Erstellungsdatum des Coupons. Beispiel: 2025-07-21 11:50:20
{
"stampIdx": 16,
"domain": "https://vvd.bz",
"cardIdx": 1,
"cardNm": "Accumulate 10 Americanos",
"cardTtl": "Collect 10 stamps to get one free Americano.",
"stamps": 10,
"maxStamps": 12,
"stampUrl": "https://vvd.bz/stamp/274",
"url": "https://myshopping.com",
"strtYmd": "2025-01-01",
"endYmd": "2026-12-31",
"onsiteYn": "Y",
"onsitePwd": "123456",
"memo": null,
"activeYn": "Y",
"userId": "NKkDu9X4p4mQ",
"userNm": null,
"userPhnno": null,
"userEml": null,
"userEtc1": null,
"userEtc2": null,
"stampImgUrl": "https://cdn.vivoldi.com/www/image/icon/stamp/icon.stamp.1.webp",
"regYmdt": "2025-10-30 05:11:35",
"payloadVersion": "v1"
}
Payload Parameters
- stampIdx integer
- Stempel-IDX.
- domain string
- Stempel-Domain.
- cardIdx integer
- Karten-IDX.
- cardNm string
- Kartenname.
- cardTtl string
- Kartentitel.
- stamps integer
- Anzahl der bisher gesammelten Stempel.
- maxStamps integer
- Maximale Anzahl der Stempel auf der Karte.
- stampUrl string
- URL der Stempelseite.
- url string
- URL, zu der weitergeleitet wird, wenn auf der Stempelseite die Schaltfläche angeklickt wird.
- strtYmd date
- Beginn des Gültigkeitszeitraums des Stempels.
- endYmd date
- Ablaufdatum des Stempels.
- onsiteYn string
-
Enum:
YN
-
Gibt an, ob die Vor-Ort-Sammelfunktion aktiviert ist.
Wenn der WertYist, kann das Personal Stempel direkt im Geschäft hinzufügen. - onsitePwd string
-
Passwort für die Vor-Ort-Stempelfunktion.
Erforderlich bei der Verwendung der Stempel-Belohnungs-API, wenn die Vor-Ort-Option aktiviert ist (Y). - memo string
- Interne Notiz zu Referenzzwecken.
- activeYn string
-
Enum:
YN
-
Gibt an, ob der Stempel aktiv ist.
Wenn er deaktiviert ist, kann der Kunde den Stempel nicht verwenden. - userId string
-
Benutzer-ID. Wird verwendet, um den Empfänger des Stempels zu verwalten.
Normalerweise entspricht dies der Anmelde-ID des Website-Mitglieds.
Wenn kein Wert gesetzt ist, wird die Benutzer-ID automatisch vom System generiert. - userNm string
- Benutzername. Nur für die interne Verwaltung.
- userPhnno string
- Telefonnummer des Benutzers. Nur für die interne Verwaltung.
- userEml string
- E-Mail-Adresse des Benutzers. Nur für die interne Verwaltung.
- userEtc1 string
- Zusätzliches internes Verwaltungsfeld.
- userEtc2 string
- Zusätzliches internes Verwaltungsfeld.
- stampImgUrl string
- Bild-URL des Stempels.
- regYmdt datetime
- Erstellungsdatum des Stempels. Beispiel: 2025-07-21 11:50:20
Webhook-Signaturprüfung & Codebeispiele
Die Echtheit einer Webhook-Anfrage wird mithilfe des Headers X-Vivoldi-Signature und des ausgegebenen Secret Keys überprüft.
Die Signatur wird erzeugt, indem der Zeitstempel (t), die Event-ID (X-Vivoldi-Event-Id) und der SHA-256-Hash des Request-Bodys zu einer durch Punkte (.) getrennten Zeichenkette kombiniert und anschließend mit dem Secret Key per HMAC-SHA256 gehasht werden.
timestamp.eventId.payloadSha256
Wenn der erzeugte Hashwert (v1) mit dem Wert des Headers X-Vivoldi-Signature übereinstimmt, sollte die Anfrage als gültig verarbeitet werden.
Stimmen die Werte nicht überein, sollte die Anfrage sofort abgelehnt und im Log protokolliert werden.
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 final Logger log = LoggerFactory.getLogger(getClass());
@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 actionType = headers.get("x-vivoldi-action-type");
String signature = headers.get("x-vivoldi-signature");
// Signature Verification
if (!verifySignature(payload, signature, webhookType, resourceType, eventId)) {
return ResponseEntity.status(401).body("Invalid signature");
}
// Processing by Resource Type
switch (resourceType) {
case "URL":
handleLink(payload);
break;
case "COUPON":
handleCoupon(payload);
break;
case "STAMP":
handleStamp(payload, actionType);
break;
default:
log.warn("Unknown resourceType type: {}", resourceType);
}
return ResponseEntity.ok("success");
}
private String sha256(String data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) sb.append(String.format("%02x", b));
return sb.toString();
}
private boolean verifySignature(String payload, String signature, String webhookType, String resourceType, String eventId) {
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 || eventId == null) return false;
String payloadSha256 = null;
try {
payloadSha256 = sha256(payload);
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
String signedPayload = timestamp + "." + eventId + "." + payloadSha256;
String secretKey = webhookType.equals("GLOBAL") ? globalSecretKey : "";
if (secretKey.isEmpty()) {
JSONObject jsonObj = new JSONObject(payload);
if (resourceType.equals("STAMP")) {
long cardIdx = jsonObj.optLong("cardIdx", -1);
secretKey = loadStampCardSecretKey(cardIdx);
} else {
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 loadStampCardSecretKey(long cardIdx) {
switch (cardIdx) {
case 147: return "your-stamp-card-secret-key-147";
case 523: return "your-stamp-card-secret-key-523";
default: return "";
}
}
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);
}
private void handleStamp(String payload, String actionType) {
// Stamp Usage Event Handling Logic
if (actionType.equals("ADD")) {
log.info("Stamp added: {}", payload);
} else if (actionType.equals("RMEOVE")) {
log.info("Stamp removed: {}", payload);
} else if (actionType.equals("USE")) {
log.info("Stamp 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'] ?? '';
$actionType = $headers['x-vivoldi-action-type'] ?? '';
$signature = $headers['x-vivoldi-signature'] ?? '';
// Signature Verification
if (!verifySignature($payload, $signature, $webhookType, $resourceType, $eventId)) {
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;
case 'STAMP':
handleStamp($payload, $actionType);
break;
default:
error_log('Unknown resourceType: ' . $resourceType);
}
http_response_code(200);
echo json_encode(['status' => 'success']);
}
function sha256($data) {
return hash('sha256', $data);
}
/**
* HMAC-SHA256 Signature Verification Function
*/
function verifySignature($payload, $signature, $webhookType, $resourceType, $eventId) {
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 || !$eventId) return false;
// Timestamp Tolerance Verification (±60 seconds)
if (abs(time() - (int)$timestamp) > 60) {
return false;
}
// Payload SHA256
$payloadSha256 = sha256($payload);
$signedPayload = $timestamp . '.' . $eventId . '.' . $payloadSha256;
$secretKey = getSecretKey($webhookType, $resourceType, $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, $resourceType, $payload) {
global $globalSecretKey;
if ($webhookType === 'GLOBAL') {
return $globalSecretKey;
}
// Group-Specific Secret Key Configuration
$jsonData = json_decode($payload, true);
if ($resourceType === 'STAMP') {
if (!isset($jsonData['cardIdx'])) {
return '';
}
// Stamp cardIdx
$cardIdx = $jsonData['cardIdx'];
switch ($cardIdx) {
case 617:
return 'your stamp card secret key for 617';
case 3304:
return 'your stamp card secret key for 3304';
default:
return '';
}
} else {
if (!isset($jsonData['grpIdx'])) {
return '';
}
$grpIdx = $jsonData['grpIdx'];
if ($resourceType === 'LINK') {
// Link grpIdx
switch ($grpIdx) {
case 17584:
return 'your group secret key for 17584';
case 9158:
return 'your group secret key for 9158';
default:
return '';
}
} else {
// Coupon 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}");
}
}
/**
* Stamp Event Handling Function
*/
function handleStamp($payload, $actionType) {
error_log('Stamp payload: ' . $payload);
// Parsing JSON to process coupon information
$stampData = json_decode($payload, true);
if ($stampData) {
$stampIdx = $stampData['stampIdx'] ?? 0;
switch ($actionType) {
case "ADD":
// Stamp added
break;
case "REMOVE":
// Stamp removed
break;
case "USE":
// Stamp benefit used
break;
default:
return '';
}
}
}
/**
* 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 actionType = headers['x-vivoldi-action-type'] || '';
const signature = headers['x-vivoldi-signature'] || '';
// Signature Verification
if (!verifySignature(payload, signature, webhookType, resourceType, eventId)) {
res.status(401).json({ error: 'Invalid signature' });
return;
}
// Processing by Resource Type
switch (resourceType) {
case 'URL':
handleLink(payload);
break;
case 'COUPON':
handleCoupon(payload);
break;
case 'STAMP':
handleStamp(payload);
break;
default:
console.error('Unknown resourceType: ' + resourceType);
}
res.status(200).json({ status: 'success' });
}
/**
* SHA256(hex)
*/
function sha256Hex(data) {
return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
}
/**
* HMAC-SHA256 Signature Verification Function
*/
function verifySignature(payload, signature, webhookType, resourceType, eventId) {
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 || !eventId) return false;
// Timestamp check (±180s)
if (Math.abs(Date.now()/1000 - Number(timestamp)) > 180) return false;
const signedPayload = `${timestamp}.${eventId}.${sha256Hex(payload)}`;
// Secret Key Determination
const secretKey = getSecretKey(webhookType, resourceType, 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, resourceType, payload) {
if (webhookType === 'GLOBAL') {
return globalSecretKey;
}
// Group-Specific Secret Key Configuration
let jsonData;
try {
jsonData = JSON.parse(payload);
} catch (error) {
return '';
}
if (resourceType === 'STAMP') {
if (!jsonData.cardIdx) {
return '';
}
const cardIdx = jsonData.cardIdx;
switch (cardIdx) {
case 3570:
return 'your stamp card secret key for 3570';
case 4178:
return 'your stamp card secret key for 4178';
default:
return '';
}
} else {
if (!jsonData.grpIdx) {
return '';
}
const grpIdx = jsonData.grpIdx;
if (resourceType === 'LINK') {
// Link grpIdx
switch (grpIdx) {
case 17584:
return 'your group secret key for 17584';
case 9158:
return 'your group secret key for 9158';
default:
return '';
}
} else {
// Coupon grpIdx
switch (grpIdx) {
case 6350:
return 'your group secret key for 6350';
case 17884:
return 'your group secret key for 17884';
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}`);
}
}
/**
* Stamp Event Handling Function
*/
function handleStamp(payload, actionType) {
console.error('Stamp payload: ' + payload);
// Parsing JSON to process coupon information
let stampData;
try {
stampData = JSON.parse(payload);
} catch (error) {
return;
}
if (stampData) {
const stampIdx = stampData.stampIdx || 0;
switch (actionType) {
case "ADD":
// Stamp added
break;
case "REMOVE":
// Stamp removed
break;
case "USE":
// Stamp benefit used
break;
}
}
}
/**
* 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'], headers['x-vivoldi-event-id'])) {
return res.status(401).json({ error: 'Invalid signature' });
}
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
Optimiert für Enterprise-Umgebungen, die große Mengen an Link-, Coupon- und Stempel-Events verarbeiten.
Basierend auf hochverfügbarer Infrastruktur und zuverlässigen Queueing-Systemen ermöglicht Vivoldi stabile Integrationen mit CRM-, Zahlungs- und Analyseplattformen ohne Eventverlust — selbst bei plötzlichen Traffic-Spitzen.