Webhook — Guide d’intégration
Le cœur de l’intégration Webhook de Vivoldi repose sur la vérification de la signature dans l’en-tête HTTP.
Chaque requête Webhook inclut X-Vivoldi-Request-Id, X-Vivoldi-Event-Id, X-Vivoldi-Signature, etc.,
et leur vérification vous permet de traiter en toute sécurité les événements de Liens, Coupons et Tampons.
Ce guide détaille le rôle de chaque champ d’en-tête et la procédure de vérification de la signature, étape par étape, et fournit du code d’exemple pour intégrer rapidement et en toute sécurité les requêtes Webhook.
HTTP Header
Le Webhook envoie une requête POST à l’URL de rappel désignée, permettant de vérifier l’intégrité et l’authenticité de chaque requête à l’aide des en-têtes tels que X-Vivoldi-Signature et X-Vivoldi-Timestamp.
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.
Politique de transmission · réponse · réessais
Critères de réussite
- Considéré comme réussi si le serveur récepteur renvoie une réponse HTTP 2xx (ex. : 200).
- Après vérification de la signature, retournez 200 OK immédiatement. Le délai d’expiration est de 5 secondes, donc les traitements longs doivent être exécutés en mode asynchrone après la réponse.
Réessais et désactivation
- Jusqu’à 5 réessais en cas d’erreurs réseau ou de réponses autres que 2xx.
- En cas de 5 échecs consécutifs, le Webhook est automatiquement désactivé et un e-mail d’alerte est envoyé à l’administrateur.
- Prévention des doublons : vérifiez les doublons à l’aide de la valeur
X-Vivoldi-Event-Id.
Les politiques peuvent être ajustées en fonction de l’environnement opérationnel.
Peut-on traiter un Webhook sans validation des en-têtes ?
Techniquement, traiter uniquement le corps POST (payload) suffit, mais en production vous devez systématiquement effectuer la vérification des en-têtes.
Omettre cette vérification expose à des risques de sécurité graves tels que requêtes falsifiées, altération du payload, traitements en double et perte de traçabilité.
Principaux risques :
-
Usurpation de requêtes (spoofing) : Un attaquant peut se faire passer pour Vivoldi et envoyer des requêtes falsifiées.
Sans vérification des en-têtes, le système peut considérer ces requêtes comme légitimes. - Altération des données : Si le payload est modifié en transit, l’absence de vérification de la signature empêche de détecter la falsification.
- Traitement en double : Les attaques de rejeu peuvent entraîner la réception multiple d’un même événement, provoquant des doublons ou des attributions en double.
- Perte de traçabilité : Sans les en-têtes Request-Id ou Event-Id, il devient impossible de tracer les requêtes, d’analyser les erreurs ou de reproduire les incidents.
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 — Exemple de code
Les requêtes Webhook doivent être vérifiées à l’aide de l’en-tête X-Vivoldi-Signature et de la clé secrète (Secret Key) fournie.
La signature est générée en combinant l’horodatage (t), l’ID de l’événement (X-Vivoldi-Event-Id) et le hachage SHA-256 du corps de la requête selon le format suivant :
timestamp.eventId.payloadSha256
Le résultat du hachage HMAC-SHA256 de cette chaîne avec la clé secrète devient la valeur v1, qui doit correspondre à la valeur de l’en-tête X-Vivoldi-Signature pour que la requête soit considérée comme valide.
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
Le Webhook intègre en temps réel les événements de Liens, Coupons et Tampons à vos systèmes CRM, de paiement et d’analyse.
Grâce à une infrastructure à haute disponibilité, à des mécanismes fiables de mise en file d’attente et de nouvelle tentative, ainsi qu’à une sécurité basée sur HMAC, il garantit une fiabilité totale dans les environnements Enterprise.