Documentation menu

People and segments

Module: people. See README.md for authentication and pagination.

Endpoints

Endpoint Scope Permission What it does
GET /apps/{app}/people read people.read Search person profiles
GET /apps/{app}/people/{id} read people.read Read one person dossier
PATCH /apps/{app}/people/{id} write people.write Update stage, tags or CRM fields
GET /apps/{app}/people/{id}/timeline read people.read Events, activities and errors for one person
GET /apps/{app}/segments read people.read List segments
POST /apps/{app}/segments write people.write Create or replace a segment

Personal data

Emails and names are masked unless the token's bearer holds people.pii.read:

{ "id": "p_1", "email": "a**@example.com", "name": "A** L**" }

The row count and the status code are identical either way — only the personal columns differ. A masked value means hidden, never absent; do not reconstruct it.

?q=&stage=&tag=&segment=&limit=50&cursor=&env=

q matches distinct id, email, name and company. tag is repeatable. stage is one of visitor, lead, signup, activated, paying, churned. segment is a segment id, dynamic or static.

A row carries identity, traits, first and last touch attribution, geo, device, counters (sessions, events, pageviews), lifecycleStage, tags and crm.

Update

PATCH /apps/{app}/people/{id}
{ "stage": "paying", "tags": ["vip", "beta"], "crm": { "company": "Acme", "owner": "ada" } }

tags replaces the whole list (trimmed, de-duplicated, sorted, capped at 32). A stage set here is marked manual, which stops the hourly lifecycle rules from moving it later. The response says what actually changed:

{ "changed": ["stage", "tags"], "before": { … }, "after": { … } }

An unknown stage is a 400; an unknown person is a 404.

Timeline

?before=<ISO timestamp>&kind=event|activity|error&limit=50

One chronological stream over three sources — analytics events, CRM activities (notes, stage changes, campaigns, controls) and error occurrences. kind is repeatable; omitting it returns all three.

Segments

POST /apps/{app}/segments
{
  "name": "Paying in France",
  "kind": "dynamic",
  "definition": {
    "all": [
      { "field": "stage", "op": "eq", "value": "paying" },
      { "field": "country", "op": "eq", "value": "FR" },
      { "field": "did_event", "op": "within_days", "value": 30, "event": { "name": "checkout_completed", "count": 1 } }
    ]
  }
}

The definition is { all?: Condition[], any?: Condition[] }. A condition names a field (trait.<k>, property.<k>, stage, tag, first_source, country, device, last_seen_at, first_seen_at, sessions, events, did_event, not_did_event, revenue.status), an operator (eq, neq, contains, in, gt, lt, gte, lte, exists, within_days, not_within_days) and a value.

It is compiled to parameterised SQL against a field whitelist — values are never interpolated — so a definition can be invalid but never dangerous. An invalid one is a 400 naming the field.

Pass id to replace an existing segment. The response includes the live memberCount for a dynamic segment.