FullVision
Audiences

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:

OpShapeCH 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):

FieldMeaning
pathLanding page path on the destination domain.
hostNormalized host (scheme stripped, leading www. stripped, lowercased).
channelGranular channel (e.g. google, chatgpt, linkedin).
channel_category9-enum bucket: Organic Search / AI / Organic Social / Paid Search / Paid Social / Email / Direct / Referral / Other.
mediumutm_medium.
campaignutm_campaign.
referrer_hostNormalized 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. count is 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. count MUST be null. 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's is_time_windowed flag stays false iff every leaf uses ever.

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.

CodeWhen
invalid_envelopeBody is not { version: 1, root: <node> } (wrong field name, wrong version, missing root).
unknown_fieldFilter / predicate key not in the leaf's whitelist.
invalid_countop: "did" without a count { op, value }, or a malformed count object.
unknown_opop value not in the supported enum for the predicate kind.
unanchored_negationA not or did_not leaf has no positive sibling in its enclosing and.
version_unsupportedversion 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.

On this page