Analytics API

Query events with counts, timeseries, aggregations, and grouping.

Query model

Analytics shares one canonical query model with the SDK, the API, and the dashboard. The JSON you POST to the API is exactly what the SDK helpers build and what dashboard filters produce, so a query written once behaves identically everywhere.

Every analytics endpoint lives under /v1/projects/:projectId/ and takes a body with an optional filters field and, where a window is meaningful, a required time range.

Filters

A filter is a condition on a field:

json
{
  "field": "data.plan",
  "operator": "equals",
  "value": "pro"
}

Combine conditions with and and or groups, which can nest. Nesting depth is capped at 5 levels.

json
{
  "and": [
    { "field": "type", "operator": "equals", "value": "checkout.completed" },
    {
      "or": [
        { "field": "data.plan", "operator": "equals", "value": "pro" },
        { "field": "data.plan", "operator": "equals", "value": "team" }
      ]
    }
  ]
}

Operators

OperatorDescription
equalsExact match
not_equalsNot equal
containsContains the value
not_containsDoes not contain the value
starts_withStarts with the value
ends_withEnds with the value
inValue is in an array
not_inValue is not in an array
gtGreater than
gteGreater than or equal
ltLess than
lteLess than or equal
existsField is not null
not_existsField is null

The exists and not_exists operators take no value.

Filter fields

Field names resolve to stored columns. Aliases map to their columns, and data.* and context.* paths target nested properties:

FieldResolves to
eventtype
userIduser_id
groupIdgroup_id
sourceIdsource_id
projectIdproject_id
createdAtcreated_at
data.plannested event data
context.countrynested event context

Any other single-segment field resolves against data; deeper paths are written with dots, such as data.line_items.total.

Time ranges

Time values are ISO 8601 datetimes in UTC, always ending in Z:

json
{
  "time": {
    "start": "2026-08-01T00:00:00Z",
    "end": "2026-08-31T23:59:59Z"
  }
}

Provide UTC explicitly. Naive timestamps without a timezone are rejected, and bounds are inclusive. Count and timeseries results are bucketed in UTC.

Run a query

http
POST /v1/projects/:projectId/events/query

Runs a filtered query and returns the matching events with cursor pagination.

json
{
  "filters": { "field": "type", "operator": "equals", "value": "purchase" },
  "time": {
    "start": "2026-08-01T00:00:00Z",
    "end": "2026-08-31T23:59:59Z"
  },
  "pagination": { "limit": 50 }
}

Response:

json
{
  "data": [ "matching events" ],
  "pagination": {
    "nextCursor": "...",
    "hasMore": true
  }
}

Count

http
POST /v1/projects/:projectId/analytics/count

Counts the number of events that match the query.

json
{
  "filters": { "field": "data.plan", "operator": "equals", "value": "pro" },
  "time": {
    "start": "2026-08-01T00:00:00Z",
    "end": "2026-08-31T23:59:59Z"
  }
}

Response:

json
{
  "data": { "count": 1240 }
}

Timeseries

http
POST /v1/projects/:projectId/analytics/timeseries

Buckets matching events over time. The interval is one of minute, hour, day, week, or month.

json
{
  "filters": { "field": "type", "operator": "equals", "value": "$page" },
  "time": {
    "start": "2026-08-01T00:00:00Z",
    "end": "2026-08-31T23:59:59Z"
  },
  "interval": "day"
}

Response:

json
{
  "data": {
    "buckets": [
      { "time": "2026-08-01T00:00:00Z", "count": 320 },
      { "time": "2026-08-02T00:00:00Z", "count": 415 }
    ]
  }
}

Aggregate

http
POST /v1/projects/:projectId/analytics/aggregate

Applies numeric aggregations to fields on matching events. Operations are count, count_unique, sum, avg, min, max, p50, p90, p95, and p99. The count operations need no field; the others reference a field such as data.price.

json
{
  "filters": { "field": "type", "operator": "equals", "value": "purchase" },
  "time": {
    "start": "2026-08-01T00:00:00Z",
    "end": "2026-08-31T23:59:59Z"
  },
  "aggregations": [
    { "name": "total", "field": "data.price", "operation": "sum" },
    { "name": "average", "field": "data.price", "operation": "avg" }
  ]
}

Response:

json
{
  "data": {
    "results": [
      { "name": "total", "value": 24800.5 },
      { "name": "average", "value": 99.2 }
    ]
  }
}

Unique

http
POST /v1/projects/:projectId/analytics/unique

Returns the distinct values found for a field, each with a count. Use it for questions like “which plan values exist” or “how many events per country”.

json
{
  "field": "data.plan",
  "time": {
    "start": "2026-08-01T00:00:00Z",
    "end": "2026-08-31T23:59:59Z"
  }
}

Response:

json
{
  "data": {
    "values": [
      { "value": "free", "count": 820 },
      { "value": "pro", "count": 310 },
      { "value": "team", "count": 110 }
    ]
  }
}

Group

http
POST /v1/projects/:projectId/analytics/group

Groups matching events by one or more fields, such as data.plan or context.country, and returns the count per group.

json
{
  "filters": { "field": "type", "operator": "equals", "value": "user.signup" },
  "time": {
    "start": "2026-08-01T00:00:00Z",
    "end": "2026-08-31T23:59:59Z"
  },
  "groupBy": ["data.plan"],
  "limit": 50
}

Response:

json
{
  "data": {
    "groups": [
      { "data.plan": "free", "count": 420 },
      { "data.plan": "pro", "count": 180 }
    ]
  }
}