Bidirektionaler CRM-Sync ohne Endlosschleifen — Maklerverwaltung ↔ GoHighLevel
Sie kennen das Bild: Datensatz A schreibt in System B, System B schreibt zurück in A, A interpretiert das als Update und schreibt erneut. Die Plattform Truto.one hat dafür den Begriff Vampire Records geprägt — Datensätze, die sich gegenseitig am Leben halten, ohne dass je ein menschlicher Edit dahintersteht. Solche Schleifen sind nicht laut, sie fressen still: API-Quota, Aktivitäts-Logs, später Vertrauen, sobald Makler im CRM Aktualisierungen sehen, die niemand getätigt hat.
Im Mandat zwischen einer Maklerverwaltung und GoHighLevel haben wir genau dieses Pattern frühzeitig isoliert. Der Beitrag fasst zusammen, an welchen vier Stellen CRM-Syncs typischerweise brechen und was beim GHL-↔-Maklerverwaltungs-Profil konkret anders ist als bei einer HubSpot-Anbindung.
Die vier Fehler-Kategorien beim CRM-Sync:
Rate Limits sind der harmloseste Bruch, weil er sichtbar ist. GoHighLevel deckelt pro Sub-Account, die Maklerverwaltungs-API hat eine eigene Bursts-Politik. Wer Backfills ohne Token-Bucket fährt, bekommt 429er in Wellen — und der Retry-Handler frisst dann die nächste Welle gleich mit auf. Lösung: deklarative Throttle pro Endpoint, kein generischer Retry.
Webhook-Timing ist der unterschätzte Bruch. Webhooks feuern eventually, nicht atomar. Ein Contact-Update auf System A löst einen Webhook aus, bevor das Folge-Event (z. B. Custom-Field-Update) committed ist. Wer den Webhook als Trigger für einen Read-Back nutzt, liest einen halben Zustand. Lösung: kurzes Debounce-Fenster plus Event-Versionsnummer aus dem Quell-System, nicht der lokale Zeitstempel.
Schema-Mismatch ist der teuerste Bruch, weil er erst spät auffällt. GHL-Custom-Fields haben pro Sub-Account eigene IDs, die Maklerverwaltung kennt eigene Pflicht-Felder pro Tarif-Sparte. Ein generisches Mapping-File für alle Mandanten existiert nicht — es existiert pro Sub-Account, und es muss versioniert sein, sonst laufen Daten in stille Felder. Lösung: Mapping als Daten, nicht als Code, und Drift-Check beim Boot.
Infinite Loops sind das Vampire-Records-Problem. Standard-Heuristik ist ein "modified by integration"-Flag oder eine Source-Spalte. Beides funktioniert, solange beide Systeme das Flag respektieren. GHL und Maklerverwaltung kennen kein gemeinsames Vokabular, also haben wir eine Dedup-Hash-Strategie gewählt: vor jedem Schreiben Hash über das Ziel-Payload bilden, mit dem zuletzt gesehenen Hash vergleichen, gleich = nicht schreiben.
Was an GHL ↔ Maklerverwaltung anders ist als an HubSpot:
GoHighLevel ist kein Single-Tenant-CRM. Jeder Mandant lebt in einem Sub-Account mit eigenem OAuth-Scope, eigenem Token, eigenen Custom-Field-IDs. Eine Integration, die für einen Sub-Account stabil läuft, ist beim zweiten nicht automatisch stabil — die IDs sind andere, die installierten Apps sind andere, die Webhook-Subscriptions sind andere. Das verschiebt den Fokus von "wir bauen einen Sync" zu "wir bauen einen Sync, der pro Sub-Account konfigurierbar ist und sein Mapping aus einer Konfig zieht".
Die Maklerverwaltung hat einen anderen Race-Condition-Profil als HubSpot. Tokens werden rotiert, während laufende Webhooks noch in Flight sind. Bei HubSpot sind Token-Lifetimes lang genug, dass das praktisch nie kollidiert. Hier kann der Webhook-Worker mit einem Token starten, der drei Sekunden später invalide ist — und der nächste Refresh fällt ausgerechnet in die Verarbeitung des nächsten Events.
Live-Sandbox-Validierung als Pflichtschritt:
Wir haben den Sync nicht gegen Mocks gebaut, sondern gegen eine Live-Sandbox in beiden Systemen. Der Grund ist nüchtern: Race Conditions zwischen OAuth-Refresh und Webhook-Feuer tauchen in Mocks nicht auf. Mocks deterministisch zu machen ist das ganze Geschäftsmodell von Mocks — sie produzieren das Verhalten, das man erwartet, nicht das, was eine echte API unter Last produziert. Token-Ablauf während Webhook-Verarbeitung, partielle Schreiboperationen bei 429, doppelt zugestellte Webhooks: alles Sandbox-Symptome, keine Mock-Symptome.
Konkret hieß das: jeden Sync-Pfad gegen die Sandbox triggern, dazu künstlich Token-Rotationen erzwingen und Webhook-Replays parallel laufen lassen. Erst wenn der Sync diesen Stress übersteht, geht er in den Mandanten-Rollout.
Praxisbeleg mit Pratfall:
Ich hatte zwei Wochen verbrannt, weil ich die Race Condition zuerst falsch eingegrenzt habe. Meine Hypothese war, dass der Webhook nach dem Token-Refresh feuerte und mit einem stale Cache-Eintrag arbeitete. Ich habe den Cache-Layer dreimal umgebaut, ohne dass die sporadischen 401er aufhörten. Erst der dritte Reproduktions-Versuch im Live-Sandbox hat das Pattern offengelegt: der Webhook feuerte vor dem Token-Refresh, nicht danach. Der Worker griff zu einem Token, dessen Refresh gerade in der gleichen Iteration angestoßen wurde — der Sandbox-Lauf hat das durch erzwungene parallele Refreshes sichtbar gemacht, der Mock hatte es serialisiert.
Die Lehre daraus war nicht "mehr Tests", sondern "frühere Sandbox". Hätten wir den Sandbox-Stress-Pfad in Woche eins statt in Woche drei gefahren, wäre das Pattern direkt gefallen. Bei jedem neuen OAuth-Sync gehen wir seitdem so vor: Sandbox-Stress vor Mapping-Feinheiten, Race-Profile vor Field-Mappings, sichtbarer Token-Refresh vor "schöner" Architektur.
Der Sync läuft seitdem stabil über mehrere Sub-Accounts, Vampire Records sind ausgeblieben, und das Mapping pro Mandant ist eine Konfig-Datei, kein Deploy.