Ein Kontaktformular, das an jemanden im Urlaub mailt, ist der Ort, an dem Leads sterben. Mit Diil landet jedes Formular deiner Website als Karte im CRM — mit Telefonnummer oder E-Mail, einem Status und jemandem, der zurückruft. Ein Attribut am <form> reicht; JavaScript- und Server-Varianten sind da, wenn du mehr Kontrolle brauchst.
Drei Wege, einen Lead zu senden — nimm, was passt:
- Ein Formular mit
data-crm-lead— null JavaScript. Das Widget fängt den Submit ab und erledigt den Rest. window.crmLead()— für React, Vue und jedes Formular, das du selbst steuerst.POST /marketing/leadvon deinem Server — telefonische Bestellungen, Backend-Formulare, Integrationen mit anderen Systemen.
Wie ein Lead vom Formular ins CRM kommt
In Diil ist ein Lead eine besondere Art von Event. Jedes Event hat einen Namen — etwa contact_form —, und dieser Name muss zuerst im CRM angelegt sein. Setz dort den Haken „Das ist ein Lead“, und jede Einsendung mit diesem Namen wird zur Karte unter CRM → Leads.
- Ein Besucher füllt das Formular aus und drückt „Senden“.
- Das Widget sammelt die Felder ein, sortiert, wer die Person ist (Name, Telefon, E-Mail, Nachricht), und schickt alles ab.
- Der Server prüft, ob der Name angelegt ist, ob der Lead eine Telefonnummer oder E-Mail hat und nicht nach Bot aussieht.
- Im CRM erscheint eine Lead-Karte — und, falls eingerichtet, eine Benachrichtigung in Telegram.
Zwei Regeln, die du dir von Anfang an merken solltest:
- Ein nicht angelegter Name wird abgelehnt. Das CRM nimmt nur, was es kennt — kein überraschender Müll durch Tippfehler.
- Angelegt, aber nicht als Lead markiert? Dann wird es als normales Event gespeichert, und es entsteht keine Lead-Karte.
Lead-Typ im CRM einrichten
Zwei Minuten Klicken, einmal pro Formulartyp:
Marketing → Ereignisse öffnen
Hier wird jedes Event beschrieben, das deine Site senden darf.Ein Event anlegen
Gib ihm den Namen, den dein Code verwenden wird: lateinische Buchstaben, Ziffern und Unterstriche, beginnend mit einem Buchstaben —contact_form,callback_request,quote_request.Haken bei „Das ist ein Lead“ setzen
Ab jetzt werden Einsendungen zu Lead-Karten. Wie der Hinweis im CRM sagt: Das Formular muss eine Antwortmöglichkeit mitschicken — Telefonnummer oder E-Mail.Optional: Haken bei „Dieses Ereignis bringt Umsatz“
Nur dann werden Betrag und Währung des Leads gespeichert. Ohne den Haken werden sie stillschweigend ignoriert.
Variante A: ein Formular mit einem Attribut
Setz data-crm-lead="dein_event_name" an ein beliebiges Formular auf einer Seite mit dem Widget-Script. Das ist die ganze Integration. Formulare, die später auftauchen — in einem Modal, nach einem Routenwechsel in einer Single-Page-App —, werden automatisch erkannt.
Komplettes Beispiel
Ein „Angebot anfordern“-Formular mit Name, Telefon, E-Mail und Nachricht, einem Betrag für den Umsatz-Report, einer Danke-Nachricht und einer eigenen Prüfung. Erst reines HTML; die React- und Vue-Versionen rufen crmLead() aus ihrem eigenen Submit-Handler auf.
<form id="quote" data-crm-lead="quote_request" data-crm-value="149900" data-crm-currency="EUR">
<label>Dein Name <input name="name" autocomplete="name" required></label>
<label>Telefon <input name="phone" type="tel" autocomplete="tel"></label>
<label>E-Mail <input name="email" type="email" autocomplete="email"></label>
<label>Was brauchst du? <textarea name="message" rows="4"></textarea></label>
<!-- Keins der bekannten Felder: erscheint so, wie es ist, in der Lead-Karte -->
<label>Teamgröße
<select name="team_size">
<option>1–5</option>
<option>6–20</option>
<option>20+</option>
</select>
</label>
<button type="submit">Anfrage senden</button>
<p class="form-status" role="status" hidden></p>
</form>
<script>
const form = document.getElementById('quote');
const status = form.querySelector('.form-status');
const button = form.querySelector('button');
function say(text) {
status.textContent = text;
status.hidden = false;
}
// 1. Vor dem Senden: unsere eigene Prüfung. preventDefault() = nichts wird gesendet
form.addEventListener('crm:lead-before', (event) => {
const phone = form.elements.phone.value.trim();
const email = form.elements.email.value.trim();
if (!phone && !email) {
event.preventDefault();
say('Bitte hinterlass eine Telefonnummer oder E-Mail, damit wir antworten können.');
return;
}
button.disabled = true;
});
// 2. Nach dem Senden: Das Widget zeigt nichts an, das „Danke“ kommt von uns
form.addEventListener('crm:lead', (event) => {
button.disabled = false;
say(event.detail.ok
? 'Danke! Wir melden uns innerhalb eines Werktags.'
: 'Da ist etwas schiefgelaufen. Versuch es bitte noch mal oder ruf uns an.');
});
</script>
<script src="https://widget.sitecog.com/widget.js" defer></script>'use client';
import { useState, type FormEvent } from 'react';
const EMPTY = { name: '', phone: '', email: '', message: '' };
export function QuoteForm() {
const [form, setForm] = useState(EMPTY);
const [status, setStatus] = useState<'idle' | 'sending' | 'done' | 'error'>('idle');
const set = (key: keyof typeof EMPTY) => (e: { target: { value: string } }) =>
setForm((prev) => ({ ...prev, [key]: e.target.value }));
async function onSubmit(e: FormEvent) {
e.preventDefault();
if (!form.phone.trim() && !form.email.trim()) {
setStatus('error');
return;
}
setStatus('sending');
// crmLead wirft nie; undefined ist es nur, wenn widget.js nicht auf der Seite ist
const res = await window.crmLead?.('quote_request', form, { value: 149900, currency: 'EUR' });
if (res?.ok) {
setForm(EMPTY);
setStatus('done');
} else {
setStatus('error');
}
}
if (status === 'done') return <p>Danke! Wir melden uns innerhalb eines Werktags.</p>;
// Hier kein data-crm-lead: Dieses Formular sendet selbst über crmLead
return (
<form onSubmit={onSubmit}>
<input value={form.name} onChange={set('name')} placeholder="Dein Name" required />
<input value={form.phone} onChange={set('phone')} type="tel" placeholder="Telefon" />
<input value={form.email} onChange={set('email')} type="email" placeholder="E-Mail" />
<textarea value={form.message} onChange={set('message')} placeholder="Was brauchst du?" />
<button disabled={status === 'sending'}>Anfrage senden</button>
{status === 'error' && <p role="alert">Bitte hinterlass Telefon oder E-Mail und versuch es noch mal.</p>}
</form>
);
}<script setup lang="ts">
import { reactive, ref } from 'vue';
const empty = () => ({ name: '', phone: '', email: '', message: '' });
const form = reactive(empty());
const status = ref<'idle' | 'sending' | 'done' | 'error'>('idle');
async function submit() {
if (!form.phone.trim() && !form.email.trim()) {
status.value = 'error';
return;
}
status.value = 'sending';
// crmLead wirft nie; undefined ist es nur, wenn widget.js nicht auf der Seite ist
const res = await window.crmLead?.('quote_request', { ...form }, { value: 149900, currency: 'EUR' });
if (res?.ok) {
Object.assign(form, empty());
status.value = 'done';
} else {
status.value = 'error';
}
}
</script>
<template>
<p v-if="status === 'done'">Danke! Wir melden uns innerhalb eines Werktags.</p>
<!-- Hier kein data-crm-lead: Dieses Formular sendet selbst über crmLead -->
<form v-else @submit.prevent="submit">
<input v-model="form.name" placeholder="Dein Name" required />
<input v-model="form.phone" type="tel" placeholder="Telefon" />
<input v-model="form.email" type="email" placeholder="E-Mail" />
<textarea v-model="form.message" placeholder="Was brauchst du?" />
<button :disabled="status === 'sending'">Anfrage senden</button>
<p v-if="status === 'error'" role="alert">Bitte hinterlass Telefon oder E-Mail und versuch es noch mal.</p>
</form>
</template>Was beim Absenden des Formulars passiert
- Das Widget bricht den nativen Submit immer ab — die Seite lädt nicht neu, und die
actiondes Formulars wird nicht genutzt. - Es feuert
crm:lead-befoream Formular. Ruft irgendein Listenerevent.preventDefault()auf, ist die Geschichte hier zu Ende, und nichts wird gesendet. - Es sammelt die Felder mit
FormDataein — außer den ausgeschlossenen. Es zählen nur Textwerte: Datei-Inputs werden ignoriert. Mehrere Felder mit demselben Namen (eine Gruppe Checkboxen) werden mit", "verbunden. - Es sendet den Lead, geschützt durch einen einmaligen Formular-Pass (mehr dazu unter Spamschutz).
- Es feuert
crm:leadmit{ ok: true }oder{ ok: false }inevent.detail. - Ist
oktrue, ruft esform.reset()auf. Bei einem Fehler bleibt das Formular, wie es ist, damit der Besucher nicht verliert, was er getippt hat.
Das Widget zeigt keine eigene UI — keinen Toast, keinen Spinner, kein „Danke“. Deine Site, dein Design, deine Worte: Hör auf crm:lead und zeig, was du willst.
Formular-Attribute
| Attribut | Beispiel | Was es tut |
|---|---|---|
data-crm-lead | "quote_request" | Macht das Formular zum Lead-Formular. Der Wert ist der im CRM angelegte Event-Name. Ein leeres Attribut bedeutet den Namen lead_submit — der muss ebenfalls angelegt sein. |
data-crm-value | "149900" | Betrag in kleinsten Einheiten, eine Ganzzahl: 149900 sind 1.499,00. Wird nur gespeichert, wenn beim Event „Dieses Ereignis bringt Umsatz“ gesetzt ist. |
data-crm-currency | "EUR" | Währung des Betrags: EUR, USD, UAH… |
data-crm-ignore | (ohne Wert) | Gehört nicht ans Formular, sondern an ein Feld oder einen Wrapper (<fieldset>, <div>…): Das Feld bzw. alles darin wird nie gesendet. Siehe Felder, die nie gesendet werden. |
Formular-Events
Beide Events bubbeln, du kannst also am Formular selbst lauschen oder einmal an document für alle Formulare der Seite.
| Event | Wann | event.detail | Abbrechbar |
|---|---|---|---|
crm:lead-before | Direkt nach dem Submit, bevor irgendetwas gesendet wird | { name } — der Event-Name | Ja: preventDefault() stoppt das Senden |
crm:lead | Nachdem der Server geantwortet hat | { ok } — true, wenn der Lead angenommen wurde | Nein |
document.addEventListener('crm:lead', (event) => {
if (event.detail.ok) {
event.target.closest('.modal')?.classList.add('is-thanks');
}
});Feldnamen, die das CRM versteht
Eine Lead-Karte hat vier Hauptfelder: Name, Telefon, E-Mail und Nachricht. Das Widget füllt sie anhand der Feldnamen. Die Namen werden getrimmt und kleingeschrieben und dann exakt mit dieser Liste verglichen (Phone funktioniert also, your-phone nicht):
| Feld | Feldnamen, die es füllen | Max. Länge |
|---|---|---|
| Name | name, fio, username, user_name, fullname, full_name, firstname, first_name, contact_name, client_name, имя, фио, ім'я | 200 |
| Telefon | phone, tel, telephone, mobile, phone_number, contact_phone, телефон, тел | 40 |
email, e_mail, e-mail, mail, contact_email, почта, пошта, емейл | 320 | |
| Nachricht | message, comment, comments, text, question, note, description, task, сообщение, комментарий, вопрос, повідомлення, коментар | 4000 |
- Der erste Treffer gewinnt. Hat ein Formular sowohl
phoneals auchmobile, füllt das erste im Formular das Feld. - Alles andere bleibt auch erhalten. Unbekannte Felder und zweite Treffer landen in „Zusätzlich“ und erscheinen in der Lead-Karte in Formular-Reihenfolge. Bis zu 30 Zusatzfelder; Keys bis 60 Zeichen, Werte bis 1000.
- Eine Telefonnummer zählt nur mit 7 bis 20 Ziffern. Leerzeichen, Klammern und Bindestriche sind okay; ein führendes
+bleibt erhalten. - Telefon oder E-Mail ist Pflicht. Ein Lead ohne beides wird mit
no_contactabgelehnt — es gäbe keinen Weg, zu antworten.
Felder, die nie gesendet werden
Manche Daten haben in einem CRM nichts verloren — Passwörter, Kartennummern, CSRF-Tokens. Das Widget lässt sie weg, bevor irgendetwas das Formular verlässt:
- Passwortfelder — jedes
<input type="password">. - Alles mit
data-crm-ignore— am Feld selbst oder an einem beliebigen Wrapper (<fieldset>,<div>…). Dann wird alles darin übersprungen. - Felder mit sensiblem Namen. Als eigenes Wort im Namen (getrennt durch
_,-,.oder camelCase):card,cc,csc,cvv,cvc,iban,ssn,secret,pass. Irgendwo im Namen:password,passwd,pwd,token,csrf,xsrf,creditcard,cardnumber,ccnum.
Ein paar Beispiele: card_number ✗, cvv ✗, csrf_token ✗, user_password ✗ — aber discard_reason ✓, denn dort ist „card“ kein eigenes Wort.
- Ein Name, ein Urteil. Ist ein Feld mit einem bestimmten Namen ausgeschlossen, sind es alle Felder mit diesem Namen.
- Versteckte Inputs werden gesendet — praktisch für Produkt, Tarif oder Seite. Es sei denn, ihr Name ist sensibel.
<form data-crm-lead="signup_request">
<input name="email" type="email"> <!-- ✓ gesendet -->
<input name="password" type="password"> <!-- ✗ Passwortfeld -->
<input type="hidden" name="plan" value="pro"> <!-- ✓ versteckt, aber gesendet -->
<fieldset data-crm-ignore> <!-- ✗ alles hier drin -->
<input name="billing_address">
<input name="vat_id">
</fieldset>
<input name="card_number"> <!-- ✗ „card“ als eigenes Wort -->
<input name="discard_reason"> <!-- ✓ hier ist „card“ kein eigenes Wort -->
<button type="submit">Registrieren</button>
</form>Dieselbe Namensregel gilt für die Keys von crmLead(). Und der Server filtert genauso — auch bei POST /lead mit geheimem Schlüssel. Ein sensibles Feld landet also nie im CRM, egal auf welchem Weg es kommt.
Variante B: Leads aus JavaScript senden
Wenn du den Formular-State selbst hältst — React, Vue, ein mehrstufiger Wizard, ein Chatbot —, ruf das Widget direkt auf. Beide Funktionen erscheinen auf window, sobald widget.js geladen ist.
crmLead(name, fields, options)
const { ok } = await window.crmLead(
'callback_request',
{ name: 'Anna', phone: '+49 30 1234567', message: 'Bitte ruf mich nach 17 Uhr an' },
{ value: 149900, currency: 'EUR' },
);namestringPflicht^[A-Za-z][A-Za-z0-9_]*$). Ein Name, der nicht passt, bringt eine Warnung in der Konsole und { ok: false }.fieldsobjectPflichtoptions.valueintegeroptional149900 = 1.499,00. Wird nur bei Events mit „Dieses Ereignis bringt Umsatz“ gespeichert.options.currencystringoptionalEUR.Gibt ein Promise zurück, das aufgelöst wird zu:
okbooleantrue — der Lead ist angenommen. false — er wurde abgelehnt oder ist nicht durchgekommen; den Grund verraten wir absichtlich nicht (siehe warum).crmLeadForm(form, name)
Macht genau das, was das Attribut data-crm-lead macht, nur aus dem Code. Praktisch, wenn das Formular von einer Fremdbibliothek gerendert wird und du keine Attribute ergänzen kannst, oder wenn der Name erst zur Laufzeit feststeht. Gibt nichts zurück; Ergebnisse kommen über dasselbe crm:lead-Event.
const form = document.querySelector('#newsletter-popup form');
window.crmLeadForm(form, 'newsletter_signup');
form.addEventListener('crm:lead', (e) => {
if (e.detail.ok) form.innerHTML = '<p>Du bist dabei! Schau in dein Postfach.</p>';
});formHTMLFormElementPflichtnamestringPflichtcrmLead.TypeScript-Deklarationen
Das Widget ist ein einfaches Script, TypeScript weiß also nichts davon. Pack das in eine beliebige .d.ts-Datei:
export {};
declare global {
interface Window {
crmLead?: (
name: string,
fields: Record<string, string>,
options?: { value?: number; currency?: string },
) => Promise<{ ok: boolean }>;
crmLeadForm?: (form: HTMLFormElement, name: string) => void;
}
}Wie der Spamschutz funktioniert
Ein öffentliches Formular zieht Bots an wie ein Magnet. Ein CAPTCHA brauchst du trotzdem nicht — Widget und Server erledigen das gemeinsam, und echte Besucher merken nichts davon.
Ein einmaliger Formular-Pass
Vor dem Senden holt sich das Widget einen signierten „Formular-Pass“ vom Server. Um Zeit zu sparen, fragt es danach, sobald der Besucher das Formular zum ersten Mal fokussiert. Ein Pass ist:
- einmalig — ein Pass, ein Lead;
- an deine Site gebunden — ein Pass von einer Site ist auf einer anderen nutzlos;
- 24 Stunden gültig.
Bots, die blind an den Endpoint POSTen, haben keinen Pass und werden abgelehnt. Und ein Doppelklick oder erneutes Absenden mit demselben Pass liefert { ok: true }, der Lead wird aber nur einmal gespeichert — keine doppelten Karten durch ungeduldige Finger.
Der Honeypot
Das versteckte Feld company_site von oben. Ein Mensch sieht es nicht, also bleibt es leer. Ein Bot, der jedes Feld ausfüllt, das er findet, verrät sich selbst, und der Lead wird abgelehnt.
„Verdächtige“ Leads
Manche Leads sehen seltsam aus, könnten aber echt sein. Die werden angenommen und bekommen im CRM das Label Verdächtig, damit ein Manager vor dem Anruf kurz draufschauen kann:
- das Formular wurde in unter 3 Sekunden ausgefüllt — schneller als menschenmöglich;
- das Formular war vor dem Absenden länger als 30 Minuten offen;
- mehr als 5 Leads kamen innerhalb einer Stunde von derselben IP-Adresse.
Die IP-Adresse, die du in der Lead-Karte siehst, ist übrigens gekürzt: Bei IPv4 ist das letzte Oktett genullt, IPv6 ist auf /48 abgeschnitten — dieselbe Regel wie unter Was wir speichern und wie lange.
„Spam“ ist ein Status, den ein Mensch im CRM von Hand setzt. Nichts wird automatisch als Spam markiert, also fliegt kein echter Kunde stillschweigend raus.
Warum der Browser nur ok: true oder false bekommt
Einem Bot „abgelehnt: Honeypot ausgefüllt“ zu sagen, ist eine Gratis-Lektion, wie man durchkommt. Deshalb sieht aus dem Browser jede Ablehnung gleich aus — { ok: false }, ohne Grund. Du debuggst dein eigenes Formular? Die Checkliste zur Fehlersuche deckt jeden Fall ab, und die Server-Route sagt dir sehr wohl, was schiefging, weil sie durch einen geheimen Schlüssel geschützt ist.
Unter der Haube: der Browser-Request
Für Neugierige — selbst bauen musst du das nie. Das Widget holt sich einen Pass über POST https://back.sitecog.com/marketing/fk (Antwort: {"pass":"…"}) und sendet dann den Lead:
POST https://back.sitecog.com/marketing/f
Content-Type: application/json
{
"n": "contact_form",
"pass": "…",
"fields": {
"name": "Anna",
"email": "anna@example.com",
"message": "Hi!",
"company_site": ""
},
"val": 149900,
"cur": "EUR",
"u": "https://shop.example/contacts",
"vid": "…",
"sid": "…"
}Die Antwort ist { "ok": true } oder { "ok": false }. vid und sid sind die IDs von Besucher und Session, damit der Lead mit dem Werbekanal verknüpft wird, über den der Besucher kam — siehe Events & Analytics.
Leads von deinem Server senden
Nicht jeder Lead beginnt im Browser. Nimm die Server-Route, wenn:
- ein Manager eine Bestellung am Telefon annimmt und in dein Backoffice eintippt;
- dein Formular im Backend verarbeitet wird (ein PHP-Handler, eine Next.js Server Action) und du nicht vom Widget abhängen willst;
- Leads aus einem anderen System kommen: einem Marktplatz, einem Buchungsservice, einem Bot.
POST https://back.sitecog.com/marketing/lead
x-event-key: sk_…
Content-Type: application/jsonGeheime Schlüssel
Die Server-Route authentifiziert sich mit einem geheimen Schlüssel. Leg einen an unter CRM → Marketing → Ereignisse → Geheime Schlüssel → Schlüssel ausstellen. Ein Schlüssel sieht aus wie sk_ + 48 Hex-Zeichen; eine Site kann bis zu 5 aktive Schlüssel haben, und jeder davon lässt sich widerrufen. Gib jedem Schlüssel einen Namen, der sagt, wo er lebt („Payment-Server“, „Telefonbestellungen“) — dein zukünftiges Ich wird es dir danken, wenn einer widerrufen werden muss.
Beispiel-Request
curl https://back.sitecog.com/marketing/lead \
-H "x-event-key: sk_3f9a1c7e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d" \
-H "Content-Type: application/json" \
-d '{
"name": "phone_order",
"event_id": "order-1024",
"fields": {
"name": "Anna Schmidt",
"phone": "+49 30 1234567",
"message": "Zwei Paar Air 3, Graphit. Lieferung ins Büro."
},
"value": 149900,
"currency": "EUR",
"url": "https://shop.example/contacts"
}'// Node 18+ — fetch ist eingebaut
const res = await fetch('https://back.sitecog.com/marketing/lead', {
method: 'POST',
headers: {
'x-event-key': process.env.CRM_EVENT_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'phone_order',
event_id: `order-${order.id}`, // ein Retry erzeugt keinen zweiten Lead
visitor_id: order.crmVid, // beim Checkout aus dem Browser gespeichert, falls vorhanden
session_id: order.crmSid,
fields: {
name: order.customerName,
phone: order.phone,
message: order.comment,
delivery: order.deliveryMethod, // landet in „Zusätzlich“
},
value: order.totalCents, // 149900 = 1.499,00
currency: 'EUR',
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`CRM lead: ${res.status} ${data.message}`);
// data → { ok: true, duplicate: false }import os
import requests
res = requests.post(
"https://back.sitecog.com/marketing/lead",
headers={"x-event-key": os.environ["CRM_EVENT_KEY"]},
json={
"name": "phone_order",
"event_id": "order-1024",
"fields": {
"name": "Anna Schmidt",
"phone": "+49 30 1234567",
"message": "Zwei Paar Air 3, Graphit.",
},
"value": 149900,
"currency": "EUR",
},
timeout=10,
)
res.raise_for_status()
print(res.json()) # {'ok': True, 'duplicate': False}Request-Body
x-event-keyheaderstringPflichtsk_… aus dem CRM. Fehlt oder widerrufen → 401 invalid_key.namestringPflichtn.fieldsobjectPflichtevent_idstringoptionalid.visitor_idstringoptionalsession_idstringoptionalcrm_sid). Damit erbt der Lead Werbekanal und UTM-Tags dieser Session. Alias: sid.valueintegeroptional149900 = 1.499,00. Wird nur bei Events mit „Dieses Ereignis bringt Umsatz“ gespeichert. Alias: val.currencystringoptionalEUR. Alias: cur.urlstringoptionalu.Response
{ "ok": true, "duplicate": false }okbooleanduplicatebooleantrue, wenn es einen Lead mit dieser event_id schon gibt. Nichts Neues wurde gespeichert, und das ist in Ordnung.Anders als der Browser nutzt die Server-Route weder Formular-Pässe noch Zeitprüfungen — der geheime Schlüssel ist Beweis genug. Der Honeypot gilt trotzdem: Ein nicht leeres company_site in fields bekommt 400 rejected.
Idempotenz: Retry ohne Angst
Netzwerke fallen im ungünstigsten Moment aus. Ist der Request durchgegangen oder nicht? Mit event_id musst du das nicht wissen: Schick ihn einfach noch mal. Eine Wiederholung liefert {"ok":true,"duplicate":true}, und der Lead wird nicht doppelt gespeichert.
{ "ok": true, "duplicate": true }Attribution: welche Anzeige den Lead gebracht hat
Leads aus dem Widget werden automatisch mit dem Besucher verknüpft. Ein Server-Lead weiß nichts über den Browser — es sei denn, du sagst es ihm. Das Widget legt die Besucher-ID in localStorage unter crm_vid ab und die Session-ID unter crm_sid. Schick sie zusammen mit dem Formular oder der Bestellung an dein Backend und reich sie weiter:
const crm = {
vid: localStorage.getItem('crm_vid'),
sid: localStorage.getItem('crm_sid'),
};
await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...order, crm }),
});
// …und auf dem Server: visitor_id: body.crm.vid, session_id: body.crm.sidMit session_id erbt der Lead Werbekanal und UTM-Tags des ersten Seitenaufrufs dieser Session und taucht so in den Reports neben der Kampagne auf, die ihn verdient hat. Mehr dazu unter Events & Analytics.
Fehler
Aus dem Browser sagen crmLead() und das crm:lead-Event immer nur ok: false. Die Server-Route antwortet mit einem Status und einer message:
| Status | Body | Was passiert ist |
|---|---|---|
| 400 | {"message":"unknown_event","name":"phone_order"} | Kein Event mit diesem Namen im CRM. Leg es unter Marketing → Ereignisse an (prüf Schreibweise und Groß-/Kleinschreibung). |
| 400 | {"message":"invalid_event_name"} | Der Name enthält Zeichen außer lateinischen Buchstaben, Ziffern und Unterstrichen oder beginnt nicht mit einem Buchstaben. |
| 400 | {"message":"no_contact"} | Weder Telefonnummer (7–20 Ziffern) noch E-Mail in fields. |
| 400 | {"message":"invalid_body"} | Der Body ist kein JSON-Objekt in der erwarteten Form. |
| 400 | {"message":"rejected"} | Das Honeypot-Feld company_site ist ausgefüllt. |
| 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. Warte auf die nächste. |
Browser-Requests können außerdem 400 unknown_origin oder 400 unknown_domain bekommen, wenn die Domain der Seite keine Site im CRM ist — die siehst du im Network-Tab, während der Code weiterhin ok: false bekommt.
Limits
| Was | Limit |
|---|---|
| Requests pro IP (alle Marketing-Routen zusammen) | 120 pro Minute |
| Requests pro Site | 6000 pro Minute |
| Formular-Pässe pro IP | 20 pro Minute |
| Request-Body | 8 KB |
| Name / Telefon / E-Mail / Nachricht | 200 / 40 / 320 / 4000 Zeichen |
| Zusatzfelder | bis zu 30; Key bis 60, Wert bis 1000 Zeichen |
| Telefon | 7–20 Ziffern, um als Kontakt zu zählen |
event_id | bis 128 Zeichen |
| Geheime Schlüssel | bis zu 5 aktive pro Site |
Die Zeitfenster sind feste Minuten: Nach einer 429 wartest du, bis die nächste Minute beginnt. Ein echter Besucher kommt nie in die Nähe; ein Script, das auf dein Formular einhämmert, schon.
Fehlersuche
Der Lead ist nicht angekommen
Geh die Liste durch — fast immer ist es einer dieser Punkte:
- Der Name ist nicht angelegt. Unter Marketing → Ereignisse muss es ein Event mit genau diesem Namen geben, inklusive Groß-/Kleinschreibung. Ein leeres
data-crm-leadbedeutetlead_submit— leg genau das an. - „Das ist ein Lead“ ist nicht angehakt. Dann wird die Einsendung als normales Event gespeichert: Such sie in der Event-Statistik, nicht unter Leads.
- Kein Telefon und keine E-Mail. Oder sie sind da, aber unter Namen, die das CRM nicht kennt (
your-phone,contact[email]), und sind in „Zusätzlich“ gelandet. Benenne sie um — siehe die Feldtabelle. Eine Telefonnummer mit weniger als 7 Ziffern zählt auch nicht. - Die Daten steckten in einem Datei-Input. Dateien werden ignoriert — das Widget sendet nur Text.
- Ein echtes Feld heißt
company_site. Das ist der Honeypot-Name; der Lead wird als Bot behandelt. - Dein eigener
crm:lead-before-Listener hatpreventDefault()aufgerufen — vielleicht nicht dann, wann du dachtest. - Das Widget ist nicht auf dieser Seite. Ohne
widget.jstut das Attribut nichts, und das Formular wird auf die altmodische Art abgeschickt (oder gar nicht). - Eine Content Security Policy blockiert es. Hat deine Site eine CSP, braucht sie
script-src https://widget.sitecog.comundconnect-src https://back.sitecog.com. Die Browser-Konsole sagt es dir in Rot. - Die Domain ist keine Site im CRM. Die Site wird am Origin der Seite erkannt (
www.wird entfernt). Ein Test auflocalhostoder einer Staging-Domain, die nicht im CRM eingetragen ist, ergibtunknown_domainim Network-Tab. - Du hast viel getestet. 20 Formular-Pässe pro Minute und IP reichen für Menschen locker, aber nicht für hektisches Klicken. Warte eine Minute.
Ein Feld ist nicht angekommen
Der Lead ist da, aber ein Feld fehlt? Meist wurde es mit Absicht weggelassen:
- Der Name klingt sensibel. Prüf ihn gegen die Regeln unter Felder, die nie gesendet werden:
promo_passetwa fliegt wegenpassraus. Benenn es um —promo_codekommt problemlos durch. - Irgendwo steht
data-crm-ignore. Am Feld selbst oder an einem Wrapper weiter oben im DOM — dort gilt es für alles darin. - Ein anderes Feld mit demselben Namen ist ausgeschlossen. Dann sind es alle Felder mit diesem Namen.
- Es ist ein Passwortfeld oder ein Datei-Input. Passwörter werden nie gesendet, und Dateien ignoriert das Widget sowieso.
Der Betrag wird nicht angezeigt
Setz beim Event-Typ den Haken „Dieses Ereignis bringt Umsatz“. Prüf außerdem, ob der Betrag eine Ganzzahl in kleinsten Einheiten ist: 1499.00 wird als 149900 gesendet.
Benachrichtigungen in Telegram
Ein Lead, den niemand sieht, ist ein verlorener Lead. In den Einstellungen unter CRM → Leads kannst du einen Telegram-Bot verbinden: Bot-Token eintragen, Benachrichtigungen einschalten, entscheiden, ob verdächtige Leads auch geschickt werden, und Ruhezeiten festlegen, damit die Nachtschicht der Bots dein Sales-Team nicht weckt.
Benachrichtigungen sind mit HTML formatiert, und alles, was der Besucher eingegeben hat, wird vorher escaped. Ein „Name“ wie <a href="…">Hier klicken</a> kommt als reiner Text an: Niemand kann deinem Team einen Link in den Chat schmuggeln oder die Formatierung zerschießen. Lange Felder werden gekürzt, damit ein Roman im Nachrichtenfeld nicht zur Textwand wird – der vollständige Lead steht immer im CRM.
Daten einer Person auf Anfrage löschen
„Bitte löscht alles, was ihr über mich habt“ ist eine ganz normale Bitte, und sie zu erfüllen sollte nicht eine Woche Wühlen in Tabellen bedeuten. Das CRM hat dafür zwei Werkzeuge – ohne Code auf deiner Seite.
Aus einem Lead oder Chat
Die Lead-Karte und die Chat-Karte haben beide „Daten des Besuchers löschen“. Bevor etwas gelöscht wird, zeigt das CRM genau, was alles verschwindet:
- das Besucherprofil und Analytics – Seitenaufrufe und Events;
- Chats und Anhänge – die Zahl der Chats, Nachrichten und Dateien;
- Leads – nur, wenn du „Auch die Anfragen dieses Besuchers löschen“ ankreuzt. Standardmäßig aus: Ein Lead ist oft ein laufender Deal, und die Entscheidung liegt bei dir.
Ein Lead, den du von deinem Server ohne visitor_id gesendet hast, hängt an keinem Besucher – dort lässt sich nur der Lead selbst löschen.
Per E-Mail oder Telefon
Meist kommt die Bitte per E-Mail: „Ich bin anna@example.com, vergesst mich“. Geh zu CRM → Einstellungen → „Anfragen zur Datenlöschung“, gib die E-Mail oder Telefonnummer ein, die die Person angegeben hat, und das CRM findet ihre Leads, Chats und ihr Konto auf deiner Website. Ein Button löscht alles Gefundene; die Besuchs-Analytics der zugehörigen Besucher kann gleich mit weg.
- Rechte. Löschen braucht dieselben Rechte wie das Bearbeiten dieser Bereiche. Ein Manager, der Chats nur ansehen darf, sieht die Treffer, kann sie aber nicht löschen.
- Aktivitätsprotokoll. Jede Löschung landet im Aktivitätsprotokoll – ohne E-Mail und Telefonnummer der Person, sonst würde das Protokoll genau das aufbewahren, was vergessen werden sollte.
- Kein Rückgängig. Gelöscht ist gelöscht. Prüf die Zahlen, bevor du bestätigst.