Vivoldi Webhook API & HMAC Signature Verification
Secure Webhook integration starts with HTTP header signature verification.
Every Vivoldi Webhook request includes headers such as X-Vivoldi-Request-Id, X-Vivoldi-Event-Id, X-Vivoldi-Signature.
Validating these headers helps prevent forged requests and allows you to securely process link activity, coupon events, and stamp rewards in real time.
This guide walks through the purpose of each header, the HMAC signature verification workflow, and implementation examples for Java, PHP, and Node.js.
HTTP Header
Vivoldi Webhooks deliver HTTP POST requests to your registered Callback URL.
Each request includes dedicated headers containing signatures, timestamps, and event identifiers, enabling you to verify the request source and validate payload integrity.
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
- A unique ID for identifying the request. A new ID is generated for each HTTP request and can be used to track specific requests.
- X-Vivoldi-Event-Id string
- A unique ID for identifying an event. The same Event ID is maintained when an event is retried, allowing the receiving system to prevent duplicate event processing.
- X-Vivoldi-Webhook-Type string
- Default:GLOBAL
-
Enum:
GLOBALGROUP
-
Indicates the scope where the Webhook is applied.
GROUP: Used when a group Webhook is applied.
Stamp events are supported only with group Webhooks, so they are always sent withGROUP.
Link and coupon events are sent withGLOBALwhen no group Webhook is configured. - X-Vivoldi-Resource-Type string
-
Enum:
URLCOUPONSTAMP
-
The resource type associated with the event.
URL: Short URL
COUPON: Coupon
STAMP: Stamp - X-Vivoldi-Action-Type string
-
Enum:
CLICKUSEADDREMOVE
-
The type of action that triggered the event.
CLICK: Link click
USE: Coupon usage, stamp reward redemption
ADD: Stamp earned
REMOVE: Stamp removedUse together with
Resource-Typeto accurately identify the event type. - X-Vivoldi-Comp-Idx integer
- The organization identifier IDX. You can find it on the [Settings → Organization Settings] page.
- X-Vivoldi-Timestamp integer
- The timestamp when the request was created. It is provided in UNIX epoch seconds format. A time difference within ±5 minutes is recommended to account for server clock differences.
- X-Content-SHA256 string
- The SHA-256 hash value of the request payload. It can be used to verify payload integrity.
- X-Vivoldi-Signature string
-
Signature information used to verify the request.
Includes
t: timestamp,v1: signature value, andalg: signature algorithm.
Webhook Delivery, Responses & Retry Policies
Vivoldi Webhooks define clear rules for successful responses, automatic retries, and endpoint deactivation to ensure reliable event delivery.
Understanding these policies helps prevent duplicate processing and reduces the risk of missing events.
Success Criteria
Webhook request success is determined based on the HTTP response status code returned by the receiving server.
-
An HTTP 2xx response is considered successful.
All 2xx responses, including200,202, and204, are accepted. The response body content is not validated. -
The response timeout is 5 seconds.
After verifying the signature, we recommend immediately returning a2xxresponse and handling the actual processing asynchronously. -
HTTP redirects are not followed.
Responses such as
301and302are treated as failures, so you must register the final Callback URL.
Retry & Deactivation
When delivery fails, Webhook automatically performs retries. If repeated failures occur, the Webhook status is changed to System disabled to prevent unnecessary repeated delivery attempts.
-
Retries are performed for all HTTP response codes.
Responses such as
400,404, and401follow the same retry policy. -
During retries,
X-Vivoldi-Event-Idremains unchanged. The receiving server should use this value to prevent duplicate event processing. - Even after 5 failed retry attempts, the Webhook is not disabled immediately. An email notification is sent first, followed by a 60-minute grace period. If recovery does not occur during this period, the Webhook status is changed to System disabled.
Webhooks with the System disabled status can be found using the System disabled filter in the dashboard list and re-enabled.
| Stage | Timing | Action |
|---|---|---|
| Attempts 1–3 | Immediately · After 1 sec · After 2 sec | Retries immediately to handle temporary network errors. |
| Attempt 4 | After 10 min | Retries while considering the time required for receiving server restarts or temporary issue recovery. |
| Attempt 5 | After 30 min | Performs the final retry attempt. If it fails, automatic retries are stopped. |
| Warning email | Immediately after 5 failures | The Webhook is not disabled immediately. A 60-minute grace period begins after the 5th failure, and an email notification is sent. Depending on the notification processing cycle, the email may be delayed by up to 10 minutes. |
| Grace period | 30 min–90 min | If the server recovers within the 60-minute grace period, Webhook delivery resumes without system disablement. |
| System disabled | After 90 min | If the first delivery attempt after the grace period also fails, the Webhook status is changed to System disabled. |
If repeated failures occur from the same Callback URL, delivery is temporarily restricted to prevent requests from continuously accumulating until the receiving server recovers.
Short interruptions, such as deployments or temporary outages, are automatically resumed after recovery.
Coupon usage and stamp events are never lost.
Since these are important one-time events, they are stored in a queue during retries and the grace period, then delivered sequentially.
Link click events occur repeatedly, and analytics data is stored in Vivoldi. Therefore, these events are not stored separately or resent when Webhook delivery fails.
Webhook receiver implementation guide
-
The same event may be delivered more than once.
The same event may be delivered multiple times due to retries or network conditions. StoreX-Vivoldi-Event-Idand return200 OKwithout additional processing if the event has already been processed.
This is especially important for operations that must not be processed multiple times, such as coupon usage or stamp earning. -
Event order is not guaranteed.
A retried event may arrive after an event that occurred later.
If event ordering is required, use theregYmdtandmodYmdtvalues from the Payload as references. -
Separating response handling from actual processing is recommended.
Performing database operations or external API calls before sending a response may exceed the 5-second timeout limit.
We recommend implementing the flow as: signature verification →200 OKresponse → internal queue processing. -
Verify the signature using the original request body.
Parsing JSON and serializing it again may change the hash value due to differences in whitespace or key ordering.
If your framework automatically transforms the request body, you must separately capture the raw body. -
Ignore unknown fields.
New fields may be added to the Payload in the future. Implement your integration to ignore fields that are not recognized. -
The Secret Key depends on the Webhook target.
IfX-Vivoldi-Webhook-TypeisGLOBAL, verify the signature using the global Secret Key. If it isGROUP, verify the signature using the Secret Key configured for the corresponding group or stamp card.
Is It Safe to Process Webhooks Without Header Signature Verification?
Technically, your server can process Webhooks using only the POST body (Payload). However, in production environments, header verification should always be enforced.
Skipping header validation can expose your system to serious security risks, including forged requests, payload tampering, duplicate processing, and loss of request traceability.
Key Risks:
-
Forged Requests (Spoofing): Attackers may impersonate Vivoldi servers and send fake Webhook requests.
Without header verification, your system could mistakenly process these requests as legitimate. - Payload Tampering: If payload data is modified during network transmission, the change cannot be detected without signature validation.
- Duplicate Processing: Replay attacks may repeatedly deliver the same event, causing duplicate processing or duplicate reward issuance.
- Lack of Traceability: Without Request-Id or Event-Id headers, request tracking, debugging, and issue reproduction become significantly more difficult.
Payload
Event trigger point
Link Webhook sends event information to the configured Callback URL when a click event occurs on a shortened URL.
Webhook can be configured for individual links or link groups.
If both are configured, the link group settings take priority,
and the delivery criteria and send interval follow the group settings. The same event is not sent multiple times.
The X-Vivoldi-Action-Type value is CLICK.
Link group Webhook is a feature available only on the Enterprise plan.
You can choose either click count or visitor count as the delivery criteria, and Webhook is sent whenever the configured cumulative threshold is reached.
For example, if the delivery criteria is set to click count with a send interval of every 100 clicks, Webhook is sent whenever the cumulative click count reaches 100, 200, 300, and so on.
{
"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 identifier ID.
- domain string
- Link domain.
- compIdx integer
-
Organization IDX.
This value is the same as the
X-Vivoldi-Comp-Idxheader value. - redirectType integer
-
Enum:
200301302
-
Link redirect method.
200: Page rendering mode
301: Permanent redirect
302: Temporary redirect
For details, see the Terminology page. - url string
- Original URL.
- ttl string
- Link title.
- description string
-
Meta tag description value used when
redirectTypeis200. - metaImg string
-
Meta tag image URL used when
redirectTypeis200. - memo string
- Memo for link management.
- grpIdx integer
-
Link group IDX.
If a Webhook is configured for the link group, the group Webhook takes priority over the individual link settings. - grpNm string
- Link group name.
- strtYmdt datetime
- Link validity start date and time.
- endYmdt datetime
- Link validity expiration date and time.
- expireYn string
-
Enum:
YN
-
Indicates whether the link validity period has expired.
Returns
Ywhen the link has expired. - expireUrl string
- URL to redirect to after the link expires.
- acesCnt integer
-
Total number of clicks.
This value includes the current click event.
Webhook trigger conditions are also evaluated based on this value. For example, if the trigger interval is set to 100 clicks, the Webhook is sent whenever the total click count reaches 100, 200, 300, and so on. - pernCnt integer
- Total number of visitors (unique users). This value includes the current click event.
- acesMaxCnt integer
-
Maximum allowed click count.
If the value is
0, there is no limit. Access to the link is blocked when the limit is exceeded. - referer string
- URL of the previous page where the request originated.
- queryString string
- Query String passed when accessing the shortened URL.
- country string
- Country code of the visitor (ISO-3166).
- language string
- Language code of the visitor (ISO-639).
- regYmdt datetime
- Link creation date and time.
- modYmdt datetime
- Link modification date and time.
- payloadVersion string
- Payload specification version. Even if new fields are added, the meaning and behavior of existing fields remain unchanged until this value is updated.
Event trigger point
Coupon Webhook sends event information to the configured Callback URL when a coupon redemption event occurs.
Webhook can be configured for individual coupons or coupon groups.
If both are configured, the coupon group settings take priority,
and the same event is not sent multiple times.
Coupon group Webhook is available on the Business plan and above.
The event is sent immediately after coupon redemption is processed, and the X-Vivoldi-Action-Type value is USE.
The event is sent in the same way regardless of whether the coupon is used through the dashboard, API, or offline processing.
When the rate limit is exceeded or a retry is pending, events are stored in a queue and delivered in order.
When multiple coupons are processed at once through the API, event delivery may occur sequentially over multiple operations.
{
"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 page domain.
- nm string
- Coupon name.
- grpIdx integer
-
IDX of the coupon group this coupon belongs to.
Returns
0if the coupon does not belong to any group.
When a group Webhook is configured, group settings take priority, and theX-Vivoldi-Webhook-Typevalue is sent asGROUP.
If no group Webhook is configured, the event is sent according to the individual coupon settings. - grpNm string
- Coupon group name.
- discTypeIdx integer
-
Enum:
457458
-
Discount type.
457: Percentage discount (%)
458: Fixed amount discount - discCurrency string
- Default:KRW
-
Enum:
KRWCADCNYEURGBPIDRJPYMURRUBSGDUSD
-
Currency unit for the discount amount.
Required when using fixed amount discounts (
discTypeIdx=458). - formatDiscCurrency string
- Currency display format.
- disc double
- Default:0
-
Discount value.
Percentage discounts (457) must be between1~100%, while fixed amount discounts (458) represent the discount amount. - strtYmd date
- Coupon validity start date.
- endYmd date
- Coupon expiration date.
- useLimit integer
- Default:1
-
Enum:
012345
-
Number of times the coupon can be used.
0: Unlimited
1~5: Available for the configured number of uses - imgUrl string
- Coupon image URL.
- onsiteYn string
- Default:N
-
Enum:
YN
-
Whether in-store coupon redemption is supported.
When the value is
Y, aRedeem Couponbutton is displayed on the coupon page, and the coupon can be redeemed at an offline store after employee verification. - onsitePwd string
-
Password used to verify in-store coupon redemption.
Since it is included as plain text in thePayload, do not store it in receiving server logs. - memo string
- Internal note.
- url string
-
When configured, a
Go to Coupon Redemptionbutton is displayed on the coupon page.
Users are redirected to this URL when they click the button or coupon image. - userId string
-
ID used to identify the coupon user.
Required when the coupon usage limit is set between2~5. Typically, a service member ID or customer identifier is used. - userNm string
- Coupon user name. Used for internal management and identification purposes.
- userPhnno string
- Coupon user contact information. Used for internal management and identification purposes.
- userEml string
- Coupon user email address. Used for internal management and identification purposes.
- userEtc1 string
- Additional field for internal management.
- userEtc2 string
- Additional field for internal management.
- useCnt integer
-
Current coupon usage count.
The current redemption event is not included in this value yet.
To include the current redemption, calculate it asuseCnt + 1. - regYmdt datetime
- Coupon creation date and time. Example: 2025-07-21 11:50:20
- payloadVersion string
- Payload specification version. Even if new fields are added, the meaning and behavior of existing fields remain unchanged until this value is updated.
Event trigger point
Webhook is configured in the stamp card. All stamp events generated from the card are delivered.
Events are sent when stamps are added, removed, or rewards are redeemed.
The event type is identified using the X-Vivoldi-Action-Type header value.
ADD— Stamp addedREMOVE— Stamp removedUSE— Stamp reward redeemed
Regardless of whether the change is made through the dashboard, API, stamp management screen, or another method, the event is delivered with the same event type.
changedStamps represents the number of stamps affected.
Whether stamps were added or removed is determined by the X-Vivoldi-Action-Type value.
Reward redemption (USE) does not change the stamp count, so 0 is sent.
stamps value depends on how the event is generated.For stamp add, remove, and reward redemption events through the API,
stamps represents the stamp count before the change.
The value after the change can be calculated as stamps + changedStamps.
For REMOVE, subtract changedStamps instead.When the change is made from the dashboard stamp management screen,
stamps represents the stamp count after the change.To calculate the current stamp count accurately, use the value before the event and
changedStamps to calculate the value after the change.
{
"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
- Stamp identifier IDX.
- domain string
- Stamp page domain.
- cardIdx integer
- Stamp card identifier IDX.
- cardNm string
- Stamp card name.
- cardTtl string
- Stamp card title.
- stamps integer
-
Current stamp count. However, the reference point depends on how the event was triggered.
For stamp add, remove, and reward redemption events through the API, this value represents the stamp count before the change. The value after the change can be calculated usingstampsandchangedStamps.
(ADD: Stamp added,REMOVE: Stamp removed)
When the value is changed directly from the dashboard stamp management screen, this value represents the stamp count after the change. - maxStamps integer
- Maximum number of stamps available on the stamp card.
- changedStamps integer
-
Number of stamps changed by this event.
Whether stamps were added or removed is determined by the
X-Vivoldi-Action-Typevalue.
Reward redemption (USE) does not change the stamp count, so the value is0. - stampUrl string
- Stamp page URL.
- url string
- URL to navigate to when a button is clicked on the stamp page.
- strtYmd date
- Stamp validity start date.
- endYmd date
- Stamp validity expiration date.
- onsiteYn string
-
Enum:
YN
-
Whether in-store stamp collection is supported.
When the value is
Y, employees can verify customers and add stamps at the store. - onsitePwd string
-
Password used to verify in-store stamp collection or reward redemption.
Required for related API requests when in-store stamp collection is enabled (onsiteYn=Y). - memo string
- Internal note for reference.
- activeYn string
-
Enum:
YN
- Whether the stamp card is active. When disabled, customers cannot use the stamp card.
- userId string
-
User ID used to identify the stamp user.
Typically, a service member ID or customer identifier is used.
If not provided, Vivoldi automatically generates one. - userNm string
- Stamp user name. Used for internal management and identification purposes.
- userPhnno string
- Stamp user contact information. Used for internal management and identification purposes.
- userEml string
- Stamp user email address. Used for internal management and identification purposes.
- userEtc1 string
- Additional internal management field.
- userEtc2 string
- Additional internal management field.
- stampImgUrl string
- Stamp image URL.
- regYmdt datetime
- Stamp creation date and time. Example: 2025-07-21 11:50:20
- payloadVersion string
- Payload specification version. Even if new fields are added, the meaning and behavior of existing fields remain unchanged until this value is updated.
Webhook Signature Verification & Code Examples
The authenticity of a Webhook request is verified using the X-Vivoldi-Signature header and your issued 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 into a dot-separated string (.), then applying HMAC-SHA256 with the Secret Key.
timestamp.eventId.payloadSha256
If the generated hash value (v1) matches the value of the X-Vivoldi-Signature header, the request should be treated as valid.
If the values do not match, reject the request immediately and record the incident in your 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}`);
});
✨ Enterprise-grade Real-time Integration
Optimized for enterprise environments that handle large-scale link, coupon, and stamp event processing.
Built on high-availability infrastructure and reliable queueing systems, Vivoldi delivers stable integrations with your CRM, payment, and analytics platforms without event loss, even during sudden traffic spikes.