Criteria DSL
JSON shape for audience criteria — recursive tree of boolean composition over event, pageview, and Stripe leaves.
An audience is a saved segment of your people — everyone who hit pricing twice and never paid, everyone on the Pro plan who churned last quarter — that FullVision keeps up to date and can push to Google Ads, Meta, LinkedIn or a webhook of yours.
Use it when you want that segment built from your product and revenue data rather than exported by hand.
These pages document the audience API on https://db.fullvision.io, under
/v1/audiences. The downloadable OpenAPI spec also carries a second, older
/audiences surface on the gateway host, which these pages do not cover and
which you should not build against.
DSL below means domain-specific language: the JSON shape you write the segment's rules in.
An audience is defined by an AudienceCriteria tree. The compiler walks the tree and emits one ClickHouse SELECT DISTINCT person_id query, which the refresh worker runs on a cadence to populate the audience's membership snapshot. This page documents the JSON shape; the machine-readable schema is AudienceCriteria in the OpenAPI spec.
Overview
{
"version": 1,
"root": {
"op": "and",
"children": [
{ /* leaf or branch */ },
{ /* leaf or branch */ }
]
}
}version is always 1. root is a CriteriaNode — either a branch (and / or / not) or a leaf (a predicate over one source).
Branch nodes
Three branch operators compose children into person sets:
| Op | Shape | CH compile |
|---|---|---|
and | { "op": "and", "children": [...] } | INTERSECT chain |
or | { "op": "or", "children": [...] } | UNION DISTINCT chain |
not | { "op": "not", "child": {...} } | outer EXCEPT child |
Anchored-negation rule
A not (or a leaf with occurrence.op = did_not) MUST appear inside an and that has at least one positive sibling. The positive sibling defines the universe to subtract from.
Valid:
{
"op": "and",
"children": [
{ "source": "product_event", "name": "signed_up", "occurrence": { "op": "did", "count": null, "window": { "kind": "ever" } } },
{ "op": "not", "child": { "source": "product_event", "name": "churned", "occurrence": { "op": "did", "count": null, "window": { "kind": "ever" } } } }
]
}Rejected — top-level negation has no universe to subtract from:
{ "op": "not", "child": { "source": "product_event", "name": "churned", "occurrence": { "op": "did", "count": null, "window": { "kind": "ever" } } } }Returns:
{ "code": "unanchored_negation", "path": "/root", "message": "..." }Leaf nodes — three sources
Every leaf has a source and an occurrence block. The occurrence says what counts (did ≥ N times, or did_not) in what window (ever, last_n_days, or a between date range).
product_event
Filter on event name and whitelisted payload.* fields.
{
"source": "product_event",
"name": "feature_used",
"filter": {
"payload.plan": { "op": "=", "value": "pro" }
},
"occurrence": {
"op": "did",
"count": { "op": ">=", "value": 3 },
"window": { "kind": "last_n_days", "n": 7 }
}
}Whitelist: name is the event name itself; filter keys must be payload.<field>. Unknown field paths are rejected with unknown_field.
pageview
Whitelisted filter fields (post-rename schema, migs 175/178/179):
| Field | Meaning |
|---|---|
path | Landing page path on the destination domain. |
host | Normalized host (scheme stripped, leading www. stripped, lowercased). |
channel | Granular channel (e.g. google, chatgpt, linkedin). |
channel_category | 9-enum bucket: Organic Search / AI / Organic Social / Paid Search / Paid Social / Email / Direct / Referral / Other. |
medium | utm_medium. |
campaign | utm_campaign. |
referrer_host | Normalized referrer host. |
{
"source": "pageview",
"filter": {
"channel_category": { "op": "=", "value": "Organic Search" },
"path": { "op": "=", "value": "/pricing" }
},
"occurrence": {
"op": "did",
"count": { "op": ">=", "value": 1 },
"window": { "kind": "last_n_days", "n": 30 }
}
}stripe — state and event
v1 supports two state entities and three event kinds. Customer-state predicates (entity: customer) are deferred to a future version once a ClickHouse dictionary lands for the relevant columns.
State — assert a current property of a CH-resident Stripe entity:
{
"source": "stripe",
"entity": "subscription",
"predicate": {
"status": { "op": "=", "value": "active" }
}
}Event — assert a Stripe lifecycle event occurred in a window:
{
"source": "stripe",
"event": "first_paid_invoice",
"occurrence": { "window": { "kind": "last_n_days", "n": 90 } }
}Supported event values: first_paid_invoice, subscription_canceled, refund_issued. Lifecycle-event occurrences take window only — no op/count.
Occurrence — window and count
Every leaf's occurrence is { op, count, window }:
op: "did"— the leaf matches persons for whom the event/pageview/state holds.countis required (use{ "op": ">=", "value": 1 }for "at least once").op: "did_not"— the leaf matches persons for whom the event/pageview/state does NOT hold.countMUST benull. Subject to the anchored-negation rule above.
window.kind:
last_n_days— rolling window relative to the refresh tick.between— fixed date range, inclusive on both ends. Use for one-shot audiences pinned to a campaign window.ever— no time bound. The audience'sis_time_windowedflag staysfalseiff every leaf usesever.
Validation errors
A POST /v1/audiences or PATCH /v1/audiences/{id} with invalid criteria returns 400 with:
{
"error": "invalid_criteria",
"errors": [
{ "code": "unknown_field", "path": "$.root.children[0].filter.payload.tier", "message": "field 'payload.tier' is not in the product_event whitelist" }
]
}The same AudienceCriteriaError shape appears in the 200 body of POST /v1/audiences/compile when valid: false.
| Code | When |
|---|---|
invalid_envelope | Body is not { version: 1, root: <node> } (wrong field name, wrong version, missing root). |
unknown_field | Filter / predicate key not in the leaf's whitelist. |
invalid_count | op: "did" without a count { op, value }, or a malformed count object. |
unknown_op | op value not in the supported enum for the predicate kind. |
unanchored_negation | A not or did_not leaf has no positive sibling in its enclosing and. |
version_unsupported | version is not 1. |
Estimating audience size
Before committing a criteria definition, call POST /v1/audiences/compile:
curl -X POST https://db.fullvision.io/v1/audiences/compile \
-H "Authorization: Bearer $FULLVISION_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "criteria_json": { "version": 1, "root": { ... } } }'Response shape:
{
"valid": true,
"is_time_windowed": true,
"estimated_count": {
"count": 1842,
"is_estimate": false,
"hit_limit": false
}
}When the estimator exceeds the 5s ClickHouse budget, the response is:
{
"valid": true,
"is_time_windowed": true,
"estimated_count": {
"count": null,
"is_estimate": true,
"hit_limit": true
}
}count: null with hit_limit: true means "criteria is too broad to estimate inside the budget". The criteria is still valid — you can commit it via POST /v1/audiences and the refresh worker will compute the snapshot at its own cadence. Narrow the criteria (tighter window, additional positive predicates) to get a concrete estimate.
