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/eventmit 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:
| Key | Was es ist | Lebensdauer |
|---|---|---|
crm_vid | Besucher-ID | Bis der Besucher die Website-Daten löscht |
crm_sid | Session-ID | Nach 30 Minuten Inaktivität beginnt eine neue |
crm_sat | Zeitpunkt der letzten Aktivität — daran wird entschieden, wann eine Session vorbei ist | Wird 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:
| Wert | Was als neue Seite zählt |
|---|---|
| (kein Attribut) | Standard: Wechsel von Pfad oder Query-String. |
hash | Zusätzlich Hash-Wechsel (#/route) — für Router im Hash-Modus. |
off | SPA-Tracking aus: ein Seitenaufruf pro Script-Load, wie früher. |
<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.
Consent-Modus
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.cookienicht — die Werbe-Cookies_ga,_gcl_*,_ym_uid,_fbp,_fbcbleiben unangetastet; - legt es
crm_vid,crm_sidundcrm_satweder an noch speichert es sie.
Ohne das Attribut ändert sich nichts: Gezählt wird ab dem ersten Seitenaufruf, so wie bisher.
Die crmConsent-API
Sobald widget.js geladen ist, wartet auf window.crmConsent ein kleines Objekt:
| Aufruf | Was 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.required | true, wenn der Tag data-consent="required" hat. |
crmConsent.gpc | true, 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:
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>// CookieBanner.tsx
'use client';
import { useEffect, useState } from 'react';
type ConsentStatus = 'granted' | 'denied' | 'pending';
type CrmConsent = {
push: (cmd: 'grant' | 'deny') => void;
status: () => ConsentStatus;
};
declare global {
interface Window {
crmConsent?: CrmConsent | string[];
}
}
function readStatus(): ConsentStatus | null {
const c = window.crmConsent;
// Bevor widget.js geladen ist, ist crmConsent nur die Queue (ein Array)
return c && !Array.isArray(c) ? c.status() : null;
}
function choose(cmd: 'grant' | 'deny') {
window.crmConsent = window.crmConsent || [];
window.crmConsent.push(cmd);
}
export function CookieBanner() {
const [status, setStatus] = useState<ConsentStatus | null>(null);
useEffect(() => {
const update = () => setStatus(readStatus());
update();
window.addEventListener('load', update); // widget.js kam nach dem Effect
window.addEventListener('crm:consent', update); // die Wahl hat sich geändert
return () => {
window.removeEventListener('load', update);
window.removeEventListener('crm:consent', update);
};
}, []);
// Schon entschieden (oder gar kein Widget, z. B. Adblocker) → kein Banner
if (status !== 'pending') return null;
return (
<div className="cookie-banner">
Wir zählen Besuche, um die Site besser zu machen. Einverstanden?
<button onClick={() => choose('grant')}>Akzeptieren</button>
<button onClick={() => choose('deny')}>Ablehnen</button>
</div>
);
}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:
<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.
Werbelinks: wissen, welche Anzeige den Besucher gebracht hat
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=k3Zp9QaW1xdl 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.
Marketing → Ereignisse öffnen und auf „Ereignis erstellen“ klicken
Oben muss eine Site (Domain) ausgewählt sein.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.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.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 dervaluedes Events nicht gespeichert.Den fertigen Aufruf kopieren
Das CRM zeigt den exaktencrmTrack-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
window.crmTrack(
name: string,
params?: Record<string, string | number | boolean>,
options?: { id?: string; value?: number; currency?: string },
): voidFire-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
namestringPflichtparamsobjectoptionalStandard: {}{ 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: keineid zweimal, und das Event wird einmal gespeichert. Nimm für Käufe deine Bestell-ID.options.valueintegeroptionalStandard: keiner149900 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 EventsEUR, 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>// diil.d.ts — TypeScript einmal das Widget beibringen
export {};
type CrmParams = Record<string, string | number | boolean>;
type CrmOptions = { id?: string; value?: number; currency?: string };
declare global {
interface Window {
crmTrack?: (name: string, params?: CrmParams, options?: CrmOptions) => void;
crmq?: unknown[];
}
}
// AddToCartButton.tsx
type Product = { sku: string; price: number };
export function AddToCartButton({ product, onAdd }: { product: Product; onAdd: () => void }) {
const handleClick = () => {
onAdd();
window.crmTrack?.('add_to_cart', { sku: product.sku, price: product.price });
};
return <button onClick={handleClick}>In den Warenkorb</button>;
}// app/checkout/success/PurchaseTracker.tsx
'use client';
import { useEffect } from 'react';
type Props = { orderId: string; items: number; totalCents: number; currency: string };
export function PurchaseTracker({ orderId, items, totalCents, currency }: Props) {
useEffect(() => {
// Der Effect kann laufen, bevor widget.js geladen ist — die Queue wartet darauf
window.crmq = window.crmq || [];
window.crmq.push([
'purchase',
{ order_id: orderId, items },
{ id: orderId, value: totalCents, currency },
]);
}, [orderId, items, totalCents, currency]);
return null;
}
// app/checkout/success/page.tsx (eine Server Component)
// <PurchaseTracker orderId="A-1024" items={2} totalCents={29800} currency="EUR" />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:
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.
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 Typ | Akzeptiert | Gut zu wissen |
|---|---|---|
| Text (string) | Jeden Wert | Wird 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 Betrag | Auf 4 Nachkommastellen gerundet. |
| Ja/Nein (boolean) | true, false, "true", "false", 1, 0 | Praktisch, 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
valueam Endenull.
const total = 298.0; // was dein Warenkorb anzeigt
const value = Math.round(total * 100); // 29800 — was Diil haben willid: 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-keyheaderstringPflichtsk_….namestringPflichtn.event_idstringoptionalduplicate: true beantwortet und nicht noch einmal gespeichert. Alias: id.visitor_idstringoptionalcrm_vid des Besuchers aus dem Browser. Alias: vid.session_idstringoptionalcrm_sid des Besuchers. Damit erbt das Event Kanal, UTM-Tags und Werbelink dieser Session. Alias: sid.paramsobjectoptionalp.valueintegeroptionalval.currencystringoptionalcur.urlstringoptionalu.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;
}<?php
function send_purchase(array $order): bool
{
$payload = [
'name' => 'purchase',
'event_id' => $order['id'], // "A-1024" — Retries sind sicher
'visitor_id' => $order['crm_vid'], // beim Checkout gespeichert, kann null sein
'session_id' => $order['crm_sid'],
'params' => ['order_id' => $order['id'], 'items' => count($order['items'])],
'value' => $order['total_cents'], // 29800 = 298,00
'currency' => 'EUR',
'url' => 'https://shop.example/checkout',
];
$ch = curl_init('https://back.sitecog.com/marketing/event');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-event-key: ' . getenv('DIIL_EVENT_KEY'),
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
error_log("Diil event failed: $status $body");
return false;
}
return true;
}curl -X POST https://back.sitecog.com/marketing/event \
-H "content-type: application/json" \
-H "x-event-key: $DIIL_EVENT_KEY" \
-d '{
"name": "purchase",
"event_id": "A-1024",
"visitor_id": "<crm_vid>",
"session_id": "<crm_sid>",
"params": { "order_id": "A-1024", "items": 2 },
"value": 29800,
"currency": "EUR",
"url": "https://shop.example/checkout"
}'Response
{ "ok": true, "duplicate": false }{ "ok": true, "duplicate": true }okbooleanduplicatebooleanevent_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:
| Status | Body | Was 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. |
Server-Events mit dem Besucher verknüpfen
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:
- beim Checkout
crm_vidundcrm_sidauslocalStoragelesen; - sie mit der Bestellung an dein Backend schicken und daneben speichern;
- wenn die Zahlung bestätigt ist, sie als
visitor_idundsession_idmitgeben.
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() }),
});// server.js (Express)
app.post('/api/orders', async (req, res) => {
const { cart, crm_vid, crm_sid } = req.body;
// IDs bei der Bestellung aufheben: Die Zahlung wird oft später bestätigt, per Webhook
const order = await db.orders.create({ cart, crmVid: crm_vid, crmSid: crm_sid });
res.json({ id: order.id, payUrl: await createPayment(order) });
});
// Dein Payment-Provider ruft das auf, wenn das Geld wirklich angekommen ist
app.post('/webhooks/payment', async (req, res) => {
const order = await db.orders.markPaid(req.body.orderId);
// 3 · Diil — Analytics darf niemals den Checkout kaputt machen
try {
await sendPurchase(order); // die Funktion aus „Server-Events“ oben
} catch (err) {
console.error('Diil is unreachable, will retry later', err);
}
res.sendStatus(200);
});Limits
Die Limits gelten für alle Marketing-Routen gemeinsam — Seitenaufrufe, Events und Leads zusammen — und werden in festen Ein-Minuten-Fenstern gezählt.
| Was | Limit |
|---|---|
| Requests pro IP | 120 pro Minute |
| Requests pro Site | 6.000 pro Minute |
| Request-Body | 8 KB (größer → 413) |
| Aktive Event-Typen pro Site | 100 |
| Parameter pro Event | 20 |
| Event-Name / Parameter-Key | 64 / 40 Zeichen |
| Wert eines Text-Parameters | 500 Zeichen |
id / event_id | 128 Zeichen |
| Aktive geheime Schlüssel pro Site | 5 |
Fehlersuche
Das Event taucht nicht auf
- Es ist nicht angelegt. Der Browser-Endpoint antwortet immer mit
204und verwirft unbekannte Event-Namen stillschweigend — keine Hinweise, mit Absicht. Der Server-Endpoint ist ehrlich:400 unknown_eventmit dem Namen, den du gesendet hast. Im Zweifel schick dasselbe Event einmal per curl und lies die Antwort. - Der Name weicht ab.
addToCartim Code undadd_to_cartim 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
crmTrackwurde zu früh aufgerufen. Nutz die crmq-Queue. - Die Domain ist keine Site im CRM. Die Site wird am
Originder Seite erkannt (www.wird ignoriert); eine unbekannte bekommt400 unknown_domain— such danach im Network-Tab der DevTools. - Deine CSP blockiert es. Erlaube
https://widget.sitecog.cominscript-srcundhttps://back.sitecog.cominconnect-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. Nichtclick1, nichtButtonPressed. - Benenne das Ergebnis, nicht das UI-Element.
signup_completedüberlebt ein Redesign;green_button_clicknicht. - Ein Event, viele Params.
add_to_cartmit{ sku, price }schlägtadd_to_cart_air3,add_to_cart_air4… — und hält dich weit weg vom Limit von 100 Typen. - Immer eine
idmitgeben 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.