Webhooks Verfügbar (v1)
Mit Webhooks pusht Visz Events in deine eigenen Systeme. Wenn in deinem
Workspace etwas Nennenswertes passiert — ein Frustrations-Spike, ein
JavaScript-Error-Spike, ein Conversion-Drop — sendet Visz einen signierten
POST an eine URL, die du konfigurierst.
Webhooks richtest du unter Settings → Integrations ein: Endpoint-URL hinzufügen, Topics auswählen, die er empfangen soll — fertig. GA4, GTM und Slack werden dort ebenfalls konfiguriert, laufen aber über OAuth und brauchen keinen Code auf deiner Seite — diese Seite behandelt nur den signierten ausgehenden Webhook.
Zustellmodell
- Methode:
POSTmit JSON-Body an deine konfigurierte URL. - Signatur: Jeder Request trägt einen
X-Visz-Signature-Header (HMAC) — prüfe ihn, bevor du dem Payload vertraust. - Retries: Fehlgeschlagene Zustellungen (non-2xx oder Timeout) werden mit Backoff wiederholt. Jeder Versuch wird geloggt; du kannst das Delivery-Log einsehen und eine Zustellung aus dem Dashboard erneut abspielen.
- Idempotenz: Wegen der Retries kann dasselbe Event mehr als einmal
ankommen. Nutze die
iddes Events, um auf deiner Seite zu deduplizieren. - Datenminimierung: Payloads enthalten nur pseudonyme Identifier (z. B.
eine
session_id), nie rohe PII.
Die Signatur prüfen
Visz signiert Webhooks Stripe-artig für Replay-Resistenz. Der
X-Visz-Signature-Header enthält einen Timestamp und die Signatur:
X-Visz-Signature: t=1781804392,v1=3f8a…<hex>v1 ist HMAC-SHA256(secret, "{t}.{rawBody}") — der signierte Payload ist der
Unix-Sekunden-Timestamp t, ein Punkt, dann der rohe Request-Body. Zum
Prüfen: t und v1 parsen, den HMAC über `${t}.${rawBody}` neu
berechnen, in konstanter Zeit vergleichen und den Request ablehnen, wenn es
nicht passt (optional zusätzlich Timestamps älter als ~5 Minuten ablehnen, um
Replays zu stoppen).
import { createHmac, timingSafeEqual } from "node:crypto";
// Express example — make sure you have the RAW body, not the parsed JSON.
function verifyViszWebhook(rawBody, header, signingSecret, toleranceSec = 300) {
// header looks like: t=1781804392,v1=<hex>
const parts = Object.fromEntries(
String(header ?? "")
.split(",")
.map((p) => p.split("=").map((s) => s.trim()))
);
const t = parts.t;
const v1 = parts.v1;
if (!/^\d+$/.test(t ?? "") || !v1) return false;
// Optional replay guard: reject signatures older than the tolerance window.
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
const expected = createHmac("sha256", signingSecret)
.update(`${t}.${rawBody}`, "utf8") // signed payload = "{timestamp}.{rawBody}"
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(v1, "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}
app.post("/visz-webhook", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyViszWebhook(
req.body.toString("utf8"), // raw body as received
req.header("X-Visz-Signature"),
process.env.VISZ_WEBHOOK_SECRET
);
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// … handle event.topic … (dedupe on event.id)
res.status(200).end(); // ack quickly; do the work async
});Signiere
t.rawBody, nicht den Body allein. Der Timestamp ist Teil des HMAC-Inputs und kann daher nicht manipuliert werden. Das erneute Serialisieren des geparsten JSON kann Whitespace/Key-Reihenfolge verändern und die Signatur brechen — nutze immer exakt die Bytes, die du empfangen hast.
Payload-Envelope
Jeder Webhook teilt dieselbe äußere Struktur; topic sagt dir, was passiert
ist, und data trägt die Topic-spezifischen Felder.
{
"id": "whd_3f2504e0",
"topic": "frustration_spike",
"created_at": "2026-06-05T10:00:00.000Z",
"workspace_id": "ws_1a2b",
"site_id": "site_9z8y",
"data": { }
}| Feld | Beschreibung |
|---|---|
id | Eindeutige Delivery-ID — nutze sie zum Deduplizieren. |
topic | Eines der Topics unten. |
created_at | Wann das Event erzeugt wurde (ISO 8601). |
workspace_id / site_id | Pseudonyme Identifier der Quelle. |
data | Topic-spezifischer Payload. |
Topics
Du wählst, welche Topics ein Endpoint abonniert.
frustration_spike
Eine Häufung von Frustrations-Signalen (Rage-Clicks, Dead-Clicks, Thrashing) auf einer Seite.
{
"topic": "frustration_spike",
"data": {
"page_url": "https://shop.example.com/checkout",
"signal": "rage_click",
"count": 42,
"window_minutes": 15,
"session_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
}
}error_spike
Ein Ausbruch von JavaScript-Fehlern über der Baseline.
{
"topic": "error_spike",
"data": {
"page_url": "https://shop.example.com/checkout",
"message": "TypeError: Cannot read properties of undefined",
"count": 88,
"window_minutes": 15
}
}conversion_drop
Die Conversion-Rate eines Funnels ist gegenüber ihrer Baseline deutlich gefallen.
{
"topic": "conversion_drop",
"data": {
"funnel_id": "fn_7c4a",
"funnel_name": "Checkout",
"previous_rate": 0.041,
"current_rate": 0.018,
"window_minutes": 60
}
}blocked_purchase
Ein Besucher ist während einer kritischen Kauf-Aktion auf einen Fehler oder einen harten Blocker gestoßen.
{
"topic": "blocked_purchase",
"data": {
"page_url": "https://shop.example.com/checkout",
"reason": "js_error_during_critical_action",
"session_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
}
}mention
Du (oder ein Teammitglied) wurdest per @-Mention auf einer Aufnahme, einer Heatmap oder einer Notiz erwähnt.
{
"topic": "mention",
"data": {
"subject_type": "recording",
"subject_id": "rec_2f5a",
"mentioned_by": "member_4d7e",
"comment_excerpt": "take a look at this drop-off"
}
}weekly_summary
Ein geplanter Digest der wichtigsten Metriken der vergangenen Woche.
{
"topic": "weekly_summary",
"data": {
"period_start": "2026-05-29",
"period_end": "2026-06-05",
"sessions": 12840,
"recordings": 9210,
"top_frustration_page": "https://shop.example.com/checkout"
}
}Die
data-Beispiele oben sind repräsentative Strukturen; die Verfügbarkeit einzelner Felder kann je Event variieren. Verzweige immer übertopic, behandle unbekannte Felder als optional und bestätige schnell mit2xx— schwere Arbeit erledigst du asynchron.
Zuletzt aktualisiert: