55 Demo Requests im Monat, fünf davon Spam. Google kennt den Unterschied nicht — und sucht seit Wochen nach mehr davon.
TLDR: Jede Web-Conversion bekommt am Formular-Submit eine eigene ID mit — damit lassen sich Spam und Non-ICP-Leads nachträglich aus Google Ads zurückrufen. Ohne diese ID lässt sich eine Web-Conversion nicht zurückholen.
Das Verfahren heißt Retraction: eine bereits gemeldete Conversion wird nachträglich zurückgerufen. Google Ads entfernt sie aus Reporting und Bidding. Der Prozess dahinter ist unspektakulär — ein Sheet, ein Flag, ein täglicher Upload. Die eigentliche Arbeit liegt davor: die Conversion muss überhaupt identifizierbar sein.

Warum Web-Conversions das eigentliche Problem sind
Eine Web-Conversion feuert in dem Moment, in dem jemand auf „Absenden“ klickt. Keine Qualifikation, keine Prüfung, kein Mensch dazwischen. Ein Bot, ein Wettbewerber, ein Test-Submit des eigenen Teams, eine Firma weit außerhalb des ICP — alles zählt identisch als Demo Request.
Google Ads bekommt das Signal sofort und behandelt es als Erfolg. Smart Bidding merkt sich Klick, Keyword, Placement, Zielgruppe und sucht mehr davon.
Zwei Wege führen aus dieser Situation heraus. Der eine ist Prävention: Honeypot-Felder, Validierung im Formular, Pflichtangaben, die Bots aussteigen lassen. Der ist billiger und sollte immer zuerst kommen.
Der andere ist Korrektur — die nachträgliche Retraction. Sie ist die einzige Option für alles, was ein Formular nicht erkennen kann: eine echte Person aus einer Firma, die schlicht nicht zur Zielgruppe gehört.
Der harte Blocker: order_id
Damit Google eine bereits gemeldete Web-Conversion wiederfinden kann, braucht es einen eindeutigen Schlüssel. Bei offline importierten Conversions ist das die ClickID plus Zeitstempel. Bei web-getrackten Conversions gibt es nur einen einzigen Weg: die order_id — eine eindeutige ID, die beim Conversion-Tracking mitgeschickt wird.
Viele Advertisers schicken sie nicht mit. Sie ist kein Pflichtfeld, und für die reine Deduplizierung reicht die Einstellung „Count: One“ völlig aus. Ohne order_id lässt sich eine Web-Conversion aber nicht zurückholen — Google findet sie nicht wieder.
Daraus folgt die Reihenfolge: Erst führt der Werbetreibende die order_id ein, dann kann er über Retraction reden. Alles, was vor diesem Umstellungsdatum getrackt wurde, bleibt dauerhaft nicht rückrufbar. Kein Fehler, sondern ein akzeptierter Bruch.
*Google Ads nennt das Feld order_id, GA4 nennt denselben Wert transaction_id. In diesem Artikel heißt er durchgängig order_id.
conversion_uid: einzigartige ID, die am Submit entsteht
Der Join-Key des ganzen Systems ist ein Feld namens conversion_uid — eine eindeutige ID, die genau eine einzige Conversion identifiziert. Füllt dieselbe Person drei Formulare aus, entstehen drei IDs. Die ID identifiziert die Conversion, nicht den Menschen dahinter.
Erzeugt wird sie im Browser, im Moment des Formular-Submits. Nicht vorher, nicht serverseitig später, nicht im CRM.
Der Grund ist banal und entscheidend: Zu jedem anderen Zeitpunkt ist sie entweder noch nicht da oder für das Tracking nicht mehr erreichbar. Die Entry-ID des Formulars zum Beispiel entsteht erst auf dem Server — da haben die Conversion-Tags längst gefeuert.
Von dort aus geht die ID drei Wege — bewusst cookieless, weil cookiebasiertes Tracking von ITP und Blockern zerlegt wird:
- In ein Hidden Field im Formular. Von dort über die bestehende Automatisierung ins Retract-Sheet. Kein Cookie beteiligt, Coverage nahe 100 %.
- In den Query-String der Danke-Seite. GTM liest sie dort aus einer URL-Variable und gibt sie an die Conversion-Tags weiter — Google Ads als
order_id, GA4 alstransaction_id. - In einen kurzlebigen Cookie. Nur als Backup, falls der Query-String-Weg ausfällt. Der Cookie speichert die ID nicht, er transportiert sie über einen Seitenwechsel.
Der Nebeneffekt ist fast so wertvoll wie die Retraction selbst: Sobald dieselbe ID in beiden Tracking-Pipelines steckt, dedupliziert Google Ads deterministisch statt heuristisch. Doppelzählungen zwischen zwei parallelen Setups hören auf, unabhängig davon, ob je eine Retraction stattfindet.
Das Retract-Sheet: eine Zeile pro Web-Conversion
Das operative Herzstück ist ein Spreadsheet. Eine Zeile = eine Web-Conversion, automatisch befüllt beim Submit.
| Spalte | Funktion |
|---|---|
| conversion_uid | Die ID. Byte-identisch, nie transformieren. |
| Conversion_Name | Exakte Schreibweise der Conversion Action in Google Ads. Case-sensitive. |
| Conversion_Time | Wann die Conversion passiert ist. |
| Email / Company | Matching-Parameter für die Kaskade. |
| CRM-ID | Per Lookup aus dem Tracking-Sheet. Wenn vorhanden, der bevorzugte Schlüssel. |
| Retract | YES = zur Retraction vorgemerkt. |
| Retract_Source | manual / crm / ai — mehrere Quellen werden angehängt, nie überschrieben. |
| Date_Triggered | Wann geflaggt wurde. |
| Date_Retracted | Wann der Upload durchlief. |
| Status | pending → uploaded → confirmed / error. |
Zwei Dinge, die auf dem Papier nach Erbsenzählerei aussehen und in der Praxis darüber entscheiden, ob das System funktioniert.
Conversion_Time und Date_Triggered sind nicht dasselbe. Conversion_Time datiert die Conversion, Date_Triggered datiert die Einstufung als schlecht. Zwischen beiden liegen Stunden oder Tage. Google will für das Adjustment den zweiten Zeitpunkt.
Retract_Source wird angehängt, nicht überschrieben. Wenn Sales und ein automatischer Check denselben Lead unabhängig flaggen, darf keiner den anderen überschreiben. Klingt pedantisch, ist die klassische Race Condition in genau solchen Sheets.
Mein Sheet hält 90 Tage, ältere Zeilen werden automatisch gelöscht. Das Adjustment-Fenster von Google liegt bei rund 54 Tagen — die zusätzlichen Wochen dienen nur noch analytischen Zwecken.
Als Abfallprodukt liefert das Sheet einen Coverage-Check: Zeilen pro Tag gegen Conversions pro Tag im Tracking-Sheet, rollierend über 7 Tage gegen eine 30-Tage-Baseline. Fällt das Verhältnis, ist entweder das Snippet auf der Website kaputt oder die Automatisierung ausgefallen.
Die Kaskade: ein Lead, drei Web-Conversions
Der häufigste reale Fall: Dieselbe Person füllt innerhalb weniger Stunden zwei oder drei Formulare aus. Kontaktanfrage, Registrierung, Demo Request. Drei Conversions, drei IDs — und wenn der Lead irrelevant ist, sind alle drei Events irrelevant.
Deswegen läuft ein Check in drei Stufen:
- Exakte E-Mail oder CRM-ID — alle Zeilen desselben Leads werden mitgeflaggt. Unstrittig, vollautomatisch.
- Gleiche Person innerhalb von 7 Tagen — abgeglichen über Name, E-Mail und Firmenname, alle exakt. Registrierung, Kontaktformular und Demo Request derselben Person liegen fast immer nur wenige Tage auseinander. Dieselbe Kombination nach Monaten wäre Zufall — daher das enge Fenster.
- Alles andere → Notification, keine Aktion. Match älter als 7 Tage, kein exakter Treffer, uneinheitlicher Status: eine E-Mail mit den Kandidaten, ein Mensch entscheidet.
Der Upload an Google Ads
Der Upload läuft über den ConversionAdjustmentUploadService der Google Ads API. Nicht über die Data Manager API — die kann ausschließlich ingestieren, keine Adjustments. Wer dort sucht, findet Methoden mit „remove“ im Namen, die aber nur für Audiences gelten. Sackgasse.
Pro Zeile gehen vier Werte raus: order_id, exakter Name der Conversion Action, Adjustment Time und der Typ RETRACTION. Ein täglicher oder stündlicher Batch reicht.
Drei Timing-Regeln bestimmen den Rhythmus:
- Frühestens 24 Stunden nach der Original-Conversion. Google verlangt diese Wartezeit.
- Auf das Bidding wirkt die Retraction nur etwa 7 Tage lang. Danach ist es eine reine Reporting-Korrektur. Das ist ein gültiger Argument gegen einen wöchentlichen Rhythmus — wer einmal pro Woche flaggt, korrigiert nur noch Zahlen, nicht mehr den Algorithmus.
- Für Web-Conversions ist die Retraction endgültig. Kein Undo. Der Trick, eine zurückgerufene Conversion mit neuem Timestamp erneut hochzuladen, existiert nur für Offline-Importe. Ein Flag zurückzunehmen ist folgenlos, solange der Upload nicht gelaufen ist — die 24-Stunden-Wartezeit ist genau dieses Fenster.
Praktische Konsequenz für den ersten Live-Test: gegen eine Offline-Import-Conversion testen, nie gegen eine echte Web-Conversion. Bei der ersten geht ein Fehler zurück, bei der zweiten nicht.
Eine Voraussetzung bleibt: Die Kampagne benötigt genug Conversions. Jede Retraction senkt den kumulativen Conversion-Count, und Smart Bidding braucht Volumen zum Lernen. Wer ohnehin an der unteren Grenze fährt, baut zuerst Volumen auf und bereinigt danach.
Drei Quellen für ein Flag: manuell, Sales, AI
Bleibt die eigentliche Frage: Wer entscheidet, dass eine Conversion schlecht war? Im Retract-Sheet laufen drei Quellen zusammen.
Manuell. Ein Mensch sichtet die Zeilen und setzt das Flag. Schnell und unstrittig, aber es skaliert nur so weit, wie die Aufmerksamkeit reicht.
Sales über das CRM. Sobald ein Lead im CRM auf einen Status wie „Irrelevant“ oder „Spam“ gesetzt wird, wandert das Flag automatisch ins Retrack-Sheet — gematcht über CRM-ID oder E-Mail.
Das ist die wertvollste Quelle, weil Sales als Einziger mit der Firma gesprochen hat. Es ist auch die langsamste: Das Flag kommt Tage nach der Conversion, manchmal erst nach dem Fenster, in dem die Retraction das Bidding noch beeinflusst.
ICP Radar durch AI-Klassifikation. Ein Modell bewertet jede eingehende Firma gegen das Ideal Customer Profile und flaggt automatisch, was eindeutig nicht relevant ist (Non-Relevant + Confidence-Level 95%+).
Secondary-ICP und unklare Fälle gehen an einen Menschen. Ein irreversibler Prozess ist der falsche Ort für Wahrscheinlichkeiten.
Offline-Conversions: ähnlicher Prozess im CRM
Für Offline-Conversions läuft dieser Prozess längst im CRM — HubSpot, Zoho, Salesforce, Pipedrive, das Tool spielt keine Rolle.
Der Ablauf: Sales bewertet jeden Account auf ICP Relevanz. Passt die Firma ins Ideal Customer Profile — Primary oder Secondary — geht die zugehörige ClickID beim nächsten Upload zurück an Google Ads. Passt sie nicht, wird sie nicht importiert. D.h. nicht relevante ClickIDs werden nie importiert.
Der Filter sitzt damit vor dem Upload, nicht dahinter. Es muss nichts zurückgerufen werden, weil nie etwas Falsches ankommt. Prävention statt Korrektur.
Mehr über Google Ads B2B Conversion Tracking erfahren.
Universal Listener Code
/* ============================================================
Universal EC Listener · v3.0 · 17.08.2026
============================================================ */
add_action('wp_footer', 'siteplan_universal_ec_listener', 100);
function siteplan_universal_ec_listener() {
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
$load = false;
foreach (array('demo', 'kosten', 'cost', 'registrierung', 'kontakt') as $seg) {
if (strpos($uri, '/' . $seg) !== false) { $load = true; break; }
}
if (!$load) return;
?>
<script id="sp-universal-ec-listener">
(function() {
'use strict';
if (window.__spUniversalEC) return;
window.__spUniversalEC = true;
var path = window.location.pathname;
// Thank-You-Pages: dort liest GTM den Cookie — Listener bleibt aus
if (path.indexOf('/termin-buchen') > -1) return;
if (path.indexOf('/vielen-dank') > -1) return;
// Segment-genauer Path-Match: '/kosten/' matcht, '/kostenrechnung' nicht
function hasSeg(seg) {
var s = '/' + seg;
if (path.indexOf(s + '/') > -1) return true;
if (path.length >= s.length && path.slice(path.length - s.length) === s) return true;
return false;
}
/* ---------------- Page-Config ---------------- */
var GROUPS = [
{
name: 'demo', // Form 10
segs: ['demo', 'kosten', 'cost'],
cookie: 'demo_ud',
cuidField: 'input_41',
phoneHidden: ['input[name="input_10"]'],
phoneRaw: 'input[id$="_10_raw"]',
cic: { 'input_31':'gclid','input_32':'fbclid','input_33':'gbraid','input_34':'wbraid','input_38':'msclkid','input_39':'li_fat_id' }
},
{
name: 'registrierung', // Form 3
segs: ['registrierung'],
cookie: 'gf_ud',
cuidField: 'input_36',
phoneHidden: ['input[name="input_10"]', 'input[id="input_3_10"]'],
phoneRaw: 'input[id="input_3_10_raw"]',
cic: { 'input_24':'gclid','input_25':'fbclid','input_27':'gbraid','input_28':'wbraid','input_33':'msclkid','input_34':'li_fat_id' }
},
{
name: 'kontakt', // Form 4
segs: ['kontakt'],
cookie: 'gf_ud',
cuidField: 'input_26',
phoneHidden: ['input[name="input_10"]', 'input[name="input_5"]'],
phoneRaw: 'input[id$="_raw"]',
cic: { 'input_16':'gclid','input_17':'fbclid','input_19':'gbraid','input_20':'wbraid','input_23':'msclkid','input_24':'li_fat_id' }
}
];
var CFG = null;
for (var g = 0; g < GROUPS.length && !CFG; g++) {
for (var m = 0; m < GROUPS[g].segs.length; m++) {
if (hasSeg(GROUPS[g].segs[m])) { CFG = GROUPS[g]; break; }
}
}
if (!CFG) return;
var VERSION = 'sp-universal-v3.0-' + CFG.name;
/* ---------------- conversion_uid ---------------- */
function makeUID() {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var a = new Uint8Array(50);
crypto.getRandomValues(a);
var s = '';
for (var i = 0; i < 50; i++) { s += chars[a[i] % chars.length]; }
return s;
}
var CUID = makeUID();
/* ---------------- EC Capture (Logik v2) ---------------- */
var capturedValues = {
email: '',
first_name: '',
last_name: '',
phone: '',
conversion_uid: CUID
};
function normalizePhone(rawValue) {
if (!rawValue) return '';
var trimmed = rawValue.trim();
if (!trimmed) return '';
var p = trimmed.replace(/[^0-9+]/g, '');
if (!p) return '';
if (p.indexOf('+') === 0) return p;
if (p.indexOf('00') === 0) return '+' + p.substring(2);
if (p.indexOf('0') === 0) return '+43' + p.substring(1);
if (p.indexOf('43') === 0) return '+' + p;
return '+' + p;
}
function firstEl(selectors) {
for (var i = 0; i < selectors.length; i++) {
var el = document.querySelector(selectors[i]);
if (el) return el;
}
return null;
}
function pollAndCache(verbose) {
var emailInput = document.querySelector('input[name="input_2"]');
var firstInput = document.querySelector('input[name="input_1.3"]');
var lastInput = document.querySelector('input[name="input_1.6"]');
var phoneHiddenEl = firstEl(CFG.phoneHidden);
var phoneRawEl = document.querySelector(CFG.phoneRaw);
var phoneHiddenValue = phoneHiddenEl ? phoneHiddenEl.value : '';
var phoneRawValue = phoneRawEl ? phoneRawEl.value : '';
if (verbose) {
console.log('--- pollAndCache [' + VERSION + '] ---');
console.log('phoneHidden value:', JSON.stringify(phoneHiddenValue));
console.log('phoneRaw value: ', JSON.stringify(phoneRawValue));
}
if (emailInput && emailInput.value && emailInput.value.indexOf('@') > -1) {
capturedValues.email = emailInput.value.trim().toLowerCase();
}
if (firstInput && firstInput.value && firstInput.value.length > 0) {
capturedValues.first_name = firstInput.value.trim();
}
if (lastInput && lastInput.value && lastInput.value.length > 0) {
capturedValues.last_name = lastInput.value.trim();
}
var phoneFound = '';
if (phoneHiddenValue) {
var cleanHidden = phoneHiddenValue.trim();
if (cleanHidden.indexOf('+') === 0 && cleanHidden.length >= 5) {
phoneFound = cleanHidden;
if (verbose) console.log('Phone from hidden:', cleanHidden);
} else if (verbose) {
console.log('Hidden invalid:', JSON.stringify(cleanHidden));
}
}
if (!phoneFound && phoneRawValue && phoneRawValue.length >= 4) {
var normalized = normalizePhone(phoneRawValue);
if (normalized && normalized.length >= 5) {
phoneFound = normalized;
if (verbose) console.log('Phone from raw (normalized):', normalized);
} else if (verbose) {
console.log('Raw normalize result:', JSON.stringify(phoneRawValue), '->', JSON.stringify(normalized));
}
}
if (phoneFound) {
capturedValues.phone = phoneFound;
} else if (verbose) {
console.log('Kein Phone gefunden.');
}
var cuidEl = document.querySelector('input[name="' + CFG.cuidField + '"]');
if (cuidEl && cuidEl.value !== CUID) { cuidEl.value = CUID; }
}
function writeCookie() {
if (!capturedValues.email || capturedValues.email.indexOf('@') === -1) {
console.log('[' + CFG.cookie + '] Skip: Email nicht valid');
return;
}
var encoded = btoa(unescape(encodeURIComponent(JSON.stringify(capturedValues))));
var expires = new Date();
expires.setTime(expires.getTime() + 5 * 60 * 1000);
document.cookie = CFG.cookie + '=' + encoded +
'; expires=' + expires.toUTCString() +
'; path=/; SameSite=Lax; Secure';
console.log('[' + CFG.cookie + '] Cookie geschrieben (' + VERSION + '):');
console.log(' Email:', capturedValues.email);
console.log(' Name:', capturedValues.first_name, capturedValues.last_name);
console.log(' Phone:', capturedValues.phone || '(leer)');
console.log(' CUID:', capturedValues.conversion_uid);
}
function captureAndWrite() {
pollAndCache(false);
writeCookie();
}
function attachListeners() {
var forms = document.querySelectorAll('form[id^="gform_"]');
if (forms.length === 0) return;
forms.forEach(function(form) {
if (form.dataset.spUdAttached) return;
form.dataset.spUdAttached = 'true';
form.addEventListener('submit', captureAndWrite);
var submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
if (submitBtn) {
submitBtn.addEventListener('click', function() {
setTimeout(captureAndWrite, 0);
});
}
});
console.log('[' + VERSION + '] Listeners auf', forms.length, 'Form(s)');
}
setInterval(function() { pollAndCache(false); }, 300);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', attachListeners);
} else {
attachListeners();
}
if (typeof jQuery !== 'undefined') {
jQuery(document).on('gform_post_render', attachListeners);
}
var attempts = 0;
var formPoll = setInterval(function() {
attempts++;
attachListeners();
if (attempts > 20) clearInterval(formPoll);
}, 500);
// Debug: neuer Name + Legacy-Aliase (Muskelgedaechtnis)
function udDebug() {
pollAndCache(true);
console.log('[' + VERSION + ' DEBUG] Aktueller Cache:', JSON.parse(JSON.stringify(capturedValues)));
return capturedValues;
}
window.udDebug = udDebug;
window.demoUdDebug = udDebug;
window.gfUdDebug = udDebug;
/* ---------------- Click-ID Cookie Fallback v1.2 ---------------- */
var FIELD_MAPPING = CFG.cic;
var COOKIE_MAP = {
'gclid': { primary: '_gcl_aw', fp: 'FPGCLAW' },
'wbraid': { primary: '_gcl_gb', fp: 'FPGCLGB' },
'gbraid': { primary: '_gcl_ag', fp: 'FPGCLAG' }
};
function getCookie(name) {
var parts = document.cookie.split('; ');
for (var i = 0; i < parts.length; i++) {
var idx = parts[i].indexOf('=');
if (idx > -1 && parts[i].substring(0, idx) === name) {
return decodeURIComponent(parts[i].substring(idx + 1));
}
}
return null;
}
// Format A: GCL.{timestamp}.{value} (_gcl_aw, _gcl_gb)
function parseGCL(raw) {
if (!raw) return null;
var parts = raw.split('.');
if (parts.length >= 3 && parts[0] === 'GCL') {
return parts.slice(2).join('.');
}
return null;
}
// Format B: {v}.{d}.k{value}$i{timestamp} (_gcl_ag + alle FP-Mirror)
function parseK(raw) {
if (!raw) return null;
var kIdx = raw.indexOf('.k');
if (kIdx === -1) return null;
var dollarIdx = raw.indexOf('$', kIdx);
if (dollarIdx > kIdx) {
return raw.substring(kIdx + 2, dollarIdx);
}
return null;
}
function extractClickId(primaryName, fpName) {
var raw = getCookie(primaryName);
var val = parseGCL(raw) || parseK(raw);
if (val) return val;
if (fpName) {
val = parseK(getCookie(fpName));
if (val) return val;
}
return null;
}
function extractFbcCookie() {
var raw = getCookie('_fbc');
if (!raw) return null;
var parts = raw.split('.');
if (parts.length >= 4 && parts[0] === 'fb') {
return parts.slice(3).join('.');
}
return null;
}
function getCookieValue(key) {
if (COOKIE_MAP[key]) return extractClickId(COOKIE_MAP[key].primary, COOKIE_MAP[key].fp);
if (key === 'fbclid') return extractFbcCookie();
if (key === 'msclkid') return getCookie('_uetmsclkid');
if (key === 'li_fat_id') return getCookie('li_fat_id');
return null;
}
function fillEmptyFields() {
var filled = 0;
Object.keys(FIELD_MAPPING).forEach(function(fieldName) {
var inputs = document.querySelectorAll('input[name="' + fieldName + '"]');
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].value) continue; // URL-Wert nie ueberschreiben
var val = getCookieValue(FIELD_MAPPING[fieldName]);
if (val) {
inputs[i].value = val;
filled++;
}
}
});
if (window.console && filled > 0) {
console.log('[' + VERSION + '/cic] Filled ' + filled + ' hidden field(s) from cookies');
}
}
if (document.readyState !== 'loading') {
fillEmptyFields();
} else {
document.addEventListener('DOMContentLoaded', fillEmptyFields);
}
if (window.jQuery) {
jQuery(document).on('gform_post_render', fillEmptyFields);
}
window.__cicFallback = {
version: VERSION + '/cic-v1.2',
rerun: fillEmptyFields,
inspect: function() {
var result = {};
Object.keys(FIELD_MAPPING).forEach(function(fieldName) {
var key = FIELD_MAPPING[fieldName];
var input = document.querySelector('input[name="' + fieldName + '"]');
result[key] = {
fieldName: fieldName,
fieldFound: !!input,
fieldValue: input ? input.value : null,
cookieValue: getCookieValue(key),
source: input && input.value
? (new URLSearchParams(window.location.search).get(key) === input.value
? 'URL'
: (getCookieValue(key) === input.value ? 'Cookie' : 'Unknown'))
: 'Empty'
};
});
console.table(result);
return result;
}
};
window.__cicFallbackSignup = window.__cicFallback;
window.__cicFallbackKontakt = window.__cicFallback;
})();
</script>
<?php
}
