Webhook — Integration Guide
The core of Vivoldi Webhook integration is HTTP header signature verification.
Every Webhook request includes X-Vivoldi-Request-Id, X-Vivoldi-Event-Id, X-Vivoldi-Signature, and more,
and validating these lets you securely process Link, Coupon, and Stamp events.
This guide explains the role of each header field and the step-by-step signature verification procedure, and provides sample code so you can integrate Webhook requests quickly and safely.
HTTP Header
Webhook sends a POST request to the designated callback URL, allowing you to verify the integrity and authenticity of each request using headers such as X-Vivoldi-Signature and 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
- Unique request ID. A new ID is generated for every request and used to identify each transaction.
- X-Vivoldi-Event-Id string
-
Unique event ID.
When a request fails and is retried, the same Event-Id is preserved to prevent duplicate processing for the same event. - X-Vivoldi-Webhook-Type string
- Default:GLOBAL
-
Enum:
GLOBALGROUP
-
If a GROUP Webhook is enabled, this value is set to GROUP.
Stamp events always use GROUP since they operate per stamp card.
Link and coupon events are sent as GLOBAL if no group Webhook is configured. - X-Vivoldi-Resource-Type string
-
Enum:
URLCOUPONSTAMP
- URL: Short URL, COUPON: Coupon, STAMP: Stamp
- X-Vivoldi-Action-Type string
-
Enum:
NONEADDREMOVEUSE
-
NONE: Used for link click or coupon usage events; no additional action occurs.
ADD: Stamp added
REMOVE: Stamp removed
USE: Stamp reward redeemedFuture link or coupon actions may expand this header value (
X-Vivoldi-Action-Type). - X-Vivoldi-Comp-Idx integer
-
Organization unique IDX.
You can check it in [Settings → Organization Settings]. - X-Vivoldi-Timestamp integer
- Request time (UNIX epoch seconds). Recommended tolerance ±5 minutes.
- X-Content-SHA256 string
- SHA-256 hash value of the request payload.
- X-Vivoldi-Signature string
- Signature information of the request. Format: t=timestamp, v1=signature value, alg=algorithm.
Transmission · Response · Retry Policy
Success Criteria
- Considered successful if the receiving server returns an HTTP 2xx response (e.g., 200).
- After verifying the signature, return 200 OK immediately. Since the timeout is 5 seconds, run long processes asynchronously after responding.
Retry & Deactivation
- Up to 5 retries in case of network errors or non-2xx responses.
- If there are 5 consecutive failures, the Webhook is automatically deactivated and an alert email is sent to the administrator.
- Duplicate prevention: Check duplicates using the
X-Vivoldi-Event-Idvalue.
Policies may be adjusted depending on the operating environment.
Is it okay to process Webhooks without header validation?
Technically, processing only the POST body (payload) is sufficient, but in production you must always perform header verification.
Skipping header checks exposes you to serious security risks such as forged requests, payload tampering, duplicate processing, and loss of traceability.
Key risks:
-
Request spoofing: An attacker can impersonate Vivoldi and send forged requests.
Without header verification, your system may accept these as legitimate requests. - Data tampering: If the payload is altered in transit, tampering cannot be detected without signature verification.
- Duplicate processing: Replay attacks may cause the same event to be received multiple times, resulting in duplicate handling or double stamping.
- Loss of traceability: Without Request-Id or Event-Id headers, it becomes impossible to trace requests, analyze errors, or reproduce 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
- Link ID.
- domain string
- Link domain.
- url string
- Original URL.
- ttl string
- Link title.
- description string
-
Sets the description meta tag when
redirectTypeis200. - metaImg string
-
Sets the image meta tag when
redirectTypeis200. - memo string
- Memo for link management.
- grpIdx integer
- Group IDX. If a group is specified, the group Webhook is triggered instead of the global one.
- grpNm string
- Group name.
- strtYmdt datetime
- Link validity start date/time.
- ednYmdt datetime
- Link validity expiration date/time.
- expireYn string
- Default:N
-
Enum:
YN
-
Sent as
Ywhen the link has expired. - expireUrl string
- URL to redirect after expiration.
- acesCnt integer
- Total clicks.
- pernCnt integer
- Unique click count (unique users).
- acesMaxCnt integer
- Maximum allowed clicks. Access is blocked once exceeded.
- referer string
- URL of the page where the request originated.
- queryString string
- Query string included in the short URL request.
- country string
- User’s country code (ISO-3166).
- language string
- User’s language code (ISO-639).
- regYmdt datetime
- Link creation date/time.
- modYmdt datetime
- Link modification date/time.
- payloadVersion string
- Payload version. Incremented when changes are made afterward.
{
"cpnNo": "ZJLF0399WQBEQZJM",
"domain": "https://vvd.bz",
"nm": "$10 off cake coupon",
"grpIdx": 574,
"grpNm": "Event coupons",
"discTypeIdx": 457,
"discCurrency": "USD",
"formatDiscCurrency": "$10"
"disc": 10.0,
"strtYmd": "2025-01-01",
"endYmd": "2025-12-31",
"useLimit": 1,
"imgUrl": "https://file.vivoldi.com/coupon/2024/11/08/lmTFkqLQdCzeBuPdONKG.webp",
"onsiteYn": "Y",
"onsitePwd": "123456",
"memo": "$10 off cake with coupon at the venue",
"url": "",
"userId": "user08",
"userNm": "Emily",
"userPhnno": "202-555-0173",
"userEml": "test@gmail.com",
"userEtc1": "",
"userEtc2": "",
"useCnt": 0,
"regYmdt": "2025-08-31 18:10:22",
"payloadVersion": "v1"
}
Payload Parameters
- cpnNo string
- Coupon number.
- domain string
- Coupon domain.
- nm string
- Coupon name.
- grpIdx integer
- Group IDX. If specified, the group Webhook will be triggered instead of the global one.
- grpNm string
- Group name.
- discTypeIdx integer
- Default:457
-
Enum:
457458
- Discount type. (457: Percentage discount %, 458: Fixed amount discount)
- discCurrency string
- Default:KRW
-
Enum:
KRWCADCNYEURGBPIDRJPYMURRUBSGDUSD
- Currency unit. Required when using fixed amount discount (discTypeIdx:458).
- formatDiscCurrency string
- Currency symbol.
- disc double
- Default:0
- Discount value. (457: 1–100% range, 458: amount value)
- strtYmd date
- Coupon start date.
- endYmd date
- Coupon expiration date.
- useLimit integer
- Default:1
-
Enum:
012345
- Coupon usage limit. (0: Unlimited, 1–5: Limited times)
- imgUrl string
- Coupon image URL.
- onsiteYn string
- Default:N
-
Enum:
YN
-
On-site coupon flag. Indicates whether the
“Use Coupon” buttonis displayed on the coupon page.
Required when coupons are redeemed at offline stores. - onsitePwd string
-
On-site coupon password.
Required when redeeming the coupon. - memo string
- Internal memo.
- url string
-
If a URL is entered, a
“Go to Coupon” buttonwill appear on the coupon page.
Clicking the button or coupon image will redirect to the specified URL. - userId string
-
Used to manage coupon recipients.
Required when coupon usage limit is set to 2–5 times.
Typically the login ID or English name of a website member. - userNm string
- Coupon user name. Internal use only.
- userPhnno string
- Coupon user phone number. Internal use only.
- userEml string
- Coupon user email. Internal use only.
- userEtc1 string
- Additional internal field.
- userEtc2 string
- Additional internal field.
- useCnt integer
- Number of coupon uses.
- regYmdt datetime
- Coupon creation date. Example: 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
- Stamp domain.
- cardIdx integer
- Card IDX.
- cardNm string
- Card name.
- cardTtl string
- Card title.
- stamps integer
- Number of collected stamps so far.
- maxStamps integer
- Maximum number of stamps for the card.
- stampUrl string
- URL of the stamp page.
- url string
- The URL to which the user is redirected when clicking the button on the stamp page.
- strtYmd date
- Stamp validity start date.
- endYmd date
- Stamp expiration date.
- onsiteYn string
-
Enum:
YN
-
Indicates whether on-site accumulation is enabled.
If the value is
Y, store staff can add stamps at the location. - onsitePwd string
-
On-site accumulation password.
Required when on-site accumulation (
Y) is enabled for using the stamp reward API. - memo string
- Internal memo for reference.
- activeYn string
-
Enum:
YN
- Indicates whether the stamp is active. If deactivated, customers cannot use the stamp.
- userId string
-
User ID. Used to manage the recipient of the stamp.
Typically, this corresponds to the website member’s login ID.
If not set, the system automatically generates a user ID. - userNm string
- User name. For internal management.
- userPhnno string
- User phone number. For internal management.
- userEml string
- User email address. For internal management.
- userEtc1 string
- Additional internal management field.
- userEtc2 string
- Additional internal management field.
- stampImgUrl string
- URL of the stamp image.
- regYmdt datetime
- Stamp creation date. Example: 2025-07-21 11:50:20
Signature Verification — Code Sample
Webhook requests must be verified using the X-Vivoldi-Signature header and the issued Webhook Secret Key.
The signature is generated by combining the timestamp (t), event ID (X-Vivoldi-Event-Id), and the SHA-256 hash of the request body in the following format:
timestamp.eventId.payloadSha256
The result of hashing this string with the Secret Key using HMAC-SHA256 becomes the v1 value, which must match the X-Vivoldi-Signature header for the request to be considered valid.
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}`);
});
✨ Enterprise-grade Real-time Integration
Webhook integrates Link, Coupon, and Stamp events in real time with your CRM, payment, and analytics systems.
Combining high-availability infrastructure, reliable queuing & retry mechanisms, and HMAC-based security, it delivers complete reliability in Enterprise environments.