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-Idstring
- ID unique de la requête. Généré à chaque demande et utilisé pour identifier une transaction spécifique.
- X-Vivoldi-Event-Idstring
- 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-Typestring
- 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-Typestring
- Enum :URLCOUPONSTAMP
- URL : lien court, COUPON : coupon, STAMP : tampon.
- X-Vivoldi-Action-Typestring
- 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-Idxinteger
- IDX unique de l’organisation.
Consultable dans la page [Paramètres → Paramètres de l’organisation]. - X-Vivoldi-Timestampinteger
- Heure de la requête (secondes UNIX epoch). Tolérance recommandée : ±5 minutes.
- X-Content-SHA256string
- Valeur de hachage SHA-256 du corps (payload) de la requête.
- X-Vivoldi-Signaturestring
- 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
- linkIdstring
- ID du lien.
- domainstring
- Domaine du lien.
- redirectTypeinteger
- Enum:200301302
- Type de redirection. Pour plus de détails, consultez la page terminologie clé.
- urlstring
- URL d’origine.
- ttlstring
- Titre du lien.
- descriptionstring
- Définit la valeur de la balise meta description lorsque
redirectTypeest200. - metaImgstring
- Définit la valeur de la balise meta image lorsque
redirectTypeest200. - memostring
- Note pour la gestion du lien.
- grpIdxinteger
- IDX du groupe. Si un groupe est défini, le Webhook du groupe est appelé au lieu du global.
- grpNmstring
- Nom du groupe.
- strtYmdtdatetime
- Date/heure de début de validité du lien.
- ednYmdtdatetime
- Date/heure d’expiration du lien.
- expireYnstring
- Default:N
- Enum:YN
- Transmis comme
Ylorsque le lien a expiré. - expireUrlstring
- URL de redirection après expiration.
- acesCntinteger
- Nombre total de clics.
- pernCntinteger
- Nombre de clics uniques (utilisateurs uniques).
- acesMaxCntinteger
- Nombre maximum de clics autorisés. L’accès est bloqué une fois dépassé.
- refererstring
- URL de la page d’où provient la requête.
- queryStringstring
- Chaîne de requête incluse lors de l’accès à l’URL courte.
- countrystring
- Code pays de l’utilisateur (ISO-3166).
- languagestring
- Code de langue de l’utilisateur (ISO-639).
- regYmdtdatetime
- Date/heure de création du lien.
- modYmdtdatetime
- Date/heure de modification du lien.
- payloadVersionstring
- 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
- cpnNostring
- Numéro du coupon.
- domainstring
- Domaine du coupon.
- nmstring
- Nom du coupon.
- grpIdxinteger
- IDX du groupe. Si un groupe est défini, le Webhook de ce groupe sera appelé au lieu du Webhook global.
- grpNmstring
- Nom du groupe.
- discTypeIdxinteger
- Par défaut :457
- Enum :457458
- Type de réduction. (457 : réduction en %, 458 : réduction en montant)
- discCurrencystring
- Par défaut :KRW
- Enum :KRWCADCNYEURGBPIDRJPYMURRUBSGDUSD
- Devise. Obligatoire si le type de réduction est un montant (discTypeIdx : 458).
- formatDiscCurrencystring
- Symbole monétaire.
- discdouble
- Par défaut :0
- Pourcentage de réduction (457) : entre 1 et 100 %.
Réduction en montant (458) : saisir la valeur. - strtYmddate
- Date de début de validité du coupon.
- endYmddate
- Date d’expiration du coupon.
- useLimitinteger
- Par défaut :1
- Enum :012345
- Nombre d’utilisations du coupon. (0 : illimité, 1–5 : limité au nombre spécifié)
- imgUrlstring
- URL de l’image du coupon.
- onsiteYnstring
- 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. - onsitePwdstring
- Mot de passe du coupon sur site.
Requis pour l’utilisation du coupon. - memostring
- Mémo interne de référence.
- urlstring
- 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. - userIdstring
- 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. - userNmstring
- Nom de l’utilisateur du coupon. À usage interne.
- userPhnnostring
- Numéro de téléphone de l’utilisateur du coupon. À usage interne.
- userEmlstring
- Adresse e-mail de l’utilisateur du coupon. À usage interne.
- userEtc1string
- Champ supplémentaire pour la gestion interne.
- userEtc2string
- Champ supplémentaire pour la gestion interne.
- useCntinteger
- Nombre d’utilisations du coupon.
- regYmdtdatetime
- 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
- stampIdxinteger
- Stamp IDX.
- domainstring
- Domaine du tampon.
- cardIdxinteger
- Card IDX.
- cardNmstring
- Nom de la carte.
- cardTtlstring
- Titre de la carte.
- stampsinteger
- Nombre de tampons collectés jusqu’à présent.
- maxStampsinteger
- Nombre maximal de tampons sur la carte.
- stampUrlstring
- URL de la page du tampon.
- urlstring
- URL vers laquelle l’utilisateur est redirigé en cliquant sur le bouton de la page du tampon.
- strtYmddate
- Date de début de validité du tampon.
- endYmddate
- Date d’expiration du tampon.
- onsiteYnstring
- Enum :YN
- Indique si la validation sur site est activée.
Si la valeur estY, le personnel peut ajouter des tampons directement en magasin. - onsitePwdstring
- 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). - memostring
- Note interne à des fins de référence.
- activeYnstring
- Enum :YN
- Indique si le tampon est actif.
S’il est désactivé, le client ne peut pas l’utiliser. - userIdstring
- 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. - userNmstring
- Nom de l’utilisateur. Usage interne uniquement.
- userPhnnostring
- Numéro de téléphone de l’utilisateur. Usage interne uniquement.
- userEmlstring
- Adresse e-mail de l’utilisateur. Usage interne uniquement.
- userEtc1string
- Champ supplémentaire pour la gestion interne.
- userEtc2string
- Champ supplémentaire pour la gestion interne.
- stampImgUrlstring
- URL de l’image du tampon.
- regYmdtdatetime
- 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.