Skip to Content
API-ReferenzWebhooks

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: POST mit 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 id des 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": { } }
FeldBeschreibung
idEindeutige Delivery-ID — nutze sie zum Deduplizieren.
topicEines der Topics unten.
created_atWann das Event erzeugt wurde (ISO 8601).
workspace_id / site_idPseudonyme Identifier der Quelle.
dataTopic-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 über topic, behandle unbekannte Felder als optional und bestätige schnell mit 2xx — schwere Arbeit erledigst du asynchron.

Zuletzt aktualisiert:

Zuletzt aktualisiert am