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: CLICK
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
- Identifiant unique permettant d’identifier la requête. Un nouvel identifiant est généré pour chaque requête HTTP et peut être utilisé pour suivre une requête spécifique.
- X-Vivoldi-Event-Id string
- Identifiant unique permettant d’identifier un événement. Le même Event ID est conservé lors des nouvelles tentatives d’envoi d’un même événement, ce qui permet au système destinataire d’éviter les traitements en double.
- X-Vivoldi-Webhook-Type string
- Default:GLOBAL
-
Enum:
GLOBALGROUP
-
Indique le périmètre d’application du Webhook.
GROUP : utilisé lorsqu’un Webhook de groupe est appliqué.
Les événements de carte de fidélité prennent uniquement en charge les Webhooks de groupe et sont donc toujours envoyés avec la valeurGROUP.
Les événements liés aux liens et aux coupons sont envoyés avec la valeurGLOBALlorsqu’aucun Webhook de groupe n’est configuré. - X-Vivoldi-Resource-Type string
-
Enum:
URLCOUPONSTAMP
-
Type de ressource associé à l’événement.
URL : URL courte
COUPON : Coupon
STAMP : Tampon - X-Vivoldi-Action-Type string
-
Enum:
CLICKUSEADDREMOVE
-
Type d’action ayant déclenché l’événement.
CLICK : Clic sur un lien
USE : Utilisation d’un coupon, utilisation d’une récompense de carte de fidélité
ADD : Ajout d’un tampon
REMOVE : Suppression d’un tamponUtilisez cette valeur avec
Resource-Typepour identifier précisément le type d’événement. - X-Vivoldi-Comp-Idx integer
- IDX permettant d’identifier l’organisation. Vous pouvez le consulter dans la page [Paramètres → Paramètres de l’organisation].
- X-Vivoldi-Timestamp integer
- Heure de création de la requête. Elle est transmise au format UNIX epoch seconds. Une différence maximale de ±5 minutes est recommandée afin de tenir compte des écarts d’heure entre les serveurs.
- X-Content-SHA256 string
- Valeur de hachage SHA-256 du payload de la requête. Elle peut être utilisée pour vérifier l’intégrité du payload.
- X-Vivoldi-Signature string
-
Informations de signature utilisées pour vérifier la requête.
Elles incluent
t: horodatage,v1: valeur de signature, etalg: algorithme de signature.
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
Le succès d’une requête Webhook est déterminé selon le code de statut HTTP renvoyé par le serveur destinataire.
-
Une réponse HTTP 2xx est considérée comme réussie.
Toutes les réponses 2xx, y compris200,202et204, sont acceptées. Le contenu du corps de la réponse n’est pas vérifié. -
Le délai d’attente de la réponse est de 5 secondes.
Après la vérification de la signature, nous recommandons de renvoyer immédiatement une réponse2xxet d’effectuer le traitement réel de manière asynchrone. -
Les redirections HTTP ne sont pas suivies.
Les réponses telles que
301et302sont considérées comme des échecs. Vous devez donc enregistrer l’URL de Callback finale.
Tentatives de réessai & Désactivation
En cas d’échec d’envoi, le Webhook effectue automatiquement des tentatives de réessai. Si des échecs répétés se produisent, le statut du Webhook passe à Désactivé par le système afin d’éviter les tentatives d’envoi inutiles.
-
Les tentatives de réessai sont effectuées pour tous les codes de réponse HTTP.
Les réponses telles que
400,404et401suivent la même politique de réessai. -
Pendant les tentatives de réessai,
X-Vivoldi-Event-Idreste identique. Le serveur destinataire doit utiliser cette valeur pour éviter le traitement en double des événements. - Même après 5 tentatives de réessai échouées, le Webhook n’est pas désactivé immédiatement. Une notification par e-mail est d’abord envoyée, puis un délai de grâce de 60 minutes est accordé. Si le rétablissement n’a pas lieu pendant cette période, le statut du Webhook passe à Désactivé par le système.
Les Webhooks avec le statut Désactivé par le système peuvent être retrouvés via le filtre Désactivé par le système dans la liste du tableau de bord, puis réactivés.
| Étape | Délai | Action |
|---|---|---|
| Tentatives 1 à 3 | Immédiatement · Après 1 s · Après 2 s | Des nouvelles tentatives immédiates sont effectuées pour gérer les erreurs réseau temporaires. |
| 4e tentative | Après 10 min | Une nouvelle tentative est effectuée en tenant compte du temps nécessaire au redémarrage du serveur destinataire ou à la résolution d’un incident temporaire. |
| 5e tentative | Après 30 min | La dernière tentative d’envoi est effectuée. En cas d’échec, les nouvelles tentatives automatiques sont arrêtées. |
| E-mail d’avertissement | Immédiatement après 5 échecs | Le Webhook n’est pas désactivé immédiatement. Un délai de grâce de 60 minutes commence après le 5e échec et une notification par e-mail est envoyée. Selon le cycle de traitement des notifications, l’e-mail peut être retardé jusqu’à environ 10 minutes. |
| Délai de grâce | 30 min à 90 min | Si le serveur est rétabli pendant le délai de grâce de 60 minutes, l’envoi du Webhook reprend sans désactivation du système. |
| Désactivation par le système | Après 90 min | Si la première tentative d’envoi après le délai de grâce échoue également, le statut du Webhook passe à Désactivé par le système. |
Si des échecs répétés se produisent depuis la même URL de Callback, l’envoi est temporairement limité afin d’éviter l’accumulation continue des requêtes jusqu’au rétablissement du serveur destinataire.
Les interruptions courtes, comme les déploiements ou les incidents temporaires, reprennent automatiquement après rétablissement.
Les événements d’utilisation de coupons et de tampons ne sont jamais perdus.
Comme il s’agit d’événements importants qui ne se produisent qu’une seule fois, ils sont conservés dans une file d’attente pendant les nouvelles tentatives d’envoi et le délai de grâce, puis transmis dans l’ordre.
Les événements de clic sur les liens se produisent de manière répétée et les données analytiques sont enregistrées dans Vivoldi. Par conséquent, ces événements ne sont pas stockés séparément et ne sont pas renvoyés en cas d’échec d’envoi du Webhook.
Guide d’implémentation du serveur de réception Webhook
-
Le même événement peut être envoyé plusieurs fois.
Le même événement peut être reçu plusieurs fois en raison des nouvelles tentatives d’envoi ou des conditions réseau. ConservezX-Vivoldi-Event-Idet retournez200 OKsans traitement supplémentaire si l’événement a déjà été traité.
Ceci est particulièrement important pour les opérations qui ne doivent pas être traitées plusieurs fois, comme l’utilisation d’un coupon ou l’ajout d’un tampon. -
L’ordre des événements n’est pas garanti.
Un événement renvoyé peut arriver après un événement généré ultérieurement.
Si le respect de l’ordre des événements est nécessaire, utilisez les valeursregYmdtetmodYmdtdu Payload comme référence. -
Il est recommandé de séparer la gestion de la réponse du traitement réel.
L’exécution d’opérations en base de données ou d’appels API externes avant l’envoi de la réponse peut dépasser la limite de 5 secondes.
Nous recommandons le flux suivant : vérification de la signature → réponse200 OK→ traitement via une file d’attente interne. -
Vérifiez la signature avec le corps original de la requête.
L’analyse du JSON puis sa sérialisation peuvent modifier la valeur de hachage en raison des changements d’espaces ou d’ordre des clés.
Si votre framework transforme automatiquement le corps de la requête, récupérez séparément le raw body. -
Ignorez les champs inconnus.
De nouveaux champs peuvent être ajoutés au Payload à l’avenir. Votre intégration doit ignorer les champs qui ne sont pas reconnus. -
La Secret Key dépend de la cible du Webhook.
SiX-Vivoldi-Webhook-TypeestGLOBAL, vérifiez la signature avec la Secret Key globale. S’il estGROUP, utilisez la Secret Key configurée pour le groupe ou la carte de fidélité concernée.
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
Moment de déclenchement de l’événement
Link Webhook envoie les informations d’événement vers l’URL de Callback configurée lorsqu’un clic sur une URL raccourcie se produit.
Webhook peut être configuré pour des liens individuels ou des groupes de liens.
Si les deux sont configurés, les paramètres du groupe de liens sont prioritaires,
et les critères ainsi que l’intervalle d’envoi suivent les paramètres du groupe. Le même événement n’est pas envoyé plusieurs fois.
La valeur de X-Vivoldi-Action-Type est CLICK.
Le Webhook de groupe de liens est une fonctionnalité réservée au forfait Enterprise.
Vous pouvez choisir le nombre de clics ou le nombre de visiteurs comme critère d’envoi, et le Webhook est envoyé chaque fois que le seuil cumulé configuré est atteint.
Par exemple, si le critère d’envoi est le nombre de clics avec un intervalle d’envoi défini tous les 100 clics, le Webhook est envoyé lorsque le nombre cumulé de clics atteint 100, 200, 300, et ainsi de suite.
{
"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 d’identification du lien.
- domain string
- Domaine du lien.
- compIdx integer
-
IDX de l’organisation.
Cette valeur correspond à la valeur de l’en-tête
X-Vivoldi-Comp-Idx. - redirectType integer
-
Enum:
200301302
-
Méthode de redirection du lien.
200: Mode d’affichage de page
301: Redirection permanente
302: Redirection temporaire
Pour plus d’informations, consultez la page Terminologie. - url string
- URL d’origine.
- ttl string
- Titre du lien.
- description string
-
Valeur de la balise meta description utilisée lorsque
redirectTypeest200. - metaImg string
-
URL de l’image de la balise meta utilisée lorsque
redirectTypeest200. - memo string
- Note pour la gestion du lien.
- grpIdx integer
-
IDX du groupe de liens.
Si un Webhook est configuré pour le groupe de liens, le Webhook du groupe est prioritaire sur les paramètres du lien individuel. - grpNm string
- Nom du groupe de liens.
- strtYmdt datetime
- Date et heure de début de validité du lien.
- endYmdt datetime
- Date et heure d’expiration de validité du lien.
- expireYn string
-
Enum:
YN
-
Indique si la période de validité du lien a expiré.
La valeur
Yest envoyée lorsque le lien est expiré. - expireUrl string
- URL vers laquelle la redirection est effectuée après l’expiration du lien.
- acesCnt integer
-
Nombre total de clics cumulés.
Cette valeur inclut l’événement de clic actuel.
La condition d’envoi du Webhook est également évaluée selon cette valeur. Par exemple, si l’intervalle d’envoi est défini sur 100 clics, le Webhook est envoyé chaque fois que le nombre total de clics atteint 100, 200, 300, etc. - pernCnt integer
- Nombre total de visiteurs (utilisateurs uniques). Cette valeur inclut l’événement de clic actuel.
- acesMaxCnt integer
-
Nombre maximal de clics autorisés.
Si la valeur est
0, aucune limite n’est appliquée. Lorsque la limite est dépassée, l’accès au lien est bloqué. - referer string
- URL de la page précédente à l’origine de la requête.
- queryString string
- Query String transmis lors de l’accès à l’URL raccourcie.
- country string
- Code pays de l’utilisateur ayant effectué l’accès (ISO-3166).
- language string
- Code langue de l’utilisateur ayant effectué l’accès (ISO-639).
- regYmdt datetime
- Date et heure de création du lien.
- modYmdt datetime
- Date et heure de modification du lien.
- payloadVersion string
- Version de la spécification du Payload. Même si de nouveaux champs sont ajoutés, la signification et le comportement des champs existants restent inchangés jusqu’à la mise à jour de cette valeur.
Moment de déclenchement de l’événement
Coupon Webhook envoie les informations d’événement vers l’URL de Callback configurée lorsqu’un événement d’utilisation d’un coupon se produit.
Webhook peut être configuré pour des coupons individuels ou des groupes de coupons.
Si les deux sont configurés, les paramètres du groupe de coupons sont prioritaires,
et le même événement n’est pas envoyé plusieurs fois.
Coupon Webhook pour les groupes de coupons est disponible à partir du forfait Business.
L’événement est envoyé immédiatement après le traitement de l’utilisation du coupon, et la valeur de X-Vivoldi-Action-Type est USE.
L’événement est envoyé de la même manière, que le coupon soit utilisé via le tableau de bord, l’API ou un traitement hors ligne.
Lorsque la limite d’appels est dépassée ou qu’une nouvelle tentative est en attente, les événements sont stockés dans une file d’attente puis transmis dans l’ordre.
Lorsque plusieurs coupons sont traités simultanément via l’API, la livraison des événements peut être effectuée progressivement en plusieurs opérations.
{
"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 de la page du coupon.
- nm string
- Nom du coupon.
- grpIdx integer
-
IDX du groupe auquel appartient le coupon.
Si le coupon n’appartient à aucun groupe, la valeur est
0.
Lorsqu’un Webhook de groupe est configuré, les paramètres du groupe sont prioritaires, et la valeur deX-Vivoldi-Webhook-Typeest envoyée avec la valeurGROUP.
Si aucun Webhook de groupe n’est configuré, l’envoi suit les paramètres du coupon individuel. - grpNm string
- Nom du groupe de coupons.
- discTypeIdx integer
-
Enum:
457458
-
Type de remise.
457: Remise en pourcentage (%)
458: Remise d’un montant fixe - discCurrency string
- Default:KRW
-
Enum:
KRWCADCNYEURGBPIDRJPYMURRUBSGDUSD
-
Devise utilisée pour le montant de la remise.
Obligatoire lors de l’utilisation d’une remise d’un montant fixe (
discTypeIdx=458). - formatDiscCurrency string
- Format d’affichage de la devise.
- disc double
- Default:0
-
Valeur de la remise.
Pour une remise en pourcentage (457), la valeur doit être comprise entre1~100%, tandis qu’une remise d’un montant fixe (458) correspond au montant de la réduction. - strtYmd date
- Date de début de validité du coupon.
- endYmd date
- Date d’expiration du coupon.
- useLimit integer
- Default:1
-
Enum:
012345
-
Nombre d’utilisations autorisées du coupon.
0: Sans limite
1~5: Utilisable selon le nombre de fois défini - imgUrl string
- URL de l’image du coupon.
- onsiteYn string
- Default:N
-
Enum:
YN
-
Indique si l’utilisation du coupon en magasin est prise en charge.
Lorsque la valeur est
Y, le boutonUtiliser le coupons’affiche sur la page du coupon, et le coupon peut être utilisé en magasin après vérification par un employé. - onsitePwd string
-
Mot de passe utilisé pour l’authentification de l’utilisation du coupon en magasin.
Comme il est inclus en clair dans lePayload, veillez à ne pas l’enregistrer dans les journaux du serveur destinataire. - memo string
- Note interne.
- url string
-
Si cette valeur est définie, le bouton
Accéder à l’utilisation du coupons’affiche sur la page du coupon.
L’utilisateur est redirigé vers cette URL lorsqu’il clique sur le bouton ou l’image du coupon. - userId string
-
ID permettant d’identifier l’utilisateur du coupon.
Obligatoire lorsque la limite d’utilisation du coupon est définie entre2~5. Il s’agit généralement de l’ID membre du service ou d’un identifiant client. - userNm string
- Nom de l’utilisateur du coupon. Utilisé pour la gestion interne et l’identification.
- userPhnno string
- Coordonnées de l’utilisateur du coupon. Utilisées pour la gestion interne et l’identification.
- userEml string
- Adresse e-mail de l’utilisateur du coupon. Utilisée pour la gestion interne et l’identification.
- userEtc1 string
- Champ supplémentaire pour la gestion interne.
- userEtc2 string
- Champ supplémentaire pour la gestion interne.
- useCnt integer
-
Nombre actuel d’utilisations du coupon.
L’événement d’utilisation actuel n’est pas encore inclus dans cette valeur.
Pour inclure l’utilisation actuelle, calculez la valeur avecuseCnt + 1. - regYmdt datetime
- Date et heure de création du coupon. Exemple : 2025-07-21 11:50:20
- payloadVersion string
- Version de la spécification du Payload. Même si de nouveaux champs sont ajoutés, la signification et le comportement des champs existants restent inchangés jusqu’à la modification de cette valeur.
Moment de déclenchement de l’événement
Le Webhook est configuré dans la carte de fidélité. Tous les événements de tampons générés depuis cette carte sont envoyés.
Il est envoyé lors des événements d’ajout, de suppression ou d’utilisation d’une récompense de fidélité.
Le type d’événement est identifié par la valeur de l’en-tête X-Vivoldi-Action-Type.
ADD— Tampon ajoutéREMOVE— Tampon suppriméUSE— Récompense de fidélité utilisée
Quelle que soit l’origine de la modification (tableau de bord, API, écran de gestion des tampons ou autre méthode), l’événement est envoyé avec le même type.
changedStamps représente le nombre de tampons concernés.
L’ajout ou la suppression est déterminé par la valeur de X-Vivoldi-Action-Type.
L’utilisation d’une récompense (USE) ne modifie pas le nombre de tampons, la valeur envoyée est donc 0.
stamps dépend de la manière dont l’événement est généré.Pour les événements d’ajout, de suppression et d’utilisation d’une récompense via l’API,
stamps correspond au nombre de tampons avant modification.
La valeur après modification peut être calculée avec stamps + changedStamps.
Pour REMOVE, il faut soustraire changedStamps.Lorsque la modification est effectuée depuis l’écran de gestion des tampons du tableau de bord,
stamps correspond au nombre de tampons après modification.Pour calculer précisément le nombre actuel de tampons, utilisez la valeur avant l’événement et
changedStamps afin de calculer la valeur après modification.
{
"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,
"changedStamps": 2,
"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
- IDX d’identification du tampon.
- domain string
- Domaine de la page de fidélité.
- cardIdx integer
- IDX d’identification de la carte de fidélité.
- cardNm string
- Nom de la carte de fidélité.
- cardTtl string
- Titre de la carte de fidélité.
- stamps integer
-
Nombre actuel de tampons. Toutefois, le point de référence dépend de la manière dont l’événement est généré.
Pour les événements d’ajout, de suppression et d’utilisation de récompense via l’API, cette valeur correspond au nombre de tampons avant modification. La valeur après modification peut être calculée à l’aide destampsetchangedStamps.
(ADD: tampon ajouté,REMOVE: tampon supprimé)
Lorsque la modification est effectuée directement depuis l’écran de gestion des tampons du tableau de bord, cette valeur correspond au nombre de tampons après modification. - maxStamps integer
- Nombre maximal de tampons de la carte de fidélité.
- changedStamps integer
-
Nombre de tampons modifiés par cet événement.
L’ajout ou la suppression est déterminé par la valeur de
X-Vivoldi-Action-Type.
L’utilisation d’une récompense (USE) ne modifie pas le nombre de tampons, la valeur est donc0. - stampUrl string
- URL de la page de fidélité.
- url string
- URL vers laquelle l’utilisateur est redirigé lorsqu’un bouton est cliqué sur la page de fidélité.
- strtYmd date
- Date de début de validité des tampons.
- endYmd date
- Date d’expiration de validité des tampons.
- onsiteYn string
-
Enum:
YN
-
Indique si la collecte de tampons en magasin est prise en charge.
Lorsque la valeur est
Y, les employés peuvent vérifier le client et ajouter des tampons en magasin. - onsitePwd string
-
Mot de passe utilisé pour vérifier la collecte de tampons en magasin ou l’utilisation d’une récompense.
Requis pour les appels API associés lorsque la collecte de tampons en magasin est activée (onsiteYn=Y). - memo string
- Note interne.
- activeYn string
-
Enum:
YN
- Indique si la carte de fidélité est active. Lorsqu’elle est désactivée, les clients ne peuvent pas utiliser la carte de fidélité.
- userId string
-
ID utilisateur permettant d’identifier l’utilisateur de la carte de fidélité.
Il s’agit généralement de l’ID membre du service ou d’un identifiant client.
Si aucune valeur n’est définie, Vivoldi la génère automatiquement. - userNm string
- Nom de l’utilisateur de la carte de fidélité. Utilisé pour la gestion interne et l’identification.
- userPhnno string
- Coordonnées de l’utilisateur de la carte de fidélité. Utilisées pour la gestion interne et l’identification.
- userEml string
- Adresse e-mail de l’utilisateur de la carte de fidélité. Utilisée pour la gestion interne et l’identification.
- 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 et heure de création du tampon. Exemple : 2025-07-21 11:50:20
- payloadVersion string
- Version de la spécification du Payload. Même si de nouveaux champs sont ajoutés, la signification et le comportement des champs existants restent inchangés jusqu’à la modification de cette valeur.
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;
// Timestamp tolerance (±5 minutes)
// X-Vivoldi-Timestamp is in MILLISECONDS, so compare against System.currentTimeMillis().
if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > 300_000L) {
log.warn("Webhook timestamp out of tolerance: {}", timestamp);
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 (±5 minutes)
// X-Vivoldi-Timestamp is in MILLISECONDS, so compare against time() * 1000.
if (abs(time() * 1000 - (int)$timestamp) > 300000) {
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 tolerance (±5 minutes)
// X-Vivoldi-Timestamp is in MILLISECONDS, so compare against Date.now() directly.
if (Math.abs(Date.now() - Number(timestamp)) > 300000) 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.