Diil Docs
  1. Doku
  2. Das Widget

Events und Analytics: Seitenaufrufe, eigene Events, Conversions

Aktualisiert:

Der widget.js-Tag, den du fürs Live-Editing schon eingebaut hast, zählt ganz nebenbei jeden Seitenaufruf und findet heraus, woher der Besucher kam und welche Anzeige ihn gebracht hat. Eine Zeile JavaScript dazu — oder ein Request von deinem Server —, und du siehst auch, wer etwas in den Warenkorb gelegt, wer sich registriert und wer tatsächlich bezahlt hat, samt Betrag. Kein zweites Analytics-Script, kein Tag Manager, kein neuer Cookie-Banner.

Das steht auf der Karte:

  • Seitenaufrufe — automatisch, null Code. Kanäle, UTM-Tags, Klick-IDs von Anzeigen, Standort, Geräte.
  • Eigene Events aus dem Browser — window.crmTrack('add_to_cart', …) für UX-Signale.
  • Server-Events — POST /marketing/event mit einem geheimen Schlüssel, für Käufe und alles andere, was genau einmal gezählt werden muss.

Alles landet im CRM unter Marketing: Traffic für Besuche, Ereignisse für deine eigenen Events, Werbekanäle für getaggte Links.

Was sofort funktioniert: Seitenaufrufe

Ist widget.js auf der Seite, werden Seitenaufrufe bereits gezählt. Der Tag ist derselbe wie beim Live-Editing:

<script src="https://widget.sitecog.com/widget.js" defer></script>

Bei jedem Seitenaufruf schickt das Widget einen Pageview per navigator.sendBeacon — ein winziger Fire-and-forget-Request, der die Seite nicht bremst und überlebt, wenn der Besucher den Tab schließt. Der Server antwortet mit 204 No Content; zurückzulesen gibt es nichts.

Was erfasst wird

Aus dem Browser:

  • URL, Titel und Referrer der Seite;
  • UTM-Tags: utm_source, utm_medium, utm_campaign, utm_term, utm_content;
  • Klick-IDs von Anzeigen: gclid, gbraid, wbraid (Google), yclid (Yandex), fbclid (Meta), msclkid (Microsoft);
  • der Werbelink-Code ?dl= aus Links unter Werbekanäle;
  • ein paar Werbe-Cookies, falls deine Site sie schon hat: _ga, _gcl_*, _ym_uid, _fbp, _fbc — im Consent-Modus erst nach der Einwilligung, und sendet der Browser Global Privacy Control, gar nicht;
  • Bildschirm- und Viewport-Größe, Pixel Ratio, Browsersprache und Zeitzone.

Auf unserer Seite ergänzt:

  • Standort, anhand der IP-Adresse;
  • Gerätetyp, Browser und Betriebssystem;
  • Traffic-Kanal: Bezahlt, Social, Organisch, Verweise, E-Mail oder Direkt;
  • neuer oder wiederkehrender Besucher.

Gespeichert wird dabei sparsam: Die IP-Adresse landet gekürzt in der Datenbank, und aus den URLs fliegen der Hash und sensible Query-Parameter raus — die Details stehen unter Was wir speichern und wie lange.

Bots werden nicht weggeworfen — sie werden markiert und aus den Reports herausgefiltert. Die Zahlen, die du siehst, handeln also von Menschen, und die Rohdaten sind trotzdem da, falls du dich mal fragst, wie viel deines Traffics Crawler sind (Spoiler: mehr, als dir lieb ist).

Die Reports findest du unter Marketing → Traffic: Besucher, Sessions, Kanäle, Seiten, Geografie und Geräte.

Tage und Stunden in Reports

„Gestern“ ist in einem Report gestern in der Zeitzone deiner Website – nicht in UTC und nicht in der Zone dessen, der gerade auf das Diagramm schaut. Ein Shop in Berlin sieht seinen Abendansturm um 20 Uhr, nicht um 18 Uhr, und eine Inhaberin in Berlin und ein Manager in New York schauen auf dieselben Tage.

  • Die Zone stellst du unter CRM → Einstellungen → Zeitzone ein. Solange niemand eine gewählt hat, nimmt das CRM die Zeitzone aus dem Browser des Inhabers.
  • Danach richten sich die Tage in Traffic, Werbekanälen und Ereignissen und die Stunden in den Diagrammen.
  • Ein Wechsel der Zone rechnet auch vergangene Reports neu. Die Daten selbst ändern sich nicht – nur, wo Mitternacht liegt.

Besucher- und Session-IDs

Das Widget erkennt wiederkehrende Besucher ohne Cookies. Es legt drei Keys in localStorage ab:

KeyWas es istLebensdauer
crm_vidBesucher-IDBis der Besucher die Website-Daten löscht
crm_sidSession-IDNach 30 Minuten Inaktivität beginnt eine neue
crm_satZeitpunkt der letzten Aktivität — daran wird entschieden, wann eine Session vorbei istWird beim Surfen aktualisiert

Ist der Speicher blockiert (manche Privatsphäre-Modi tun das), leben die IDs im Arbeitsspeicher, solange der Tab offen ist. Merk dir crm_vid und crm_sid — du brauchst sie, um Server-Events mit dem Besucher zu verknüpfen. Im Consent-Modus entstehen die IDs erst nach der Einwilligung, und crmConsent.deny() löscht sie wieder.

Single-Page-Apps

React Router, Next.js-Client-Navigation, Vue Router — das Widget merkt clientseitige Routenwechsel von selbst, du musst dafür keine Zeile schreiben. Es hängt sich an history.pushState und history.replaceState und lauscht auf popstate (die Zurück- und Vor-Buttons). Nach einer kurzen Atempause von 300 ms zählt es einen Seitenaufruf — aber nur, wenn sich Pfad oder Query-String tatsächlich geändert haben. Ein Router, der beim Navigieren dreimal kurz hintereinander an der History dreht, erzeugt also einen Seitenaufruf, nicht drei.

Als Referrer eines solchen „virtuellen“ Seitenaufrufs gilt die vorige Seite deiner Site — genau wie bei einem echten Klick von Seite zu Seite.

Feinjustiert wird mit dem Attribut data-spa am Tag:

WertWas als neue Seite zählt
(kein Attribut)Standard: Wechsel von Pfad oder Query-String.
hashZusätzlich Hash-Wechsel (#/route) — für Router im Hash-Modus.
offSPA-Tracking aus: ein Seitenaufruf pro Script-Load, wie früher.
SPA mit Hash-Routinghtml
<script src="https://widget.sitecog.com/widget.js" data-spa="hash" defer></script>

Willst du einen Schritt messen, bei dem sich die URL gar nicht ändert — ein Modal, ein Tab, ein Akkordeon —, schick ihn als eigenes Event.

Datenschutz und Einwilligung

Das Widget setzt keine Cookies. Braucht deine Site vor Analytics trotzdem eine Einwilligung, schaltest du den Consent-Modus ein: Das Widget hält dann still, bis dein Cookie-Banner grünes Licht gibt. Einen eigenen Banner bringt es nicht mit — es dockt an deinen an.

Ein Attribut am Tag genügt:

<script src="https://widget.sitecog.com/widget.js" data-consent="required" defer></script>

Bis der Besucher zustimmt:

  • gehen keine Seitenaufrufe und keine Events raus. Was in dieser Zeit passiert, wird verworfen, nicht gepuffert — nachträglich wird nichts nachgeschickt;
  • liest das Widget document.cookie nicht — die Werbe-Cookies _ga, _gcl_*, _ym_uid, _fbp, _fbc bleiben unangetastet;
  • legt es crm_vid, crm_sid und crm_sat weder an noch speichert es sie.

Ohne das Attribut ändert sich nichts: Gezählt wird ab dem ersten Seitenaufruf, so wie bisher.

Sobald widget.js geladen ist, wartet auf window.crmConsent ein kleines Objekt:

AufrufWas er tut
crmConsent.grant()Einwilligung erteilt. Das Widget legt los und sendet sofort einen Seitenaufruf für die aktuelle Seite (einmal) — der Besuch, bei dem auf „Akzeptieren“ geklickt wurde, geht also nicht verloren.
crmConsent.deny()Einwilligung verweigert. Löscht crm_vid, crm_sid und crm_sat. Wirkt auch ohne data-consent — siehe unten.
crmConsent.status()'granted', 'denied' oder 'pending'.
crmConsent.requiredtrue, wenn der Tag data-consent="required" hat.
crmConsent.gpctrue, wenn der Browser Global Privacy Control sendet.

Die Wahl merkt sich das Widget in localStorage unter crm_consent — beim nächsten Besuch weiß es also noch Bescheid, und dein Banner kann über status() prüfen, ob überhaupt noch gefragt werden muss.

Dein Banner-Script läuft womöglich, bevor widget.js da ist. Dafür gibt es — wie bei crmq — eine Queue:

Funktioniert vor und nach dem Laden von widget.jsjs
window.crmConsent = window.crmConsent || [];
crmConsent.push('grant');   // oder 'deny'

push funktioniert auch nach dem Laden weiter. Dein Banner kann also einfach immer push nutzen und muss nie über die Ladereihenfolge nachdenken.

Ändert sich die Wahl, feuert das Widget auf window das Event crm:consent. Den neuen Stand liest du im Handler mit status() ab:

window.addEventListener('crm:consent', () => {
  console.log('Einwilligung:', window.crmConsent.status());
});
<div id="cookie-banner" hidden>
  Wir zählen Besuche, um die Site besser zu machen. Einverstanden?
  <button id="consent-yes">Akzeptieren</button>
  <button id="consent-no">Ablehnen</button>
</div>

<script>
  window.crmConsent = window.crmConsent || [];
  const banner = document.getElementById('cookie-banner');

  function choose(cmd) {
    crmConsent.push(cmd); // klappt vor und nach dem Laden von widget.js
    banner.hidden = true;
  }
  document.getElementById('consent-yes').onclick = () => choose('grant');
  document.getElementById('consent-no').onclick = () => choose('deny');

  // widget.js ist deferred und läuft vor DOMContentLoaded —
  // danach weiß status(), ob schon jemand entschieden hat
  document.addEventListener('DOMContentLoaded', () => {
    const c = window.crmConsent;
    if (typeof c.status === 'function' && c.status() === 'pending') {
      banner.hidden = false;
    }
  });
</script>

<script src="https://widget.sitecog.com/widget.js" data-consent="required" defer></script>

Global Privacy Control und Opt-out

Manche Browser und Erweiterungen senden das Signal Global Privacy Control (navigator.globalPrivacyControl). Ist es an, liest das Widget die Werbe-Cookies nie — in jedem Modus, mit oder ohne data-consent. Ob das Signal gesetzt ist, verrät dir crmConsent.gpc.

Und deny() wirkt auch ganz ohne Consent-Modus: Es löscht die IDs, und das Widget hält sich an die Entscheidung. Ein Opt-out funktioniert also auf jeder Site — ein „Nicht tracken“-Link im Footer ist ein Einzeiler:

Opt-out im Footerhtml
<a href="#" onclick="window.crmConsent = window.crmConsent || []; crmConsent.push('deny'); this.textContent = 'Alles klar, kein Tracking mehr'; return false;">Nicht tracken</a>

Was wir speichern und wie lange

IP-Adressen werden gekürzt gespeichert: Bei IPv4 wird das letzte Oktett genullt (203.0.113.57 → 203.0.113.0), IPv6 wird auf /48 abgeschnitten. Land und Stadt berechnen wir vorher aus der vollen IP — die Geo-Reports funktionieren also wie gewohnt.

URLs werden geputzt, bevor sie in der Datenbank landen — Seiten-URLs, Referrer, die Einstiegs-URL und die URLs von Events. Der Hash (#fragment) fliegt raus, ebenso diese Query-Parameter:

token *_token access_token id_token refresh_token auth_token auth password pass passwd pwd email e-mail mail key api_key apikey secret client_secret otp session sessionid jwt

*_token heißt: jeder Name, der auf _token endet. code bleibt bewusst drin — Promo-Codes willst du ja sehen. UTM-Tags, Klick-IDs und dl bleiben ebenfalls unangetastet.

Aufbewahrung: Seitenaufrufe und Events werden 13 Monate (395 Tage) aufbewahrt und danach gelöscht; den Zeitraum kann der Betreiber der Installation anpassen. Leads betrifft diese Frist nicht — sie werden dadurch nicht gelöscht.

Ein Besucher kann verlangen, früher vergessen zu werden. Der Inhaber der Website löscht sein Profil, seine Seitenaufrufe und Events anhand der Besucher-ID direkt im CRM; Leads nur, wenn das extra gewählt wird. Mehr dazu unter Leads → Daten einer Person löschen.

Unter Marketing → Werbekanäle legst du getaggte Links für jeden Ort an, an dem du wirbst — einen Social-Media-Post, einen Newsletter, ein Banner auf einer fremden Site. Jeder Link zeigt auf deine Site und trägt UTM-Tags plus einen kurzen Code:

https://shop.example/?utm_source=instagram&utm_medium=social&utm_campaign=autumn_sale&dl=k3Zp9QaW1x

dl ist ein 10-stelliger Code, der den Link identifiziert. Auf der Site musst du nichts tun: Das Widget greift ihn beim ersten Seitenaufruf ab, und jeder Besuch, jedes Event und jeder Lead in dieser Session wird dem Link zugeordnet. Jeder Link bekommt im CRM seine eigene Statistik.

Eigene Events

Seitenaufrufe sagen dir, wohin die Leute gegangen sind. Events sagen dir, was sie getan haben: in den Warenkorb gelegt, registriert, den Preisrechner geöffnet, bezahlt. Du beschreibst ein Event einmal im CRM und sendest es dann aus dem Browser oder von deinem Server.

Schritt null: das Event im CRM anlegen

Diil nimmt nur Events an, die es kennt. Das ist ein Feature: Ein Tippfehler in deinem Code kann nicht heimlich einen neuen Event-Typ erzeugen und deine Reports in zwei Hälften spalten.

  1. Marketing → Ereignisse öffnen und auf „Ereignis erstellen“ klicken

    Oben muss eine Site (Domain) ausgewählt sein.
  2. Genau so benennen wie im Code

    add_to_cart, signup_completed, purchase. Lateinische Buchstaben, Ziffern und Unterstriche, beginnend mit einem Buchstaben, bis 64 Zeichen. Ergänz einen lesbaren Titel und eine Beschreibung — dein zukünftiges Ich wird es dir danken.
  3. Die Parameter beschreiben

    Bis zu 20 pro Event, jeder mit einem Typ: Text, Zahl oder Ja/Nein (string, number, boolean). Was hier nicht beschrieben ist, erreicht die Datenbank nicht.
  4. Geld im Spiel? Haken bei „Dieses Ereignis bringt Umsatz“

    Und den Namen der Währung setzen (EUR, USD, USDT, sogar deine eigenen Treuepunkte). Ohne diesen Haken wird der value des Events nicht gespeichert.
  5. Den fertigen Aufruf kopieren

    Das CRM zeigt den exakten crmTrack-Aufruf und den Server-Request für dieses Event. Einfügen, fertig.

Eine Site kann bis zu 100 aktive Event-Typen haben. Sobald ein Event Daten hat, ist sein Name gesperrt (er steht ja schon in deinem Code), und das Event lässt sich nur archivieren statt löschen — so landen in den Reports nie namenlose Zeilen.

Events aus dem Browser senden: crmTrack

Signaturts
window.crmTrack(
  name: string,
  params?: Record<string, string | number | boolean>,
  options?: { id?: string; value?: number; currency?: string },
): void

Fire-and-forget: gibt nichts zurück, wirft dir nie etwas um die Ohren und sendet per sendBeacon, sodass das Event sogar überlebt, wenn der Klick von der Seite wegnavigiert. Besucher- und Session-ID werden automatisch angehängt — du beschreibst nur, was passiert ist.

Argumente

namestringPflicht
Der Event-Name, wie er im CRM angelegt ist. Lateinische Buchstaben, Ziffern und Unterstriche, beginnt mit einem Buchstaben, bis 64 Zeichen. Ein nicht angelegter Name wird stillschweigend verworfen — siehe Fehlersuche.
paramsobjectoptionalStandard: {}
Flaches Objekt mit Event-Details: { sku: 'air3-graphite', price: 149 }. Für Keys gelten dieselben Regeln wie für Namen, bis 40 Zeichen. Nur im CRM beschriebene Parameter bleiben erhalten; Werte werden in den angelegten Typ umgewandelt — siehe Parameter und Typen.
options.idstringoptionalStandard: keine
Idempotenz-Key, bis 128 Zeichen, eindeutig pro Site. Schick dieselbe id zweimal, und das Event wird einmal gespeichert. Nimm für Käufe deine Bestell-ID.
options.valueintegeroptionalStandard: keiner
Betrag in kleinsten Einheiten: 149900 heißt 1499,00. Von 0 bis 1012. Wird nur gespeichert, wenn beim Event „Dieses Ereignis bringt Umsatz“ an ist.
options.currencystringoptionalStandard: Währung des Events
Währungslabel, 1–10 lateinische Buchstaben oder Ziffern: EUR, USD, UAH. Lässt du es weg, wird die im CRM beim Event eingestellte Währung verwendet.

Beispiele

Die drei Events, die fast jeder Shop braucht — egal, woraus deine Site gebaut ist:

<button id="buy" data-sku="air3-graphite" data-price="149">In den Warenkorb</button>

<form id="signup">…</form>

<script>
  // ?. — damit ein Adblocker, der widget.js gefressen hat, deinen Button nicht kaputt macht
  document.getElementById('buy').addEventListener('click', (e) => {
    const { sku, price } = e.currentTarget.dataset;
    window.crmTrack?.('add_to_cart', { sku, price: Number(price) });
  });

  // Aufrufen, wenn das Konto wirklich angelegt ist, nicht beim ersten Klick
  function onSignupSuccess() {
    window.crmTrack?.('signup_completed', { method: 'email', newsletter: true });
  }

  // Auf der Danke-Seite. Das läuft vor dem deferred widget.js,
  // geht also über die Queue (siehe unten); id macht einen Reload harmlos
  window.crmq = window.crmq || [];
  crmq.push([
    'purchase',
    { order_id: 'A-1024', items: 2 },
    { id: 'A-1024', value: 29800, currency: 'EUR' }, // 298,00 EUR
  ]);
</script>

Aufrufe vor dem Laden des Scripts: die crmq-Queue

widget.js wird mit defer geladen, also gibt es window.crmTrack für einen kurzen Moment noch nicht. Früh gefeuerte Events — beim Laden der Seite, in einem Effect, aus einem Inline-Script im <head> — gingen verloren. Die Queue löst das:

Funktioniert vor und nach dem Laden von widget.jsjs
window.crmq = window.crmq || [];
crmq.push(['purchase', { order_id: 'A-1024', items: 2 }, { id: 'A-1024', value: 29800, currency: 'EUR' }]);

Jeder Eintrag ist ein Array aus denselben drei Argumenten wie bei crmTrack: Name, Params, Options. Sobald widget.js lädt, spielt es alles aus der Queue ab. Danach sendet crmq.push sofort — du kannst also einfach immer die Queue nutzen und musst nie über die Ladereihenfolge nachdenken.

Ein kleiner Helper, den du immer gefahrlos aufrufen kannstts
type CrmParams = Record<string, string | number | boolean>;
type CrmOptions = { id?: string; value?: number; currency?: string };

export function track(name: string, params?: CrmParams, options?: CrmOptions) {
  if (typeof window === 'undefined') return; // Server-Rendering: nichts zu tun
  window.crmq = window.crmq || [];
  window.crmq.push([name, params || {}, options || {}]);
}

Parameter und Typen

Parameter werden gegen die Beschreibung im CRM geprüft und nach diesen Regeln in den angelegten Typ normalisiert:

Angelegter TypAkzeptiertGut zu wissen
Text (string)Jeden WertWird auf 500 Zeichen gekürzt. Objekte werden zu JSON-Strings — flache Werte sehen in Reports aber viel schöner aus.
Zahl (number)Endliche Zahlen bis 1012 im BetragAuf 4 Nachkommastellen gerundet.
Ja/Nein (boolean)true, false, "true", "false", 1, 0Praktisch, wenn der Wert aus einem data-*-Attribut kommt.

Parameter-Keys: lateinische Buchstaben, Ziffern und Unterstriche, beginnend mit einem Buchstaben, bis 40 Zeichen. Bis zu 20 Parameter pro Event.

Umsatz und Idempotenz

value und currency

Geld wird als Ganzzahl in kleinsten Einheiten gesendet: Cent, Kopeken, Satoshi — was auch immer die kleinste Einheit deiner Währung ist. 29800 mit EUR sind 298,00 €. Gebrochene Geldbeträge in einer Datenbank produzieren früher oder später Cent-Differenzen, also erlauben wir sie einfach nicht.

  • value: eine Ganzzahl von 0 bis 1012.
  • currency: 1–10 lateinische Buchstaben oder Ziffern (EUR, USD, UAH, USDT). Lässt du sie weg, gilt die beim Event eingestellte Währung.
  • Beides wird nur gespeichert, wenn beim Event Dieses Ereignis bringt Umsatz angehakt ist; sonst ist value am Ende null.
const total = 298.0;                         // was dein Warenkorb anzeigt
const value = Math.round(total * 100);       // 29800 — was Diil haben will

id: einmal zählen

Danke-Seiten werden neu geladen, aus dem Verlauf geöffnet, auf ein zweites Gerät geteilt. Gib eine id (im Browser) oder event_id (vom Server) mit, und Diil speichert das Event einmal, egal wie oft es ankommt. Bis 128 Zeichen, eindeutig pro Site. Deine Bestellnummer ist der perfekte Kandidat.

Server-Events: POST /marketing/event

Alles, wo Geld im Spiel ist, zählt man nicht im Browser. Adblocker können den Request blockieren, Leute schließen den Tab, bevor die Danke-Seite lädt, und jeder kann crmTrack('purchase') aus der Konsole aufrufen. Dein Server dagegen weiß genau, wann eine Zahlung bestätigt ist. Schick das Event von dort:

POST https://back.sitecog.com/marketing/event
content-type: application/json
x-event-key: sk_…

Geheime Schlüssel

Server-Events werden mit einem geheimen Schlüssel signiert: Marketing → Ereignisse → Geheime Schlüssel → Schlüssel ausstellen. Er sieht aus wie sk_ + 48 Hex-Zeichen. Du kannst bis zu 5 aktive Schlüssel pro Site haben — einen pro Server oder Integration — und jeden davon im CRM widerrufen. Leads, die von einem Server gesendet werden, nutzen dieselben Schlüssel.

Request

x-event-keyheaderstringPflicht
Dein geheimer Schlüssel, sk_….
namestringPflicht
Event-Name, wie im CRM angelegt. Kurzer Alias: n.
event_idstringoptional
Idempotenz-Key, bis 128 Zeichen, eindeutig pro Site. Eine Wiederholung wird mit duplicate: true beantwortet und nicht noch einmal gespeichert. Alias: id.
visitor_idstringoptional
Die crm_vid des Besuchers aus dem Browser. Alias: vid.
session_idstringoptional
Die crm_sid des Besuchers. Damit erbt das Event Kanal, UTM-Tags und Werbelink dieser Session. Alias: sid.
paramsobjectoptional
Event-Parameter, dieselben Regeln wie im Browser. Alias: p.
valueintegeroptional
Betrag in kleinsten Einheiten, 0 bis 1012. Alias: val.
currencystringoptional
Währungslabel; Standard ist die Währung des Events. Alias: cur.
urlstringoptional
Die Seite, auf die sich das Event bezieht, z. B. dein Checkout. Alias: u.

Der gesamte Body muss in 8 KB passen.

// Node 18+ — fetch ist eingebaut
export async function sendPurchase(order) {
  const res = await fetch('https://back.sitecog.com/marketing/event', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-event-key': process.env.DIIL_EVENT_KEY,
    },
    body: JSON.stringify({
      name: 'purchase',
      event_id: order.id,              // "A-1024" — Retries sind sicher
      visitor_id: order.crmVid,        // beim Checkout gespeichert, kann null sein
      session_id: order.crmSid,
      params: { order_id: order.id, items: order.items.length },
      value: order.totalCents,         // 29800 = 298,00
      currency: 'EUR',
      url: 'https://shop.example/checkout',
    }),
    signal: AbortSignal.timeout(5000),
  });

  if (!res.ok) {
    console.error('Diil event failed:', res.status, await res.text());
  }
  return res.ok;
}

Response

200 OKjson
{ "ok": true, "duplicate": false }
200 OK — dieselbe event_id noch einmaljson
{ "ok": true, "duplicate": true }
okboolean
Das Event wurde angenommen.
duplicateboolean
True, wenn es ein Event mit dieser event_id schon gibt. Nichts Neues wurde gespeichert — und das ist in Ordnung, kein Fehler.

Fehler

Anders als der Browser sagt dir die Server-Route genau, was schiefgelaufen ist:

StatusBodyWas passiert ist
400{"message":"invalid_body"}Der Body ist kein gültiges JSON oder nicht das erwartete Objekt.
400{"message":"invalid_event_name"}Der Name verletzt das Format: lateinische Buchstaben, Ziffern, Unterstriche, beginnt mit einem Buchstaben, bis 64 Zeichen.
400{"message":"unknown_event","name":"purchse"}Kein solches Event unter Marketing → Ereignisse. Tippfehler oder noch nicht angelegt. Der Body gibt den gesendeten Namen zurück.
401{"message":"invalid_key"}x-event-key fehlt, ist fehlerhaft oder widerrufen.
413—Der Body ist größer als 8 KB.
429{"message":"rate_limit_exceeded"}Zu viele Requests in dieser Minute. Siehe Limits.

Ein Server-Event allein weiß nichts über Anzeigen: Dein Server hat keine Ahnung, dass der Käufer vor drei Tagen über einen Instagram-Post kam. Der Browser schon. Der Trick ist also, die IDs des Widgets zusammen mit der Bestellung vom Browser an dein Backend zu tragen:

  1. beim Checkout crm_vid und crm_sid aus localStorage lesen;
  2. sie mit der Bestellung an dein Backend schicken und daneben speichern;
  3. wenn die Zahlung bestätigt ist, sie als visitor_id und session_id mitgeben.

Das Event erbt dann Kanal, UTM-Tags und Werbelink des ersten Seitenaufrufs der Session — so zeigt Marketing → Ereignisse, welcher Kanal und welcher Werbelink das Geld gebracht hat, nicht nur die Klicks.

// checkout.js — wenn der Kunde auf „Bezahlen“ drückt
function diilIds() {
  try {
    return {
      crm_vid: localStorage.getItem('crm_vid'),
      crm_sid: localStorage.getItem('crm_sid'),
    };
  } catch {
    return { crm_vid: null, crm_sid: null }; // Speicher blockiert: Die Bestellung geht trotzdem durch
  }
}

const res = await fetch('/api/orders', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ cart, ...diilIds() }),
});

Limits

Die Limits gelten für alle Marketing-Routen gemeinsam — Seitenaufrufe, Events und Leads zusammen — und werden in festen Ein-Minuten-Fenstern gezählt.

WasLimit
Requests pro IP120 pro Minute
Requests pro Site6.000 pro Minute
Request-Body8 KB (größer → 413)
Aktive Event-Typen pro Site100
Parameter pro Event20
Event-Name / Parameter-Key64 / 40 Zeichen
Wert eines Text-Parameters500 Zeichen
id / event_id128 Zeichen
Aktive geheime Schlüssel pro Site5

Fehlersuche

Das Event taucht nicht auf

  • Es ist nicht angelegt. Der Browser-Endpoint antwortet immer mit 204 und verwirft unbekannte Event-Namen stillschweigend — keine Hinweise, mit Absicht. Der Server-Endpoint ist ehrlich: 400 unknown_event mit dem Namen, den du gesendet hast. Im Zweifel schick dasselbe Event einmal per curl und lies die Antwort.
  • Der Name weicht ab. addToCart im Code und add_to_cart im CRM sind zwei verschiedene Dinge. Kopier den Aufruf aus der Event-Karte.
  • Du testest im Live-Modus. Im iframe des CRM wird nichts getrackt. Nimm einen normalen Tab.
  • Der Consent-Modus ist an, und der Besucher hat noch nicht zugestimmt. Dann liefert crmConsent.status() den Wert 'pending', und Seitenaufrufe und Events vor der Einwilligung werden verworfen — nicht gepuffert, nicht nachgeholt. Klick in deinem Banner auf „Akzeptieren“ und probier es noch mal. Mehr unter Consent-Modus.
  • Das Script hat nicht geladen, oder crmTrack wurde zu früh aufgerufen. Nutz die crmq-Queue.
  • Die Domain ist keine Site im CRM. Die Site wird am Origin der Seite erkannt (www. wird ignoriert); eine unbekannte bekommt 400 unknown_domain — such danach im Network-Tab der DevTools.
  • Deine CSP blockiert es. Erlaube https://widget.sitecog.com in script-src und https://back.sitecog.com in connect-src.

Das Event ist da, aber Parameter fehlen

Öffne das Event unter Marketing → Ereignisse. Siehst du Kommt an, ist aber nicht beschrieben, hat uns der Parameter erreicht, steht aber nicht in der Beschreibung des Events — ergänz ihn (oder korrigier den Tippfehler im Code). Prüf außerdem, ob die Werte zu den angelegten Typen aus Parameter und Typen passen.

Der Kauf hat keinen Betrag

Setz beim Event den Haken Dieses Ereignis bringt Umsatz und sende value als Ganzzahl in kleinsten Einheiten — 29800, nicht 298.00 und nicht "298 €".

Die Zahlen passen nicht zum Zahlungssystem

Manche Besucher nutzen Adblocker oder Privatsphäre-Erweiterungen, die Analytics-Requests blockieren, und manche schließen den Tab vor der Danke-Seite. Browser-Events werden also immer etwas zu wenige sein. Für UX-Signale ist das okay, für Geld nicht: Sende Käufe vom Server — das lässt sich nicht blockieren, nicht aus der Konsole fälschen und wird mit event_id nie doppelt gezählt.

Events benennen: ein paar Gewohnheiten, die sich auszahlen

  • snake_case, verbartig, Vergangenheit oder Grundform: add_to_cart, signup_completed, purchase, calculator_opened. Nicht click1, nicht ButtonPressed.
  • Benenne das Ergebnis, nicht das UI-Element. signup_completed überlebt ein Redesign; green_button_click nicht.
  • Ein Event, viele Params. add_to_cart mit { sku, price } schlägt add_to_cart_air3, add_to_cart_air4… — und hält dich weit weg vom Limit von 100 Typen.
  • Immer eine id mitgeben bei allem, was zweimal feuern kann: Käufe, Bestätigungen, einmalige Registrierungen.
  • Browser fürs Verhalten, Server fürs Geld. Klicks und Schritte über crmTrack; Zahlungen, Erstattungen und Abos aus deinem Backend.
  • Schreib die Beschreibung im CRM. „Feuert, wenn der Payment-Provider die Abbuchung bestätigt“ spart dir in sechs Monaten ein Meeting.

Wie es weitergeht