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:
{
"field": "data.plan",
"operator": "equals",
"value": "pro"
}Combine conditions with and and or groups, which can nest. Nesting depth is capped at 5 levels.
{
"and": [
{ "field": "type", "operator": "equals", "value": "checkout.completed" },
{
"or": [
{ "field": "data.plan", "operator": "equals", "value": "pro" },
{ "field": "data.plan", "operator": "equals", "value": "team" }
]
}
]
}Operators
| Operator | Description |
|---|---|
equals | Exact match |
not_equals | Not equal |
contains | Contains the value |
not_contains | Does not contain the value |
starts_with | Starts with the value |
ends_with | Ends with the value |
in | Value is in an array |
not_in | Value is not in an array |
gt | Greater than |
gte | Greater than or equal |
lt | Less than |
lte | Less than or equal |
exists | Field is not null |
not_exists | Field 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:
| Field | Resolves to |
|---|---|
event | type |
userId | user_id |
groupId | group_id |
sourceId | source_id |
projectId | project_id |
createdAt | created_at |
data.plan | nested event data |
context.country | nested 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:
{
"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
POST /v1/projects/:projectId/events/queryRuns a filtered query and returns the matching events with cursor pagination.
{
"filters": { "field": "type", "operator": "equals", "value": "purchase" },
"time": {
"start": "2026-08-01T00:00:00Z",
"end": "2026-08-31T23:59:59Z"
},
"pagination": { "limit": 50 }
}Response:
{
"data": [ "matching events" ],
"pagination": {
"nextCursor": "...",
"hasMore": true
}
}Count
POST /v1/projects/:projectId/analytics/countCounts the number of events that match the query.
{
"filters": { "field": "data.plan", "operator": "equals", "value": "pro" },
"time": {
"start": "2026-08-01T00:00:00Z",
"end": "2026-08-31T23:59:59Z"
}
}Response:
{
"data": { "count": 1240 }
}Timeseries
POST /v1/projects/:projectId/analytics/timeseriesBuckets matching events over time. The interval is one of minute, hour, day, week, or month.
{
"filters": { "field": "type", "operator": "equals", "value": "$page" },
"time": {
"start": "2026-08-01T00:00:00Z",
"end": "2026-08-31T23:59:59Z"
},
"interval": "day"
}Response:
{
"data": {
"buckets": [
{ "time": "2026-08-01T00:00:00Z", "count": 320 },
{ "time": "2026-08-02T00:00:00Z", "count": 415 }
]
}
}Aggregate
POST /v1/projects/:projectId/analytics/aggregateApplies 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.
{
"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:
{
"data": {
"results": [
{ "name": "total", "value": 24800.5 },
{ "name": "average", "value": 99.2 }
]
}
}Unique
POST /v1/projects/:projectId/analytics/uniqueReturns 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”.
{
"field": "data.plan",
"time": {
"start": "2026-08-01T00:00:00Z",
"end": "2026-08-31T23:59:59Z"
}
}Response:
{
"data": {
"values": [
{ "value": "free", "count": 820 },
{ "value": "pro", "count": 310 },
{ "value": "team", "count": 110 }
]
}
}Group
POST /v1/projects/:projectId/analytics/groupGroups matching events by one or more fields, such as data.plan or context.country, and returns the count per group.
{
"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:
{
"data": {
"groups": [
{ "data.plan": "free", "count": 420 },
{ "data.plan": "pro", "count": 180 }
]
}
}