Vivoldi Webhook API & HMAC-Signaturprüfung
Eine sichere Webhook-Integration beginnt mit der Signaturprüfung über HTTP-Header.
Jede Vivoldi Webhook-Anfrage enthält Header wie X-Vivoldi-Request-Id, X-Vivoldi-Event-Id, X-Vivoldi-Signature.
Durch die Validierung dieser Header lassen sich manipulierte Anfragen erkennen und Link-, Coupon- sowie Stempel-Events sicher verarbeiten.
Diese Anleitung erklärt Schritt für Schritt die Funktion der einzelnen Header, den Ablauf der HMAC-Signaturprüfung sowie Implementierungsbeispiele für Java, PHP und Node.js.
HTTP Header
Vivoldi Webhooks senden HTTP-POST-Anfragen an die registrierte Callback-URL.
Jede Anfrage enthält spezielle Header mit Signaturen, Zeitstempeln und Event-IDs, sodass sich die Herkunft der Anfrage und die Integrität der Payload zuverlässig überprüfen lassen.
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
- Eine eindeutige ID zur Identifizierung der Anfrage. Für jede HTTP-Anfrage wird eine neue ID erstellt, die zur Nachverfolgung bestimmter Anfragen verwendet werden kann.
- X-Vivoldi-Event-Id string
- Eine eindeutige ID zur Identifizierung eines Ereignisses. Auch bei erneuten Zustellversuchen bleibt dieselbe Event-ID erhalten, sodass das empfangende System doppelte Ereignisverarbeitung verhindern kann.
- X-Vivoldi-Webhook-Type string
- Default:GLOBAL
-
Enum:
GLOBALGROUP
-
Gibt den Anwendungsbereich des Webhooks an.
GROUP: Wird verwendet, wenn ein Gruppen-Webhook angewendet wird.
Stempelereignisse unterstützen nur Gruppen-Webhooks und werden daher immer alsGROUPgesendet.
Link- und Coupon-Ereignisse werden alsGLOBALgesendet, wenn kein Gruppen-Webhook konfiguriert ist. - X-Vivoldi-Resource-Type string
-
Enum:
URLCOUPONSTAMP
-
Der Ressourcentyp, der mit dem Ereignis verbunden ist.
URL: Kurz-URL
COUPON: Coupon
STAMP: Stempel - X-Vivoldi-Action-Type string
-
Enum:
CLICKUSEADDREMOVE
-
Der Aktionstyp, der das Ereignis ausgelöst hat.
CLICK: Link-Klick
USE: Coupon-Nutzung, Einlösen einer Stempelbelohnung
ADD: Stempel hinzugefügt
REMOVE: Stempel entferntVerwenden Sie diesen Wert zusammen mit
Resource-Type, um den Ereignistyp eindeutig zu bestimmen. - X-Vivoldi-Comp-Idx integer
- Die IDX zur Identifizierung der Organisation. Sie finden sie auf der Seite [Einstellungen → Organisationseinstellungen].
- X-Vivoldi-Timestamp integer
- Der Zeitpunkt, zu dem die Anfrage erstellt wurde. Die Übertragung erfolgt im Format UNIX epoch seconds. Um Zeitunterschiede zwischen Servern zu berücksichtigen, wird eine Abweichung von ±5 Minuten empfohlen.
- X-Content-SHA256 string
- Der SHA-256-Hashwert des Request-Payloads. Er kann zur Überprüfung der Payload-Integrität verwendet werden.
- X-Vivoldi-Signature string
-
Signaturinformationen zur Überprüfung der Anfrage.
Enthält
t: Zeitstempel,v1: Signaturwert, undalg: Signaturalgorithmus.
Webhook-Zustellung, Antworten & Retry-Richtlinien
Vivoldi Webhooks definieren klare Regeln für erfolgreiche Antworten, automatische Wiederholungsversuche und die Deaktivierung von Endpoints, um eine zuverlässige Event-Zustellung sicherzustellen.
Das Verständnis dieser Richtlinien hilft dabei, doppelte Verarbeitungen und den Verlust von Events zu vermeiden.
Erfolgskriterien
Der Erfolg einer Webhook-Anfrage wird anhand des HTTP-Statuscodes bestimmt, den der Empfangsserver zurückgibt.
-
Eine HTTP-Antwort mit 2xx wird als erfolgreich betrachtet.
Alle 2xx-Antworten wie200,202und204werden akzeptiert. Der Inhalt des Response-Bodys wird nicht geprüft. -
Die Antwortzeitüberschreitung beträgt 5 Sekunden.
Nach der Signaturprüfung empfehlen wir, sofort eine2xx-Antwort zurückzugeben und die eigentliche Verarbeitung asynchron auszuführen. -
HTTP-Weiterleitungen werden nicht verfolgt.
Antworten wie
301und302werden als Fehler behandelt. Daher muss die endgültige Callback-URL registriert werden.
Wiederholungsversuche & Deaktivierung
Bei einem Übertragungsfehler führt der Webhook automatisch Wiederholungsversuche durch. Wenn wiederholt Fehler auftreten, wird der Status des Webhooks auf Vom System deaktiviert geändert, um unnötige erneute Zustellversuche zu verhindern.
-
Wiederholungsversuche werden für alle HTTP-Statuscodes durchgeführt.
Antworten wie
400,404und401unterliegen derselben Wiederholungsrichtlinie. -
Während der Wiederholungsversuche bleibt
X-Vivoldi-Event-Idunverändert. Der Empfangsserver sollte diesen Wert verwenden, um doppelte Event-Verarbeitung zu verhindern. - Auch nach 5 fehlgeschlagenen Wiederholungsversuchen wird der Webhook nicht sofort deaktiviert. Zunächst wird eine E-Mail-Benachrichtigung gesendet und eine 60-minütige Karenzzeit gewährt. Wenn keine Wiederherstellung innerhalb dieses Zeitraums erfolgt, wird der Webhook-Status auf Vom System deaktiviert gesetzt.
Webhooks mit dem Status Vom System deaktiviert können über den Filter Vom System deaktiviert in der Dashboard-Liste gefunden und wieder aktiviert werden.
| Phase | Zeitpunkt | Aktion |
|---|---|---|
| Versuche 1–3 | Sofort · Nach 1 Sek. · Nach 2 Sek. | Es werden sofortige Wiederholungsversuche durchgeführt, um vorübergehende Netzwerkfehler zu behandeln. |
| 4. Versuch | Nach 10 Min. | Ein weiterer Versuch wird durchgeführt, wobei die Zeit für Neustarts des Empfangsservers oder die Behebung vorübergehender Störungen berücksichtigt wird. |
| 5. Versuch | Nach 30 Min. | Der letzte Wiederholungsversuch wird durchgeführt. Bei einem Fehler werden automatische Wiederholungsversuche beendet. |
| Warn-E-Mail | Direkt nach 5 Fehlern | Der Webhook wird nicht sofort deaktiviert. Nach dem fünften Fehler beginnt eine 60-minütige Karenzzeit und eine E-Mail-Benachrichtigung wird gesendet. Abhängig vom Benachrichtigungsprozess kann die E-Mail um bis zu etwa 10 Minuten verzögert werden. |
| Karenzzeit | 30 Min.–90 Min. | Wenn der Server innerhalb der 60-minütigen Karenzzeit wiederhergestellt wird, wird die Webhook-Zustellung ohne Systemdeaktivierung fortgesetzt. |
| Vom System deaktiviert | Nach 90 Min. | Wenn der erste Zustellversuch nach Ablauf der Karenzzeit ebenfalls fehlschlägt, wird der Webhook-Status auf Vom System deaktiviert geändert. |
Wenn wiederholt Fehler bei derselben Callback-URL auftreten, wird die Zustellung vorübergehend eingeschränkt, damit sich Anfragen bis zur Wiederherstellung des Empfangsservers nicht weiter ansammeln.
Kurze Unterbrechungen wie Deployments oder vorübergehende Störungen werden nach der Wiederherstellung automatisch fortgesetzt.
Coupon-Nutzungs- und Stempelereignisse gehen niemals verloren.
Da es sich um wichtige einmalige Ereignisse handelt, werden sie während der Wiederholungsversuche und der Karenzzeit in einer Warteschlange gespeichert und anschließend der Reihe nach übertragen.
Link-Klickereignisse treten wiederholt auf und Analysedaten werden in Vivoldi gespeichert. Daher werden diese Ereignisse bei einem fehlgeschlagenen Webhook-Versand nicht separat gespeichert oder erneut gesendet.
Implementierungsleitfaden für den Webhook-Empfangsserver
-
Dasselbe Ereignis kann mehrfach zugestellt werden.
Aufgrund von Wiederholungsversuchen oder Netzwerkbedingungen kann dasselbe Ereignis mehrfach empfangen werden. Speichern SieX-Vivoldi-Event-Idund geben Sie200 OKzurück, ohne eine weitere Verarbeitung auszuführen, wenn das Ereignis bereits verarbeitet wurde.
Dies ist besonders wichtig für Vorgänge, die nicht doppelt ausgeführt werden dürfen, wie die Verwendung von Coupons oder das Hinzufügen von Stempeln. -
Die Reihenfolge der Events ist nicht garantiert.
Ein erneut gesendetes Event kann nach einem später erzeugten Event eintreffen.
Wenn eine Verarbeitung in Reihenfolge erforderlich ist, verwenden Sie die WerteregYmdtundmodYmdtaus dem Payload als Referenz. -
Es wird empfohlen, die Antwortverarbeitung vom eigentlichen Prozess zu trennen.
Datenbankoperationen oder externe API-Aufrufe vor dem Senden der Antwort können das Zeitlimit von 5 Sekunden überschreiten.
Empfohlen wird folgender Ablauf: Signaturprüfung →200 OK-Antwort → Verarbeitung über eine interne Queue. -
Prüfen Sie die Signatur mit dem ursprünglichen Request Body.
Das Parsen von JSON und eine erneute Serialisierung können den Hash-Wert durch Änderungen an Leerzeichen oder Schlüsselreihenfolge verändern.
Wenn Ihr Framework den Request Body automatisch verarbeitet, erfassen Sie den Raw Body separat. -
Ignorieren Sie unbekannte Felder.
Dem Payload können zukünftig neue Felder hinzugefügt werden. Stellen Sie sicher, dass nicht bekannte Felder ignoriert werden. -
Der Secret Key hängt vom Webhook-Ziel ab.
WennX-Vivoldi-Webhook-TypeGLOBAList, prüfen Sie die Signatur mit dem globalen Secret Key. BeiGROUPverwenden Sie den Secret Key, der für die entsprechende Gruppe oder Stempelkarte konfiguriert wurde.
Ist die Verarbeitung von Webhooks ohne Header-Signaturprüfung sicher?
Technisch gesehen können Webhooks auch nur anhand des POST-Body (Payload) verarbeitet werden. In Produktionsumgebungen sollte jedoch immer eine Header-Validierung durchgeführt werden.
Wird die Header-Prüfung ausgelassen, kann dies zu schwerwiegenden Sicherheitsrisiken wie gefälschten Anfragen, manipulierten Payloads, doppelter Verarbeitung und fehlender Nachvollziehbarkeit führen.
Wichtige Risiken:
-
Gefälschte Anfragen (Spoofing): Angreifer können sich als Vivoldi-Server ausgeben und gefälschte Webhook-Anfragen senden.
Ohne Header-Validierung könnte das System diese irrtümlich als legitime Anfragen behandeln. - Datenmanipulation: Wird die Payload während der Übertragung verändert, kann dies ohne Signaturprüfung nicht erkannt werden.
- Doppelte Verarbeitung: Replay-Angriffe können dazu führen, dass dasselbe Event mehrfach empfangen wird, was doppelte Verarbeitung oder doppelte Gutschriften verursachen kann.
- Fehlende Nachvollziehbarkeit: Ohne Request-Id- oder Event-Id-Header werden Request-Tracking, Fehleranalyse und Problemreproduktion deutlich erschwert.
Payload
Zeitpunkt des Ereignisses
Link-Webhooks senden Ereignisinformationen an die konfigurierte Callback-URL, sobald ein Klickereignis auf einer Kurz-URL auftritt.
Webhooks können für einzelne Links oder Linkgruppen konfiguriert werden.
Wenn beide konfiguriert sind, haben die Einstellungen der Linkgruppe Vorrang,
und die Versandbedingungen sowie das Versandintervall richten sich nach den Gruppeneinstellungen. Dasselbe Ereignis wird nicht mehrfach gesendet.
Der Wert von X-Vivoldi-Action-Type ist CLICK.
Linkgruppen-Webhooks sind eine exklusive Funktion des Enterprise-Tarifs.
Als Versandbedingung kann entweder die Anzahl der Klicks oder die Anzahl der Besucher ausgewählt werden. Der Webhook wird jedes Mal gesendet, wenn der festgelegte kumulative Wert erreicht wird.
Wenn beispielsweise die Versandbedingung auf Klickanzahl und das Versandintervall auf jeweils 100 Klicks festgelegt ist, wird der Webhook gesendet, sobald die kumulative Klickanzahl 100, 200, 300 usw. erreicht.
{
"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-Identifikations-ID.
- domain string
- Link-Domain.
- compIdx integer
-
Organisations-IDX.
Dieser Wert entspricht dem Wert des Headers
X-Vivoldi-Comp-Idx. - redirectType integer
-
Enum:
200301302
-
Weiterleitungsmethode des Links.
200: Seitenanzeige-Modus
301: Permanente Weiterleitung
302: Temporäre Weiterleitung
Weitere Informationen finden Sie auf der Seite Terminologie. - url string
- Ursprüngliche URL.
- ttl string
- Link-Titel.
- description string
-
Wert des Meta-Tags description, der verwendet wird, wenn
redirectTypeden Wert200hat. - metaImg string
-
URL des Meta-Tag-Bildes, das verwendet wird, wenn
redirectTypeden Wert200hat. - memo string
- Notiz zur Link-Verwaltung.
- grpIdx integer
-
IDX der Link-Gruppe.
Wenn für die Link-Gruppe ein Webhook konfiguriert ist, hat der Gruppen-Webhook Vorrang vor der individuellen Link-Konfiguration. - grpNm string
- Name der Link-Gruppe.
- strtYmdt datetime
- Startdatum und Uhrzeit der Link-Gültigkeit.
- endYmdt datetime
- Ablaufdatum und Uhrzeit der Link-Gültigkeit.
- expireYn string
-
Enum:
YN
-
Gibt an, ob die Gültigkeitsdauer des Links abgelaufen ist.
Bei einem abgelaufenen Link wird der Wert
Ygesendet. - expireUrl string
- URL, zu der nach Ablauf des Links weitergeleitet wird.
- acesCnt integer
-
Gesamte Anzahl der Klicks.
Dieser Wert enthält das aktuelle Klickereignis.
Die Webhook-Auslösebedingung wird ebenfalls anhand dieses Werts geprüft. Wenn beispielsweise das Sendeintervall auf 100 Klicks eingestellt ist, wird der Webhook gesendet, sobald die kumulative Klickzahl 100, 200, 300 usw. erreicht. - pernCnt integer
- Gesamte Anzahl der Besucher (eindeutige Benutzer). Dieser Wert enthält das aktuelle Klickereignis.
- acesMaxCnt integer
-
Maximal zulässige Anzahl von Klicks.
Bei
0gibt es keine Begrenzung. Wird das Limit überschritten, wird der Zugriff auf den Link blockiert. - referer string
- URL der vorherigen Seite, von der die Anfrage stammt.
- queryString string
- Query String, der beim Zugriff auf die Kurz-URL übergeben wurde.
- country string
- Ländercode des zugreifenden Benutzers (ISO-3166).
- language string
- Sprachcode des zugreifenden Benutzers (ISO-639).
- regYmdt datetime
- Datum und Uhrzeit der Link-Erstellung.
- modYmdt datetime
- Datum und Uhrzeit der Link-Änderung.
- payloadVersion string
- Version der Payload-Spezifikation. Auch wenn neue Felder hinzugefügt werden, bleiben Bedeutung und Verhalten bestehender Felder unverändert, bis dieser Wert aktualisiert wird.
Zeitpunkt des Ereignisses
Coupon-Webhooks senden Ereignisinformationen an die konfigurierte Callback-URL, sobald ein Coupon-Nutzungsereignis auftritt.
Webhooks können für einzelne Coupons oder Coupon-Gruppen konfiguriert werden.
Wenn beide konfiguriert sind, haben die Einstellungen der Coupon-Gruppe Vorrang,
und dasselbe Ereignis wird nicht mehrfach gesendet.
Coupon-Gruppen-Webhooks sind ab dem Business-Tarif oder höher verfügbar.
Das Ereignis wird unmittelbar nach Abschluss der Coupon-Nutzung gesendet, und der Wert von X-Vivoldi-Action-Type ist USE.
Unabhängig davon, ob der Coupon über das Dashboard, die API oder eine Offline-Verarbeitung verwendet wird, erfolgt die Übertragung auf dieselbe Weise.
Wenn das Aufruflimit überschritten wurde oder ein Wiederholungsversuch aussteht, werden die Ereignisse in einer Warteschlange gespeichert und der Reihe nach zugestellt.
Wenn mehrere Coupons gleichzeitig über die API verarbeitet werden, kann die Ereigniszustellung schrittweise über mehrere Vorgänge erfolgen.
{
"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
- Gutscheinnummer.
- domain string
- Domain der Gutscheinseite.
- nm string
- Gutscheinname.
- grpIdx integer
-
IDX der Gruppe, zu der der Gutschein gehört.
Wenn der Gutschein keiner Gruppe zugeordnet ist, lautet der Wert
0.
Wenn ein Gruppen-Webhook konfiguriert ist, hat die Gruppeneinstellung Vorrang und der Wert vonX-Vivoldi-Webhook-Typewird alsGROUPgesendet.
Wenn kein Gruppen-Webhook konfiguriert ist, erfolgt die Übertragung entsprechend der individuellen Gutscheineinstellungen. - grpNm string
- Name der Gutschein-Gruppe.
- discTypeIdx integer
-
Enum:
457458
-
Rabatttyp.
457: Prozentualer Rabatt (%)
458: Fester Rabattbetrag - discCurrency string
- Default:KRW
-
Enum:
KRWCADCNYEURGBPIDRJPYMURRUBSGDUSD
-
Währungseinheit für den Rabattbetrag.
Bei Verwendung eines festen Rabattbetrags (
discTypeIdx=458) ist dieses Feld erforderlich. - formatDiscCurrency string
- Format zur Anzeige der Währung.
- disc double
- Default:0
-
Rabattwert.
Prozentuale Rabatte (457) liegen im Bereich von1~100%, während feste Rabattbeträge (458) den abzuziehenden Betrag darstellen. - strtYmd date
- Startdatum der Gutschein-Gültigkeit.
- endYmd date
- Ablaufdatum der Gutschein-Gültigkeit.
- useLimit integer
- Default:1
-
Enum:
012345
-
Anzahl der zulässigen Gutscheinverwendungen.
0: Unbegrenzt
1~5: Nutzung entsprechend der festgelegten Anzahl möglich - imgUrl string
- URL des Gutscheinbildes.
- onsiteYn string
- Default:N
-
Enum:
YN
-
Gibt an, ob die Nutzung des Gutscheins vor Ort unterstützt wird.
Bei
Ywird auf der Gutscheinseite die SchaltflächeGutschein verwendenangezeigt, und der Gutschein kann nach Bestätigung durch Mitarbeiter in einem Geschäft eingelöst werden. - onsitePwd string
-
Passwort zur Authentifizierung der Gutscheinverwendung vor Ort.
Da es imPayloadim Klartext enthalten ist, darf es nicht in den Logs des empfangenden Servers gespeichert werden. - memo string
- Interne Notiz.
- url string
-
Wenn festgelegt, wird auf der Gutscheinseite die Schaltfläche
Gutschein verwendenangezeigt.
Beim Klicken auf die Schaltfläche oder das Gutscheinbild wird zu dieser URL weitergeleitet. - userId string
-
ID zur Identifizierung des Gutscheinbenutzers.
Erforderlich, wenn das Nutzungslimit des Gutscheins auf2~5festgelegt ist. Normalerweise wird eine Service-Mitglieds-ID oder ein Kundenidentifikator verwendet. - userNm string
- Name des Gutscheinbenutzers. Wird für die interne Verwaltung und Identifizierung verwendet.
- userPhnno string
- Kontaktdaten des Gutscheinbenutzers. Wird für die interne Verwaltung und Identifizierung verwendet.
- userEml string
- E-Mail-Adresse des Gutscheinbenutzers. Wird für die interne Verwaltung und Identifizierung verwendet.
- userEtc1 string
- Zusätzliches Feld für die interne Verwaltung.
- userEtc2 string
- Zusätzliches Feld für die interne Verwaltung.
- useCnt integer
-
Aktuelle Anzahl der Gutscheinverwendungen.
Das aktuelle Nutzungsereignis ist in diesem Wert noch nicht enthalten.
Wenn die Anzahl einschließlich der aktuellen Verwendung benötigt wird, berechnen Sie sie mituseCnt + 1. - regYmdt datetime
- Datum und Uhrzeit der Gutscheinerstellung. Beispiel: 2025-07-21 11:50:20
- payloadVersion string
- Version der Payload-Spezifikation. Auch wenn neue Felder hinzugefügt werden, bleiben Bedeutung und Verhalten bestehender Felder unverändert, bis dieser Wert geändert wird.
Zeitpunkt des Ereignisses
Webhooks werden in der Stempelkarte konfiguriert. Alle Stempelereignisse, die über diese Karte ausgelöst werden, werden gesendet.
Die Übertragung erfolgt bei Ereignissen zum Hinzufügen, Entfernen oder Einlösen von Stempelbelohnungen.
Der Ereignistyp wird anhand des Werts im Header X-Vivoldi-Action-Type bestimmt.
ADD— Stempel hinzugefügtREMOVE— Stempel entferntUSE— Stempelbelohnung eingelöst
Unabhängig davon, ob die Änderung über das Dashboard, die API, die Stempelverwaltung oder eine andere Methode erfolgt, wird derselbe Ereignistyp gesendet.
changedStamps gibt die Anzahl der betroffenen Stempel an.
Ob Stempel hinzugefügt oder entfernt wurden, wird anhand des Werts von X-Vivoldi-Action-Type bestimmt.
Beim Einlösen einer Belohnung (USE) ändert sich die Stempelanzahl nicht, daher wird 0 übertragen.
stamps hängt davon ab, wie das Ereignis ausgelöst wurde.Bei Stempel-Hinzufügung, Entfernung und Belohnungseinlösung über die API stellt
stamps die Stempelanzahl vor der Änderung dar.
Der Wert nach der Änderung kann mit stamps + changedStamps berechnet werden.
Bei REMOVE wird changedStamps abgezogen.Wenn die Änderung über die Stempelverwaltung im Dashboard erfolgt, stellt
stamps die Stempelanzahl nach der Änderung dar.Um die aktuelle Stempelanzahl korrekt zu berechnen, verwenden Sie den Wert vor dem Ereignis und
changedStamps, um den Wert nach der Änderung zu bestimmen.
{
"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 zur Identifizierung des Stempels.
- domain string
- Domain der Stempelseite.
- cardIdx integer
- IDX zur Identifizierung der Stempelkarte.
- cardNm string
- Name der Stempelkarte.
- cardTtl string
- Titel der Stempelkarte.
- stamps integer
-
Aktuelle Anzahl der Stempel. Der Bezugszeitpunkt kann jedoch je nach Auslöser des Ereignisses unterschiedlich sein.
Bei Ereignissen zum Hinzufügen, Entfernen und Einlösen von Belohnungen über die API entspricht dieser Wert der Stempelanzahl vor der Änderung. Der Wert nach der Änderung kann mitstampsundchangedStampsberechnet werden.
(ADD: Stempel hinzugefügt,REMOVE: Stempel entfernt)
Wenn die Änderung direkt über die Stempelverwaltung im Dashboard vorgenommen wurde, entspricht dieser Wert der Stempelanzahl nach der Änderung. - maxStamps integer
- Maximale Anzahl der Stempel auf der Stempelkarte.
- changedStamps integer
-
Anzahl der durch dieses Ereignis geänderten Stempel.
Ob Stempel hinzugefügt oder entfernt wurden, wird anhand des Werts von
X-Vivoldi-Action-Typebestimmt.
Beim Einlösen einer Belohnung (USE) ändert sich die Stempelanzahl nicht, daher ist der Wert0. - stampUrl string
- URL der Stempelseite.
- url string
- URL, zu der beim Klicken auf eine Schaltfläche auf der Stempelseite weitergeleitet wird.
- strtYmd date
- Startdatum der Stempelgültigkeit.
- endYmd date
- Ablaufdatum der Stempelgültigkeit.
- onsiteYn string
-
Enum:
YN
-
Gibt an, ob das Sammeln von Stempeln vor Ort unterstützt wird.
Wenn der Wert
Yist, können Mitarbeiter Kunden verifizieren und Stempel im Geschäft hinzufügen. - onsitePwd string
-
Passwort zur Verifizierung der Stempelvergabe vor Ort oder der Nutzung von Belohnungen.
Erforderlich für entsprechende API-Aufrufe, wenn die Stempelvergabe vor Ort aktiviert ist (onsiteYn=Y). - memo string
- Interne Notiz.
- activeYn string
-
Enum:
YN
- Gibt an, ob die Stempelkarte aktiv ist. Wenn sie deaktiviert ist, können Kunden die Stempelkarte nicht verwenden.
- userId string
-
Benutzer-ID zur Identifizierung des Stempelkartenbenutzers.
Normalerweise wird eine Service-Mitglieds-ID oder ein Kundenidentifikator verwendet.
Wenn kein Wert festgelegt ist, wird dieser automatisch von Vivoldi erstellt. - userNm string
- Name des Stempelkartenbenutzers. Wird zur internen Verwaltung und Identifizierung verwendet.
- userPhnno string
- Kontaktdaten des Stempelkartenbenutzers. Wird zur internen Verwaltung und Identifizierung verwendet.
- userEml string
- E-Mail-Adresse des Stempelkartenbenutzers. Wird zur internen Verwaltung und Identifizierung verwendet.
- userEtc1 string
- Zusätzliches Feld für die interne Verwaltung.
- userEtc2 string
- Zusätzliches Feld für die interne Verwaltung.
- stampImgUrl string
- URL des Stempelbildes.
- regYmdt datetime
- Datum und Uhrzeit der Erstellung des Stempels. Beispiel: 2025-07-21 11:50:20
- payloadVersion string
- Version der Payload-Spezifikation. Auch wenn neue Felder hinzugefügt werden, bleiben Bedeutung und Verhalten bestehender Felder unverändert, bis dieser Wert aktualisiert wird.
Webhook-Signaturprüfung & Codebeispiele
Die Echtheit einer Webhook-Anfrage wird mithilfe des Headers X-Vivoldi-Signature und des ausgegebenen Secret Keys überprüft.
Die Signatur wird erzeugt, indem der Zeitstempel (t), die Event-ID (X-Vivoldi-Event-Id) und der SHA-256-Hash des Request-Bodys zu einer durch Punkte (.) getrennten Zeichenkette kombiniert und anschließend mit dem Secret Key per HMAC-SHA256 gehasht werden.
timestamp.eventId.payloadSha256
Wenn der erzeugte Hashwert (v1) mit dem Wert des Headers X-Vivoldi-Signature übereinstimmt, sollte die Anfrage als gültig verarbeitet werden.
Stimmen die Werte nicht überein, sollte die Anfrage sofort abgelehnt und im Log protokolliert werden.
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}`);
});
✨ Echtzeit-Integration auf Enterprise-Niveau
Optimiert für Enterprise-Umgebungen, die große Mengen an Link-, Coupon- und Stempel-Events verarbeiten.
Basierend auf hochverfügbarer Infrastruktur und zuverlässigen Queueing-Systemen ermöglicht Vivoldi stabile Integrationen mit CRM-, Zahlungs- und Analyseplattformen ohne Eventverlust — selbst bei plötzlichen Traffic-Spitzen.