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.
Search
?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.