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:
- Trigger contracts — versioned mappings from event names to journey-relevant fields, decoupled from the underlying TrackingEvent schema.
- 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
versionAttimestamp; 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
/api/journey-triggersList 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
/api/journey-triggersCreate a new trigger contract.
| Parameter | Type | Description |
|---|---|---|
namerequired | string | Display name. |
eventNamerequired | string | Exact event name from your SDK / snippet (e.g. 'user_signed_up'). |
subjectFieldPathrequired | string | Dot-path to the end-user identity in the event payload. Defaults to 'endUserId'. |
recipientEmailFieldPath | string | Dot-path to the recipient's email in the event payload. Required for email send steps; optional otherwise. |
filter | object | Pre-filter applied at trigger time before the journey Segment runs. Same predicate language as Segments. |
Update / delete
/api/journey-triggers/[id]Update a contract. Bumps versionAt — in-flight executions stay on their pinned version.
/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:
- Resolve subject — extract the end-user ID from
subjectFieldPath. If null/missing, the trigger is skipped (no journey starts). - Resolve recipient email — extract from
recipientEmailFieldPathif set. - Apply pre-filter — if the trigger has a
filterpredicate, evaluate it against the subject. Skip if it doesn't match. - 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>. TheeventIdis the event's stable ID; this ensures the same event firing twice produces only one execution. - Call
sfn.StartExecutionwith 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:
- The trigger evaluator sees the event come through
/api/events. - Attribution checks execution records: any execution with
status: succeededorstatus: runningfor this journey/person within the attribution window is counted. - Journey-level: bumps the converted-exposed counter.
- Experiment-step: if the person traversed an Experiment step, the assigned letter gets the credit.
Censoring rules:
failedandtimed_outexecutions 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.
Related
- Events API — the underlying ingestion endpoint
- Journeys API —
goalEventName+goalEventWindowDaysconfig - Segments API — predicate language
- Calibrated Impact concept — measurement methodology