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 undconsent.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 auchemails=für Kontakte ohneexternalUserId.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_REGISTEREDuser.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 AppemailmarketingConsent--- 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):
- Event: unique
(integrationId, idempotencyKey). Wiederholung desselben Schlüssels → 200 mitidempotentReplay: true; kein zweites Event und kein zweiter Versand für diesen Schlüssel. - Versand:
welcome:{integrationId}:{externalUserId}füruser.registered, undactivated:{integrationId}:{externalUserId}füruser.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
externalUserIdsund/oderemails--- max. 50 IDs insgesamt; Antwort enthältsummaries[]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 ohneexternalUserId.
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-ErgebnisuseMailooLifecycleStatus({ endpoint, getHeaders })--- Bulk-/EinzelstatususeMailooLifecycleRetry({ endpoint, getHeaders })--- Retry pereventId
Ü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-Submitwebsite-forms-nextjs-example{.interpreted-text role="doc"} --- Next.js-BFFtransactional-email{.interpreted-text role="doc"} --- Single-Recipient-Send-API + Delivery-Webhooksnextjs-packages{.interpreted-text role="doc"} --- Paketübersicht