Lifecycle-Ereignisse

Zuletzt aktualisiert: Sep 11, 2026Abschnitt: Integrationen

Lifecycle-Ereignisse lassen Ihre Anwendung Mailoo mitteilen, dass ein identifizierter Benutzer sich registriert oder aktiviert hat. Mailoo legt einen APP_EVENT-Kontakt an/aktualisiert ihn und kann eine On-Event-Kampagne über das SMTP der Integration senden.

Ereignisnamen und Payload-Form sind in @mailoo/forms fest. Ihr Code entscheidet wann gesendet wird (nach Ihrer eigenen Registrierung oder Aktivierung). Erfinden Sie keine Ereignisnamen oder Attributschlüssel.

Das ist getrennt von anonymer Newsletter-Übermittlung (FORM) und Feedback (CONTACT_FORM). Diese nutzen andere Webhooks und Scopes.

Isolation

Zielgruppen und Kampagnen sind an Projekt + FORM- oder CONTACT_FORM-Integration gebunden. Es gibt keine separate „App"-Entität: zwei Produkte brauchen zwei Integrationen (meist zwei API-Schlüssel mit lifecycle.ingest). Kontakte und Status-Abfragen kreuzen integrationId nicht. Newsletter-Submit und Lifecycle-Ingest auf derselben FORM teilen die Subscriber-Tabelle ((integrationId, email) unique); nutzen Sie getrennte Integrationen, wenn Produkte keine Kontakte teilen dürfen.

Identitätsschlüssel

Ein Kontakt hat drei Identifikatoren. Vermischen Sie sie nicht:

  • externalUserId --- Ihre App-Benutzer-ID (z. B. Firebase UID). Pflicht beim Ingest. Stabiler Schlüssel für Status, Retry, Welcome-Idempotenz und consent.changed.
  • email --- normalisierter eindeutiger Join-Schlüssel der Integration. Dient dazu, eine bestehende Newsletter-Zeile beim ersten Ingest mit dem App-Benutzer zu verknüpfen. Die Bulk-Status-Abfrage akzeptiert auch emails= für Kontakte ohne externalUserId.
  • contactId / Subscriber.id --- Mailoo-cuid. Nur im Ingest-JSON zur Fehlersuche. Niemals als Host-Benutzer-ID behandeln; sie entspricht nie der Firebase UID.

Merge-Reihenfolge beim Ingest: zuerst externalUserId, sonst email und UID anhängen, sonst neu anlegen. Zeigen UID und E-Mail auf verschiedene Zeilen → 400.

Legen Sie die E-Mail-Adresse nicht in externalUserId. Die Welcome-Idempotenz ist welcome:{integrationId}:{externalUserId}; E-Mail als UID bricht bei Adresswechsel und kollidiert mit echten UIDs.

Vertrag

Geschlossener Katalog (@mailoo/forms/events):

  • user.registered --- MAILOO_LIFECYCLE_EVENTS.USER_REGISTERED
  • user.activated --- MAILOO_LIFECYCLE_EVENTS.USER_ACTIVATED

Erlaubte attributes-Schlüssel nur: plan, tier, country, signupChannel, referrer. Attribute werden für spätere Segmentierung gespeichert; sie ändern heute nicht, welche Kampagne gewählt wird.

Pflichtfelder im Body:

  • eventName --- Katalogwert (kein Default)
  • idempotencyKey --- eindeutig pro logischem Emit (max. 256)
  • externalUserId --- Benutzer-ID Ihrer App
  • email
  • marketingConsent --- Boolean oder { granted, at?, source? }

Optional: locale, name, attributes.

Unbekannter eventName oder Attributschlüssel → 400 (fail-fast).

Einwilligung ↔ Listenmitgliedschaft. marketingConsent.granted: true löscht unsubscribedAt und setzt marketingConsentAt. granted: false setzt unsubscribedAt und löscht die Einwilligung. Mailoo sendet bei jedem Mitgliedschaftswechsel consent.changed (siehe unten).

Sendeverhalten

Nach gültigem Ingest kann Mailoo einen TransactionalSend mit purpose: LIFECYCLE anlegen und ihn mit dem LifecycleEvent verknüpfen.

Einwilligung. Ohne erteiltes marketingConsent: Event gespeichert, Verarbeitungsstatus SKIPPED mit skipReason SKIPPED_NO_CONSENT --- keine E-Mail.

Abmeldung. Bei gesetztem unsubscribedAt: SKIPPED mit SKIPPED_UNSUBSCRIBED.

On-Event-Kampagne. Mailoo sucht eine ON_EVENT-Kampagne auf derselben Integration mit triggerEvent USER_REGISTERED oder USER_ACTIVATED (passend zum Ereignisnamen) und Status ``SCHEDULED``. Im Dashboard entsteht zuerst DRAFT; ohne Aktivierung (Status SCHEDULED) → SKIPPED_NO_CAMPAIGN. Pro Integration und Trigger nur eine SCHEDULED On-Event-Kampagne.

Idempotenz (zwei Ebenen):

  1. Event: unique (integrationId, idempotencyKey). Wiederholung desselben Schlüssels → 200 mit idempotentReplay: true; kein zweites Event und kein zweiter Versand für diesen Schlüssel.
  2. Versand: welcome:{integrationId}:{externalUserId} für user.registered, und activated:{integrationId}:{externalUserId} für user.activated. Ein zweiter anderer Event-Schlüssel für denselben Benutzer kann für denselben Trigger kein zweites Welcome senden.

API

POST /api/v1/webhooks/lifecycle/{projectUid}/{integrationId}/events

Scope: lifecycle.ingest (oder FULL). Aktive FORM- oder CONTACT_FORM-Integration. Keinen form-only RESTRICTED-Schlüssel wiederverwenden.

Erfolgs-data (auch von Retry): eventId, contactId, idempotentReplay, processingStatus, skipReason, send.

Retry (Admin / fehlgeschlagener Versand):

POST /api/v1/webhooks/lifecycle/{projectUid}/{integrationId}/events/{eventId}/retry

Führt Match/Send für ein gespeichertes Event erneut aus. Erzeugt keinen neuen idempotencyKey. Ist ein Welcome-Send bereits SENT, wird dieses Ergebnis ohne zweite E-Mail zurückgegeben. SKIPPED_NO_CONSENT / SKIPPED_UNSUBSCRIBED werden nicht überschrieben. Bei FAILED-Sends mit persistierter Message wird SMTP erneut versucht; sonst wird eine nicht-SENT Send-Zeile gelöscht und neu gematcht (z. B. nach Korrektur von Kampagne/SMTP).

Status:

GET /api/v1/webhooks/lifecycle/{projectUid}/{integrationId}/status?externalUserId=
GET .../status?externalUserIds=id1,id2&emails=a@b.c,d@e.f
  • Einzelnes externalUserId --- Kontakt + letzte 20 Events/Sends (Legacy-Form).
  • Bulk externalUserIds und/oder emails --- max. 50 IDs insgesamt; Antwort enthält summaries[] mit kompakten Zeilen pro Schlüssel (matchedBy, contactId, marketingConsentGranted, letztes Event, letzter Lifecycle-Send). Nach dem Ingest UID-Schlüssel bevorzugen; E-Mail für Backfill-Zeilen ohne externalUserId.

Dashboard (Bearer JWT):

GET /api/v1/projects/{uid}/integrations/{id}/lifecycle/status

Im Setup-Tab der FORM-Integration: Lifecycle Welcome-Zusammenfassung (Welcome-Kampagne, letztes Event/Send, Zähler). Die UI ist FORM-fokussiert; Ingest akzeptiert weiterhin CONTACT_FORM.

Consent.changed (Delivery-Webhook)

Bei jedem Wechsel der Listenmitgliedschaft (Abmeldung, Restore, FORM-Re-Opt-in, Lifecycle-Einwilligung erteilen/widerrufen) stellt Mailoo in die Warteschlange:

{
  "event": "consent.changed",
  "integrationId": "...",
  "externalUserId": "firebase-uid-or-null",
  "recipientEmail": "user@example.com",
  "marketingConsentGranted": true,
  "unsubscribedAt": null,
  "timestamp": "2026-09-10T12:00:00.000Z"
}

Beim Verlassen der Liste bleibt zusätzlich das Legacy-Payload event: "unsubscribed" für ältere Verbraucher. Konfigurieren Sie deliveryWebhook auf der Integration. In @mailoo/forms: verifyMailooDeliveryWebhook / createDeliveryWebhookHandler und marketingConsentGranted auf Ihr Host-Flag kopieren (z. B. newsletterConsent).

Tracking

Lifecycle-Sends nutzen dasselbe Open/Click-Tracking wie Transactional Mail. TransactionalSend speichert campaignId, lifecycleEventId, openedAt und clickedAt (nur erster Klick --- unique; kein Raw-Zähler; siehe transactional-email{.interpreted-text role="doc"}). Open-Pixel und Click-Redirect-URLs sind öffentliche Transactional-Tracking-Endpunkte.

Server-Emit (empfohlen)

Nachdem Sie den Benutzer in Ihrer Datenbank angelegt haben:

import { ingestMailooLifecycleEvent } from '@mailoo/forms/server'
import { MAILOO_LIFECYCLE_EVENTS } from '@mailoo/forms/events'

const result = await ingestMailooLifecycleEvent({
  eventName: MAILOO_LIFECYCLE_EVENTS.USER_REGISTERED,
  idempotencyKey: `reg:${user.id}`,
  externalUserId: user.id,
  email: user.email,
  marketingConsent: { granted: user.acceptedMarketing, source: 'signup' },
  locale: user.locale,
})
// result.ok && result.data.processingStatus / skipReason / send

Env-Präfix (Standard): MAILOO_LIFECYCLE_{API,API_KEY,PROJECT_UID,ID}.

Browser → BFF

Mounten Sie createLifecycleEventHandler, createLifecycleStatusHandler und createLifecycleRetryHandler aus @mailoo/forms/routes.

Sicherheit: createLifecycleEventHandler ist ein offener Proxy --- jeder Client, der den BFF erreicht, kann E-Mails POSTen. Für identifizierte Benutzer: Session prüfen und externalUserId / email vor dem Forward aus der Session überschreiben (oder den Handler wrappen).

Client-Hooks (@mailoo/forms/hooks):

  • useMailooLifecycleEvent({ endpoint, getHeaders }) --- tipisiertes Ingest-Ergebnis
  • useMailooLifecycleStatus({ endpoint, getHeaders }) --- Bulk-/Einzelstatus
  • useMailooLifecycleRetry({ endpoint, getHeaders }) --- Retry per eventId

Übergeben Sie getHeaders, um Host-Auth anzuhängen (z. B. Authorization: Bearer Firebase-ID-Token). Der Browser sieht nie X-API-Key.

Verwandt

  • website-forms{.interpreted-text role="doc"} --- Newsletter-FORM-Submit
  • website-forms-nextjs-example{.interpreted-text role="doc"} --- Next.js-BFF
  • transactional-email{.interpreted-text role="doc"} --- Single-Recipient-Send-API + Delivery-Webhooks
  • nextjs-packages{.interpreted-text role="doc"} --- Paketübersicht