Webhook API & Vérification HMAC Vivoldi
Une intégration Webhook sécurisée repose avant tout sur la vérification des signatures via les en-têtes HTTP.
Chaque requête Webhook Vivoldi inclut des en-têtes tels que X-Vivoldi-Request-Id, X-Vivoldi-Event-Id, X-Vivoldi-Signature.
La validation de ces en-têtes permet de bloquer les requêtes falsifiées et de traiter en toute sécurité les événements liés aux liens, coupons et stamps.
Ce guide présente le rôle de chaque en-tête, le processus de validation HMAC ainsi que des exemples d’implémentation en Java, PHP et Node.js.
HTTP Header
Les Webhooks Vivoldi envoient des requêtes HTTP POST vers l’URL de callback enregistrée.
Chaque requête inclut des en-têtes dédiés contenant une signature, un horodatage et des identifiants d’événement afin de vérifier l’origine de la requête et l’intégrité du payload.
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
- ID unique de la requête. Généré à chaque demande et utilisé pour identifier une transaction spécifique.
- X-Vivoldi-Event-Id string
-
ID unique de l’événement.
Si la première requête échoue et est retentée, le même Event-Id est conservé afin d’éviter le traitement en double du même événement. - X-Vivoldi-Webhook-Type string
- Default :GLOBAL
-
Enum :
GLOBALGROUP
-
Si un Webhook de type GROUP est activé, cette valeur est définie sur GROUP.
Les événements de tampon utilisent toujours GROUP car ils fonctionnent par carte de tampons.
Les événements de lien et de coupon sont envoyés en mode GLOBAL lorsqu’aucun Webhook de groupe n’est configuré. - X-Vivoldi-Resource-Type string
-
Enum :
URLCOUPONSTAMP
- URL : lien court, COUPON : coupon, STAMP : tampon.
- X-Vivoldi-Action-Type string
-
Enum :
NONEADDREMOVEUSE
-
NONE : utilisé pour les événements de clic sur un lien ou d’utilisation d’un coupon, sans action supplémentaire.
ADD : ajout d’un tampon
REMOVE : suppression d’un tampon
USE : utilisation d’une récompense de tamponSi de nouvelles actions sont ajoutées aux événements de lien ou de coupon à l’avenir, cette valeur d’en-tête (
X-Vivoldi-Action-Type) pourra être étendue. - X-Vivoldi-Comp-Idx integer
-
IDX unique de l’organisation.
Consultable dans la page [Paramètres → Paramètres de l’organisation]. - X-Vivoldi-Timestamp integer
- Heure de la requête (secondes UNIX epoch). Tolérance recommandée : ±5 minutes.
- X-Content-SHA256 string
- Valeur de hachage SHA-256 du corps (payload) de la requête.
- X-Vivoldi-Signature string
- Informations de signature de la requête. Format : t=horodatage, v1=valeur de signature, alg=algorithme.
Livraison des Webhooks, Réponses & Politique de Retry
Les Webhooks Vivoldi définissent clairement les conditions de réponse réussie, les retries automatiques et les règles de désactivation des endpoints afin de garantir une livraison fiable des événements.
La compréhension de ces politiques permet d’éviter les traitements en double et de réduire le risque de perte d’événements.
Critères de réussite
Ces critères permettent à Vivoldi de déterminer si votre serveur a correctement reçu une requête Webhook.
- La livraison est considérée comme réussie lorsque le serveur renvoie une réponse HTTP 2xx (par exemple 200).
- Après la vérification de la signature, retournez immédiatement 200 OK. Le délai d’expiration du Webhook étant de 5 secondes, les traitements longs doivent être exécutés de manière asynchrone après l’envoi de
200 OK.
Réessais et désactivation
En cas d’échec de livraison, Vivoldi effectue automatiquement des retries et peut désactiver le Webhook après plusieurs échecs afin d’éviter un trafic inutile.
- Jusqu’à 5 retries automatiques sont effectués en cas d’erreur réseau ou de réponse non 2xx.
- Le Webhook est automatiquement désactivé après 5 échecs consécutifs, et un e-mail de notification est envoyé à l’administrateur.
- Prévention des événements en double : utilisez la valeur
X-Vivoldi-Event-Idpour détecter les événements déjà reçus.
Les politiques peuvent être ajustées selon l’environnement d’exploitation.
Est-il sûr de traiter les Webhooks sans vérification de signature des en-têtes ?
Techniquement, il est possible de traiter les Webhooks uniquement à partir du corps POST (Payload). Cependant, dans un environnement de production, la vérification des en-têtes doit toujours être appliquée.
Ignorer cette validation peut exposer le système à des risques de sécurité majeurs, notamment des requêtes falsifiées, des modifications du payload, des traitements en double et une perte de traçabilité.
Principaux risques :
-
Requêtes falsifiées (Spoofing) : Un attaquant peut usurper l’identité des serveurs Vivoldi et envoyer de fausses requêtes Webhook.
Sans vérification des en-têtes, le système peut considérer ces requêtes à tort comme légitimes. - Altération des données : Si le payload est modifié pendant la transmission réseau, la modification ne pourra pas être détectée sans validation de signature.
- Traitement en double : Les attaques par rejeu peuvent provoquer la réception répétée du même événement et entraîner des traitements ou crédits multiples.
- Absence de traçabilité : Sans les en-têtes Request-Id ou Event-Id, le suivi des requêtes, l’analyse des erreurs et la reproduction des incidents deviennent beaucoup plus difficiles.
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
- ID du lien.
- domain string
- Domaine du lien.
- redirectType integer
-
Enum:
200301302
- Type de redirection. Pour plus de détails, consultez la page terminologie clé.
- url string
- URL d’origine.
- ttl string
- Titre du lien.
- description string
-
Définit la valeur de la balise meta description lorsque
redirectTypeest200. - metaImg string
-
Définit la valeur de la balise meta image lorsque
redirectTypeest200. - memo string
- Note pour la gestion du lien.
- grpIdx integer
- IDX du groupe. Si un groupe est défini, le Webhook du groupe est appelé au lieu du global.
- grpNm string
- Nom du groupe.
- strtYmdt datetime
- Date/heure de début de validité du lien.
- ednYmdt datetime
- Date/heure d’expiration du lien.
- expireYn string
- Default:N
-
Enum:
YN
-
Transmis comme
Ylorsque le lien a expiré. - expireUrl string
- URL de redirection après expiration.
- acesCnt integer
- Nombre total de clics.
- pernCnt integer
- Nombre de clics uniques (utilisateurs uniques).
- acesMaxCnt integer
- Nombre maximum de clics autorisés. L’accès est bloqué une fois dépassé.
- referer string
- URL de la page d’où provient la requête.
- queryString string
- Chaîne de requête incluse lors de l’accès à l’URL courte.
- country string
- Code pays de l’utilisateur (ISO-3166).
- language string
- Code de langue de l’utilisateur (ISO-639).
- regYmdt datetime
- Date/heure de création du lien.
- modYmdt datetime
- Date/heure de modification du lien.
- payloadVersion string
- Version du payload. Augmente en cas de modifications ultérieures.
{
"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
- Numéro du coupon.
- domain string
- Domaine du coupon.
- nm string
- Nom du coupon.
- grpIdx integer
- IDX du groupe. Si un groupe est défini, le Webhook de ce groupe sera appelé au lieu du Webhook global.
- grpNm string
- Nom du groupe.
- discTypeIdx integer
- Par défaut :457
-
Enum :
457458
- Type de réduction. (457 : réduction en %, 458 : réduction en montant)
- discCurrency string
- Par défaut :KRW
-
Enum :
KRWCADCNYEURGBPIDRJPYMURRUBSGDUSD
- Devise. Obligatoire si le type de réduction est un montant (discTypeIdx : 458).
- formatDiscCurrency string
- Symbole monétaire.
- disc double
- Par défaut :0
-
Pourcentage de réduction (457) : entre 1 et 100 %.
Réduction en montant (458) : saisir la valeur. - strtYmd date
- Date de début de validité du coupon.
- endYmd date
- Date d’expiration du coupon.
- useLimit integer
- Par défaut :1
-
Enum :
012345
- Nombre d’utilisations du coupon. (0 : illimité, 1–5 : limité au nombre spécifié)
- imgUrl string
- URL de l’image du coupon.
- onsiteYn string
- Par défaut :N
-
Enum :
YN
-
Coupon sur site. Indique si le bouton
« Utiliser le coupon »est affiché sur la page du coupon.
Utilisé dans les magasins physiques lorsque le personnel valide le coupon. - onsitePwd string
-
Mot de passe du coupon sur site.
Requis pour l’utilisation du coupon. - memo string
- Mémo interne de référence.
- url string
-
Si une URL est saisie, un bouton
« Aller utiliser le coupon »est affiché sur la page du coupon.
En cliquant sur le bouton ou l’image du coupon, l’utilisateur est redirigé vers cette URL. - userId string
-
Utilisé pour gérer le bénéficiaire du coupon.
Obligatoire si la limite d’utilisation du coupon est définie entre 2 et 5.
Généralement, on saisit l’ID de connexion du membre du site ou un nom en anglais. - userNm string
- Nom de l’utilisateur du coupon. À usage interne.
- userPhnno string
- Numéro de téléphone de l’utilisateur du coupon. À usage interne.
- userEml string
- Adresse e-mail de l’utilisateur du coupon. À usage interne.
- userEtc1 string
- Champ supplémentaire pour la gestion interne.
- userEtc2 string
- Champ supplémentaire pour la gestion interne.
- useCnt integer
- Nombre d’utilisations du coupon.
- regYmdt datetime
- Date de création du coupon. Exemple : 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
- Stamp IDX.
- domain string
- Domaine du tampon.
- cardIdx integer
- Card IDX.
- cardNm string
- Nom de la carte.
- cardTtl string
- Titre de la carte.
- stamps integer
- Nombre de tampons collectés jusqu’à présent.
- maxStamps integer
- Nombre maximal de tampons sur la carte.
- stampUrl string
- URL de la page du tampon.
- url string
- URL vers laquelle l’utilisateur est redirigé en cliquant sur le bouton de la page du tampon.
- strtYmd date
- Date de début de validité du tampon.
- endYmd date
- Date d’expiration du tampon.
- onsiteYn string
-
Enum :
YN
-
Indique si la validation sur site est activée.
Si la valeur estY, le personnel peut ajouter des tampons directement en magasin. - onsitePwd string
-
Mot de passe pour la validation sur site.
Requis lors de l’utilisation de l’API d’avantage si l’option sur site est activée (Y). - memo string
- Note interne à des fins de référence.
- activeYn string
-
Enum :
YN
-
Indique si le tampon est actif.
S’il est désactivé, le client ne peut pas l’utiliser. - userId string
-
ID utilisateur. Utilisé pour gérer le bénéficiaire du tampon.
Généralement, il correspond à l’identifiant de connexion du membre du site web.
S’il n’est pas défini, un ID utilisateur est automatiquement généré par le système. - userNm string
- Nom de l’utilisateur. Usage interne uniquement.
- userPhnno string
- Numéro de téléphone de l’utilisateur. Usage interne uniquement.
- userEml string
- Adresse e-mail de l’utilisateur. Usage interne uniquement.
- userEtc1 string
- Champ supplémentaire pour la gestion interne.
- userEtc2 string
- Champ supplémentaire pour la gestion interne.
- stampImgUrl string
- URL de l’image du tampon.
- regYmdt datetime
- Date de création du tampon. Exemple : 2025-07-21 11:50:20
Vérification de Signature Webhook & Exemples de Code
L’authenticité d’une requête Webhook est vérifiée à l’aide de l’en-tête X-Vivoldi-Signature et de la Secret Key fournie.
La signature est générée en combinant le timestamp (t), l’identifiant d’événement (X-Vivoldi-Event-Id) et le hash SHA-256 du corps de la requête dans une chaîne séparée par des points (.), puis en appliquant un hash HMAC-SHA256 avec la Secret Key.
timestamp.eventId.payloadSha256
Si la valeur de hash générée (v1) correspond à la valeur de l’en-tête X-Vivoldi-Signature, la requête doit être considérée comme valide.
Dans le cas contraire, rejetez immédiatement la requête et consignez l’incident dans les logs.
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}`);
});
✨ Intégration en temps réel de niveau Enterprise
Optimisé pour les environnements d’entreprise traitant de grands volumes d’événements liés aux liens, coupons et tampons.
Grâce à une infrastructure haute disponibilité et à des systèmes de queueing fiables, Vivoldi assure une intégration stable avec vos plateformes CRM, de paiement et d’analyse, sans perte d’événements, même lors de pics soudains de trafic.