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.
- Goal events — the event a journey is optimizing for, used for calibrated impact and adaptive branch attribution.
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
PROJ#<projectKey>scoping as everything else.
List trigger contracts
/api/journey-triggersList all trigger contracts in the workspace.
[
{
"id": "trig-signup",
"projectKey": "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 | AudiencePredicate | Pre-filter applied at trigger time before the journey audience runs. Same predicate language as audiences. |
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 audience predicate.
- If the audience matches, compute a deterministic execution name:
<projectKey>__<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-projectKey trigger cache (60s TTL) keeps the hot path fast — typical added latency on /api/events is < 5ms.
Goal events
A journey's goal event is configured on the journey itself (not on the trigger). See goalEventName and goalEventWindowDays in the Journeys API.
When the goal event fires for a subject who has an active execution on a journey:
- The trigger evaluator sees the goal event come through
/api/events. - Goal attribution checks
JEXEC#records: any execution withstatus: succeededorstatus: runningfor this journey/subject within the attribution window is counted. - Journey-level: bumps the
JIMPACT#converted-exposed counter. - Branch-level: for any adaptive branch the subject traversed, bumps the
JBARM#goalEventscounter for the arm they were assigned to.
Censoring rules (per the council Stage-2 review):
failedandtimed_outexecutions are censored — neither denominator nor numerator bumps.- Goal events outside the attribution window don't credit.
- A subject can credit at most once per journey-version (deduplicated via
JIMPACT#+JBARM#write-once-per-subject conditional).
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:
- Goal events from held-out subjects credit the holdout cohort.
- Goal events from exposed subjects 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 }}.
"Goal 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 goal 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 - Audiences API — predicate language
- Calibrated Impact concept — measurement methodology