Journey Events API

Adaptive Journeys consume events from the same /api/events endpoint your snippet and SDK already use. There's no separate "journey events" ingestion path — events are first-class data that journeys subscribe to via trigger contracts.

This page covers two related surfaces:

  1. Trigger contracts — versioned mappings from event names to journey-relevant fields, decoupled from the underlying TrackingEvent schema.
  2. Conversion events — the event a journey is optimizing for, used for calibrated impact and Experiment-step attribution. The field is still goalEventName.

Why trigger contracts (not raw event names)?

Journeys reference event names through a trigger contract layer instead of binding directly to event names in /api/events. The reason: event schemas evolve. If you rename signup to user_signed_up in your SDK, every journey that hard-coded signup would silently break.

A trigger contract is:

  • Versioned — each contract has a versionAt timestamp; journeys pin to a specific version.
  • Field-mapped — the contract maps the event's natural fields (e.g. user_id, email) to journey-runtime fields (endUserId, recipientEmail).
  • Workspace-scoped — same WORKSPACE#<workspaceKey> scoping as everything else.

List trigger contracts

GET/api/journey-triggers

List all trigger contracts in the workspace.

[
  {
    "id": "trig-signup",
    "workspaceKey": "pk_live_xyz",
    "name": "User signed up",
    "eventName": "user_signed_up",
    "subjectFieldPath": "endUserId",
    "recipientEmailFieldPath": "email",
    "versionAt": "2026-04-15T12:00:00.000Z"
  }
]

Create a trigger contract

POST/api/journey-triggers

Create a new trigger contract.

ParameterTypeDescription
namerequiredstringDisplay name.
eventNamerequiredstringExact event name from your SDK / snippet (e.g. 'user_signed_up').
subjectFieldPathrequiredstringDot-path to the end-user identity in the event payload. Defaults to 'endUserId'.
recipientEmailFieldPathstringDot-path to the recipient's email in the event payload. Required for email send steps; optional otherwise.
filterobjectPre-filter applied at trigger time before the journey Segment runs. Same predicate language as Segments.

Update / delete

PATCH/api/journey-triggers/[id]

Update a contract. Bumps versionAt — in-flight executions stay on their pinned version.

DELETE/api/journey-triggers/[id]

Delete the contract. Journeys referencing it will fail to start new executions.

How triggers fire

When /api/events receives an event matching a trigger contract's eventName:

  1. Resolve subject — extract the end-user ID from subjectFieldPath. If null/missing, the trigger is skipped (no journey starts).
  2. Resolve recipient email — extract from recipientEmailFieldPath if set.
  3. Apply pre-filter — if the trigger has a filter predicate, evaluate it against the subject. Skip if it doesn't match.
  4. For each journey in the workspace listening to this trigger:
    • Evaluate the journey's Segment predicate.
    • If the Segment matches, compute a deterministic execution name: <workspaceKey>__<journeyId>__v<version>__<endUserId>__<eventId>. The eventId is the event's stable ID; this ensures the same event firing twice produces only one execution.
    • Call sfn.StartExecution with the subject snapshot. Step Functions' 90-day execution-name uniqueness handles idempotency.

Per-workspaceKey trigger cache (60s TTL) keeps the hot path fast — typical added latency on /api/events is < 5ms.

Conversion events

A journey's conversion event is configured on the journey itself (not on the trigger). See goalEventName and goalEventWindowDays in the Journeys API. The toolbar still says Set goal.

When that event fires for a person who has an active execution on a journey:

  1. The trigger evaluator sees the event come through /api/events.
  2. Attribution checks execution records: any execution with status: succeeded or status: running for this journey/person within the attribution window is counted.
  3. Journey-level: bumps the converted-exposed counter.
  4. Experiment-step: if the person traversed an Experiment step, the assigned letter gets the credit.

Censoring rules:

  • failed and timed_out executions are censored — neither denominator nor numerator bumps.
  • Conversion events outside the attribution window don't credit.
  • A person can credit at most once per journey-version.

See Calibrated Impact for the full estimand definition.

Holdout cohort tracking

When the trigger evaluator decides a subject is in the global holdout, it also persists a HOLDSUB#<journeyId>#v<n>#<endUserId> record. This is what enables calibrated impact's "what would have happened without the journey?" measurement:

  • Conversion events from held-out people credit the holdout cohort.
  • Conversion events from exposed people credit the exposed cohort.
  • The lift is (exposed_rate / holdout_rate) - 1.

The HOLDSUB# record TTLs to triggerTime + goalEventWindowDays + 24h grace so the table stays clean.

Common patterns

"Trigger when the user signs up — but only Pro tier"

// POST /api/journey-triggers
{
  "name": "Pro tier signup",
  "eventName": "user_signed_up",
  "subjectFieldPath": "endUserId",
  "recipientEmailFieldPath": "email",
  "filter": {
    "kind": "attribute",
    "fieldPath": "tier",
    "operator": "equals",
    "value": "pro"
  }
}

"Trigger on a custom event from your SDK"

If your SDK sends apex.track('checkout.completed', { userId: '...', email: '...', total: 99.00 }):

// POST /api/journey-triggers
{
  "name": "Checkout completed",
  "eventName": "checkout.completed",
  "subjectFieldPath": "userId",
  "recipientEmailFieldPath": "email"
}

The total field stays in the event payload and is available to journey templates as {{ event.total }}.

"Conversion event isn't a tracked event yet"

You can use any event name, including ones you'll add later. The journey will simply never credit a conversion until that event starts firing. Use this when you're authoring a journey before the SDK instrumentation lands.