Zum Inhalt springen
klaviyo 2026-08-07

Klaviyo Profile & Newsletter: Wo die Docs nicht helfen

external_id erzeugt doppelte Klaviyo-Profile neben dem Shopify-Sync. Und Unsubscribe braucht einen Klaviyo-Flow mit Webhook statt eines API-Calls.


Klaviyo-Profil-Merge-Diagramm zeigt, wie external_id doppelte Profile neben Shopify-E-Mail-Profilen erzeugt

Für CTOs, Tech Leads und Senior Developer, die Klaviyo-Profilidentifikation oder Klaviyo-Newsletter-Subscriptions in eine Flutter-E-Commerce-App integrieren — oder debuggen, warum ihre Klaviyo-Profile sich verdoppeln.

Klaviyo × Flutter Serie (Teil 4 von 4): Planung & Scope · Der Analytics-Layer · Push-Notification-Fallstricke · Profile & Newsletter.

TL;DR: Zwei Klaviyo-Features, die wie 30-Minuten-Aufgaben aussehen, verstecken Datenkorruptionsfallen. external_id zusammen mit der E-Mail zu senden hat Klaviyos Auto-Merge unterdrückt und jedes Profil verdoppelt — der Fix war eine entfernte Zeile und eine hinzugefügte Test-Assertion. Newsletter-Abmeldung hat keinen clientseitigen Endpoint; das funktionierende Pattern läuft über einen Klaviyo-Flow mit Webhook.


Zwei Features, die in den Docs trivial aussehen

Profilidentifikation und ein Newsletter-Toggle. Ein API-Endpoint pro Feature. Fünf-Minuten-Integration laut Dokumentation. Und beide haben lautlos Produktionsdaten korrumpiert — keine Errors, keine Crashes, nur doppelte Profile, die sich wochenlang ansammelten, und Abmeldeanfragen, die im Nichts verschwanden.

Dieser Beitrag behandelt zwei API-Level-Fallen, die ich bei einer Klaviyo-Integration in eine produktive Flutter-E-Commerce-App für eine Shopify-basierte DTC-Schmuckmarke getroffen habe. Der Übersichtsbeitrag zeigt den gesamten Integrationsumfang; der Push-Notification-Beitrag behandelt die Plattform-Fallstricke. Hier geht es um die Datenschicht — wo der Schaden unsichtbar ist, bis jemand nachschaut.

Falle 1: external_id verdoppelt leise deine Profile

Klaviyos Profil-Identifikations-API akzeptiert mehrere Identifier: email, phone_number und external_id. Die Dokumentation beschreibt external_id als Möglichkeit, ein Klaviyo-Profil mit einer ID aus dem eigenen System zu verknüpfen. Wenn deine App über Shopify authentifiziert und du eine Shopify-Kunden-ID hast, scheint es naheliegend, diese als external_id zu übergeben. Stärkere Identität. Zuverlässigere Verknüpfung.

Es ist das Gegenteil.

Wie Klaviyos Merge-Logik funktioniert

Klaviyo führt Profile automatisch zusammen, wenn sie eine email oder phone_number teilen. Wenn die Shopify-Backend-Integration ein Profil mit email: user@shop.example.com anlegt und deine App später ein Profil mit derselben E-Mail identifiziert, mergt Klaviyo sie zu einem Datensatz. Events, Properties, Listenmitgliedschaften — alles vereint.

Aber external_id nimmt an dieser Merge-Logik nicht teil. Es erzeugt eine separate „Identifier Group”. Wenn deine App sowohl email als auch external_id zusammen sendet, behandelt Klaviyo die external_id als starken Identitätsanker. Statt mit dem bestehenden Email-only-Profil aus dem Shopify-Sync zusammenzuführen, erzeugt es ein zweites Profil — gleiche E-Mail, verschiedenes Identitäts-Silo.

Das Ergebnis: Jeder User, der sich in der App einloggt, bekommt ein doppeltes Klaviyo-Profil. Eines von der Shopify-Backend-Integration (identifiziert per E-Mail), eines von der App (identifiziert per external_id + E-Mail). Gleiche Person, zwei Profile, geteilte Event-History.

Das Vorher und Nachher

Die erste Implementierung übergab die Shopify-Kunden-ID als external_id:

// VORHER: external_id unterdrückt Klaviyos E-Mail-basierte Auto-Merge
Future<void> _syncProfile(Customer customer) async {
  await _analyticsDispatcher.setProfile(
    externalId: customer.id,  // Shopify-Kunden-ID
    email: customer.email,
    firstName: customer.firstName,
    lastName: customer.lastName,
    properties: {
      if (customer.countryCode != null) 'app.country': customer.countryCode,
    },
  );
}

Die Lösung war, external_id komplett zu entfernen und nur per E-Mail zu identifizieren:

// NACHHER: Email-only-Identifikation — Klaviyo führt automatisch
// mit Profilen aus anderen Quellen (Shopify, Backend) zusammen,
// die dieselbe E-Mail haben. external_id verhindert diesen Merge.
Future<void> _syncProfile(Customer customer) async {
  await _analyticsDispatcher.setProfile(
    email: customer.email,
    firstName: customer.firstName,
    lastName: customer.lastName,
    properties: {
      if (customer.countryCode != null) 'app.country': customer.countryCode,
    },
  );
}

Eine Zeile entfernt. Die Tests sichern das ab:

verify(() => analyticsDispatcher.setProfile(
  externalId: null,  // festgenagelt — external_id darf nicht gesendet werden
  email: 'user@shop.example.com',
  firstName: 'Jane',
  lastName: 'Doe',
  properties: {'app.country': 'DE'},
)).called(1);

Die externalId: null-Assertion ist Absicht. Ohne sie würde jemand, der die Kunden-ID „für besseres Tracking” wieder hinzufügt, die Test-Suite bestehen und die Duplizierung lautlos wieder einführen.

Warum das schwer zu finden ist

Die Duplizierung erzeugt keinen Fehler. Die API gibt 200 OK zurück. Die App funktioniert. Events werden getrackt. Das Problem taucht erst auf, wenn ein Marketer die Profilliste öffnet und zwei Einträge für denselben Kunden sieht — oder wenn Flow-E-Mails doppelt feuern, oder wenn Segmentierungs-Zahlen aufgebläht sind. Bis dahin können Hunderte oder Tausende Profile dupliziert sein. Da Klaviyo pro aktivem Profil abrechnet, treibt jedes Duplikat die Marketing-Kosten hoch — ein Datenqualitäts-Bug, der leise zum Kostenproblem wird.

Nachträglich kannst du Profile manuell in der Klaviyo-UI oder über die Profile-Merge-API zusammenführen. Aber die nachhaltige Lösung liegt upstream: Hör auf, die Duplikate zu erzeugen.

Der Guest-Mode-Sonderfall

Eine verwandte Entscheidung: Was passiert, wenn ein eingeloggter User sich ausloggt und als Gast weiterbrowst?

Der Instinkt ist, Klaviyos Identität zurückzusetzen — Klaviyo.resetProfile() aufrufen, um die Zuordnung zu löschen. Das Problem: Das erzeugt ein neues anonymes Profil in Klaviyo. Jede Screen View, jeder Produkt-Tap aus der Gast-Session erzeugt Events, die an einer profillosen Identität hängen. Wenn der User sich wieder einloggt, mergen diese anonymen Events nicht nachträglich.

Die bewusste Entscheidung war, die Identität beim Logout nicht zurückzusetzen. Die Browsing-Events des Gasts hängen weiter am zuletzt identifizierten Profil. Das ist ein Trade-off: Die Daten sind leicht ungenau (die Views einer Gast-Session werden dem letzten eingeloggten User zugeordnet), aber es vermeidet das Erzeugen frischer anonymer Profile, die die Event-History weiter fragmentieren.

Wenn deine App mehrere User auf demselben Gerät unterstützt, dreht sich dieser Trade-off um — dann musst du die Identität zurücksetzen, um Cross-Contamination zu vermeiden. Für eine typische Single-User-E-Commerce-App ist das Warmhalten der Identität der bessere Default. Beachte, dass das Zuordnen von unauthentifiziertem Browsing zu einem identifizierten User unter der DSGVO datenschutzrelevant ist — bespreche diesen Trade-off mit deinem Datenschutzbeauftragten vor dem Release.

Falle 2: Subscribe und Unsubscribe sind asymmetrisch

Newsletter-Subscription über Klaviyos Client-API ist unkompliziert. Die App sendet einen POST /client/subscriptions-Request mit der E-Mail des Users, dem Consent-Status und der Ziel-Listen-ID. Kein privater Key nötig — der Request wird über den öffentlichen API-Key (Company-ID) geoscoped.

Der Subscribe-Call

Future<void> subscribeToNewsletter({required String email}) async {
  final listId = _newsletterListId;
  if (listId == null || listId.isEmpty) {
    developer.log(
      'KLAVIYO_NEWSLETTER_LIST_ID not configured, skipping subscribe',
      name: 'KlaviyoSubscriptionService',
    );
    return;
  }
  await _dio.post<dynamic>(
    '/client/subscriptions',
    queryParameters: {'company_id': _companyId},
    data: _buildBody(email: email, listId: listId),
  );
}

Der JSON:API-Body folgt Klaviyos Subscription-Format:

Map<String, dynamic> _buildBody({
  required String email,
  required String listId,
}) {
  return {
    'data': {
      'type': 'subscription',
      'attributes': {
        'custom_source': 'Mobile App',
        'profile': {
          'data': {
            'type': 'profile',
            'attributes': {
              'email': email,
              'subscriptions': {
                'email': {
                  'marketing': {'consent': 'SUBSCRIBED'},
                },
              },
            },
          },
        },
      },
      'relationships': {
        'list': {
          'data': {'type': 'list', 'id': listId},
        },
      },
    },
  };
}

Das custom_source-Feld taggt die Subscription mit ihrem Ursprung — nützlich in Klaviyos UI zum Filtern, woher Subscriber kommen. Die Listen-ID kommt aus der Umgebungskonfiguration (eine Liste pro Umgebung: Staging, Produktion). Der Consent-Wert SUBSCRIBED ist Klaviyos expliziter Opt-in-Marker.

Dieser Endpoint funktioniert. Das Profil wird zur Liste hinzugefügt. Die Subscribe-Hälfte ist erledigt.

Es gibt kein clientseitiges Unsubscribe

Hier bricht die Symmetrie. Klaviyos Client-API kann ein Profil zu einer Liste subscriben. Sie kann es nicht abmelden. Es gibt kein DELETE /client/subscriptions, kein consent: 'UNSUBSCRIBED', das du aus der App senden könntest. Die serverseitige API (profile-subscription-bulk-delete-jobs) erfordert einen privaten API-Key — der darf nie in einer mobilen App ausgeliefert werden.

Das funktionierende Pattern nutzt Klaviyos eigene Flow-Engine als Vermittler:

  1. Die App emittiert ein Custom Event. Wenn der User den Newsletter abschaltet, trackt die App ein app_newsletter_unsubscribe-Event über den Analytics-Dispatcher. Kein Unsubscribe-API-Call — nur ein Event.

  2. Ein Klaviyo-Flow triggert auf dieser Metrik. Im Klaviyo-Dashboard ist ein Flow konfiguriert mit dem Trigger „Person does app_newsletter_unsubscribe.” Das läuft komplett in Klaviyos Infrastruktur.

  3. Der Flow ruft einen Webhook auf. Die Action des Flows ist ein Webhook, der Klaviyos eigenen profile-subscription-bulk-delete-jobs-Endpoint aufruft, authentifiziert mit dem privaten API-Key, der in Klaviyo gespeichert ist. Der private Key verlässt Klaviyos Infrastruktur nie — er ist in der Webhook-Action konfiguriert, nicht in der App.

Der Call-Site in der App ist sauber:

if (hasAcceptedNewsletter) {
  await _klaviyoSubscriptionService.subscribeToNewsletter(
    email: customer.email,
  );
} else {
  await _analyticsDispatcher.trackEvent(
    eventName: 'app_newsletter_unsubscribe',
  );
}

Subscribe ist ein direkter API-Call. Unsubscribe ist ein Event, das einen Flow triggert. Asymmetrisch by design, weil Klaviyo die Unsubscribe-Fähigkeit nicht auf dem Security-Level der Client-API exponiert.

Die Debugging-Implikation

Diese Asymmetrie hat eine direkte Konsequenz fürs Debugging: Wenn ein User sagt „Ich habe den Newsletter abgeschaltet, aber ich bekomme immer noch E-Mails”, ist die App mit ziemlicher Sicherheit nicht das Problem.

Die Verantwortung der App endet beim Emittieren des app_newsletter_unsubscribe-Events. Du kannst das im Activity-Feed des Klaviyo-Profils verifizieren — wenn das Event dort auftaucht, hat die App ihren Job gemacht. Alles danach ist ein Klaviyo-Flow-Problem:

  • Ist der Flow aktiv?
  • Ist die Trigger-Metrik richtig geschrieben?
  • Hat der Webhook gefeuert?
  • Hat der Webhook eine Erfolgsantwort zurückgegeben?

Keine dieser Fragen ist aus den App-Logs beantwortbar. Sie leben in Klaviyos Flow-Analytics und Webhook-Logs. Das bedeutet: Die Ops-Übergabe-Dokumentation für die Integration muss explizit festhalten, dass die App das Event emittiert, der Flow das Unsubscribe übernimmt, und Debugging von Unsubscribe-Fehlern Klaviyo-Dashboard-Zugang erfordert, nicht App-Debugging.

In der Praxis ist der häufigste Fehlermodus, dass der Flow pausiert oder nach dem initialen Setup nie aktiviert wurde. Die App funktioniert einwandfrei. Der Flow existiert noch nicht.

Checkliste vor dem Launch

Bevor du eine Klaviyo-Integration auslieferst, die Profile und Newsletter betrifft, prüfe Folgendes:

  1. Identitäts-Key-Audit. Die App identifiziert Profile nur per E-Mail (oder Telefonnummer). Keine external_id wird gesendet. Tests pinnen externalId: null explizit.

  2. Duplikat-Check gegen Backend-Sync. Lege einen Test-User über Shopify an. Logge dich in der App mit derselben E-Mail ein. Prüfe Klaviyos Profilliste — es sollte exakt ein Profil geben, nicht zwei. Mach das in Staging und Produktion.

  3. Unsubscribe-Flow live und getestet. Der Klaviyo-Flow, der auf app_newsletter_unsubscribe triggert, existiert, ist aktiv, und der Webhook ist mit dem korrekten privaten Key und der korrekten Listen-ID konfiguriert. Schalte den Newsletter in der App ab und verifiziere, dass das Profil innerhalb von Klaviyos Flow-Execution-Window (typischerweise unter einer Minute) von der Liste entfernt wird.

  4. Listen-ID pro Umgebung konfiguriert. Staging und Produktion nutzen verschiedene Klaviyo-Newsletter-Listen-IDs. Stelle sicher, dass beide in der Umgebungskonfiguration gesetzt sind. Eine fehlende Listen-ID sollte den Subscribe-Call graceful überspringen (die Guard-Clause im Code oben macht das), nicht crashen.

  5. Guest-Mode-Identitätsverhalten dokumentiert. Ob die Identität beim Logout zurückgesetzt wird oder warm bleibt, ist eine Produktentscheidung mit Datenimplikationen. Dokumentiere es in der Übergabe, damit das Marketing-Team weiß, was es von Gast-Session-Daten erwarten kann.

Takeaways für dein Klaviyo-Identity-Setup

  • external_id ist keine stärkere Identität — es ist eine separate. Der Name suggeriert, dass es die E-Mail ergänzt. In der Praxis erzeugt es ein paralleles Identitäts-Silo, das Klaviyos E-Mail-basierten Merge unterdrückt. Wenn du nicht das einzige System bist, das in Klaviyo schreibt, ist Email-only-Identifikation der sichere Default.
  • Identitätsverhalten immer in Tests absichern. Die externalId: null-Assertion ist kein Test für das aktuelle Verhalten — sie ist ein Guard gegen einen zukünftigen Entwickler, der den Bug wieder einführt. Identität ist zu subtil, um sie ungeschützt zu lassen.
  • API-Asymmetrie ist eine Design-Entscheidung, kein Bug. Klaviyos Client-API ist auf das beschränkt, was eine mobile App mit einem öffentlichen Key tun dürfen sollte. Jemanden von einer Liste abzumelden ist eine destruktive Aktion, die serverseitige Authentifizierung erfordert. Der Flow-basierte Workaround ist verbose, aber er respektiert die Security-Boundary.
  • Ops-Übergabe ist Teil der Integration. Wenn das Unsubscribe von einem Klaviyo-Flow abhängt, dann ist der Flow Infrastruktur. Er muss dokumentiert, überwacht und in die Übergabe einbezogen werden — nicht einmal aufgesetzt und vergessen.

Doppelte Klaviyo-Profile debuggen oder Newsletter-Flows in deiner Flutter-App verdrahten? Ich habe beide Fixes in Produktion ausgeliefert — buch dir einen Coffee-Chat und ich zeige dir die Identitäts- und Subscription-Patterns, die tatsächlich funktionieren.

Schau dir an, wie ich Flutter-App-Entwicklung End-to-End angehe.

Weiterführende Artikel

Häufig gestellte Fragen

Warum erzeugt Klaviyo doppelte Profile für meine App-Nutzer?

Höchstwahrscheinlich identifiziert deine App Profile mit einer external_id. Klaviyo führt Profile nur bei übereinstimmender E-Mail oder Telefonnummer automatisch zusammen — eine external_id erzeugt ein separates Identitäts-Silo neben dem Profil, das deine Shopify- oder Backend-Integration über dieselbe E-Mail anlegt. Identifiziere nur per E-Mail, und die Profile werden zusammengeführt.

Betrifft das external_id-Duplikat-Problem auch Setups ohne Shopify?

Ja. Das Problem ist nicht Shopify-spezifisch — es tritt auf, wenn irgendein anderes System (Backend, CRM, Import) Klaviyo-Profile per E-Mail anlegt und deine App Profile mit einer external_id identifiziert. Die Merge-Unterdrückung passiert auf Klaviyos Identitäts-Layer, unabhängig davon, welche Integration das erste Profil erstellt hat.

Wie kann eine Mobile-App einen User in eine Klaviyo-Liste eintragen?

Über die Client-API: POST /client/subscriptions, geoscoped über deinen öffentlichen API-Key, mit einem JSON:API-Body, der die E-Mail, Marketing-Consent SUBSCRIBED und eine Beziehung zur Ziel-Listen-ID enthält. Kein privater Key in der App nötig.

Gibt es einen clientseitigen Unsubscribe-Endpoint bei Klaviyo?

Nein. Die Client-API kann anmelden, aber nicht abmelden. Ein funktionierendes Pattern: Die App emittiert ein Custom Event (z.B. app_newsletter_unsubscribe), und ein Klaviyo-Flow triggert auf dieser Metrik und ruft Klaviyos eigenen profile-subscription-bulk-delete-jobs-Endpoint per Webhook mit einem privaten Key auf, der Klaviyo nie verlässt.

KH
Khalit Hartmann Freelance Mobile & Full-Stack Developer