pex

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

GET/api/audiences

List 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

POST/api/audiences

Create a new audience.

ParameterTypeDescription
namerequiredstringDisplay name.
descriptionstringFree-form description.
subjectTypestringAlways 'end_user' in Phase 1. Defaults to 'end_user' if omitted.
predicaterequiredAudiencePredicateThe predicate tree (see below).

Update / delete

PATCH/api/audiences/[id]

Update name / description / predicate.

DELETE/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_fired without eventName → 400 event_name_required
  • attribute without fieldPath → 400 field_path_required
  • in_cohort referencing a cohort that doesn't exist in the workspace → 400 cohort_not_found
  • Empty and / or clauses → 400 empty_clauses