Audiences API
CRUD endpoints for journey audiences — saved cohort-style predicates that scope which subjects flow through a journey. Phase 1 supports subjectType: "end_user" only.
The predicate language is the same one the trigger evaluator + dry-run + production runtime all consume, so audiences behave identically across surfaces.
List audiences
/api/audiencesList all audiences in the workspace.
[
{
"id": "aud-pro-tier-active",
"projectKey": "pk_live_xyz",
"name": "Pro tier, active in last 7d",
"description": "Used for upgrade nudges and feature announcements",
"subjectType": "end_user",
"predicate": { /* see predicate language below */ },
"createdAt": "2026-04-15T12:00:00.000Z",
"updatedAt": "2026-04-30T14:22:00.000Z"
}
]
Create an audience
/api/audiencesCreate a new audience.
| Parameter | Type | Description |
|---|---|---|
namerequired | string | Display name. |
description | string | Free-form description. |
subjectType | string | Always 'end_user' in Phase 1. Defaults to 'end_user' if omitted. |
predicaterequired | AudiencePredicate | The predicate tree (see below). |
Update / delete
/api/audiences/[id]Update name / description / predicate.
/api/audiences/[id]Delete the audience. Journeys referencing this audience will fail at trigger time until they're updated.
Predicate language
A predicate is one of:
Leaves
{ "kind": "event_fired",
"eventName": "checkout_started",
"window": { "type": "last_n_days", "days": 7 },
"atLeast": 2
}
{ "kind": "event_not_fired",
"eventName": "purchase",
"window": { "type": "last_n_days", "days": 30 }
}
{ "kind": "attribute",
"fieldPath": "tier",
"operator": "equals",
"value": "pro"
}
{ "kind": "in_cohort", "cohortId": "power-users" }
Composites
{ "kind": "and", "clauses": [ <predicate>, <predicate>, ... ] }
{ "kind": "or", "clauses": [ <predicate>, <predicate>, ... ] }
{ "kind": "not", "clause": <predicate> }
Composites compose recursively. Round-trip safety (UI editor ↔ stored predicate) is pinned by 15 tests in app/src/components/journeys/__tests__/AudienceBuilder.test.ts.
Window types
{ "type": "ever" } // any time in the user's history
{ "type": "last_n_days", "days": 7 } // sliding window from now
Attribute operators
equals, not_equals, exists, not_exists, greater_than, less_than, in.
exists / not_exists ignore the value field. in matches comma-separated values (e.g. "value": "pro,enterprise").
Examples
Trial users active in the last 7 days, but not yet converted:
{ "kind": "and", "clauses": [
{ "kind": "in_cohort", "cohortId": "trial" },
{ "kind": "event_fired",
"eventName": "feature.used",
"window": { "type": "last_n_days", "days": 7 } },
{ "kind": "not", "clause": {
"kind": "event_fired",
"eventName": "subscription.upgraded",
"window": { "type": "ever" }
}}
]}
Either pro tier OR an admin in any tier:
{ "kind": "or", "clauses": [
{ "kind": "attribute", "fieldPath": "tier", "operator": "equals", "value": "pro" },
{ "kind": "attribute", "fieldPath": "is_admin", "operator": "equals", "value": true }
]}
De Morgan example — "active and NOT churned":
{ "kind": "and", "clauses": [
{ "kind": "in_cohort", "cohortId": "active" },
{ "kind": "not", "clause": {
"kind": "or", "clauses": [
{ "kind": "in_cohort", "cohortId": "churned" },
{ "kind": "in_cohort", "cohortId": "suppressed" }
]
}}
]}
Validation
The server validates predicates on save. Common rejection reasons:
event_firedwithouteventName→ 400event_name_requiredattributewithoutfieldPath→ 400field_path_requiredin_cohortreferencing a cohort that doesn't exist in the workspace → 400cohort_not_found- Empty
and/orclauses → 400empty_clauses