Theme
Read API reference
Everything you can read back out. Generated from the running service, so it cannot drift from the code.
Read your product's analytics: summaries, time series, funnels, retention, user profiles, sessions, replays and experiment results.
Authenticate with a workspace session (Authorization: Bearer <JWE>) or a project API token (Authorization: Bearer tk_…, scope read, bound to one project). The gateway resolves the token to a tenant and a project scope; you never send a tenant or project id yourself, and you cannot read another workspace's data by asking.
Time. Every timestamp you send or receive is ISO-8601 in UTC (2026-09-01T00:00:00Z). Omit from/to and you get the last 30 days ending now. Pass tz (an IANA zone such as Europe/Berlin) where an endpoint buckets by day, week or month and you want those buckets to start at local midnight; the values you send and receive stay UTC either way.
Errors are always {"error": "<code>", "message": "<human-readable>"} with the status code carrying the meaning. Read reliably: 401 means your token was not accepted, 403 means the request reached us without a project scope, 400 means your parameters do not make sense, 409 means you scoped to a cohort that has not been built yet, 503 means we could not reach the service that owns experiment definitions, and 429 means your workspace has exceeded its request budget for the current minute.
Back off on 429. The gateway budgets requests per workspace across every authenticated call, reads included, and answers 429 with a Retry-After header giving the seconds until the window resets. Honour it: retrying immediately just spends the next window too. The budget is per workspace, not per token, so a busy dashboard and a batch export share it.
Result metadata. Endpoints that run a warehouse query return meta.queryMs, meta.asOf, meta.timezone and meta.truncated so you can tell a small answer from a cut-off one.
Server: / — No public API URL is configured for this deployment, so requests resolve relative to the address you fetched this document from. Operators: set query.public-url to your gateway's public address.
Operations
| Endpoint | What it does | |
|---|---|---|
POST | /api/analytics/query | Run one metric, optionally grouped |
POST | /api/analytics/insights/validate | Check an insight spec against your real schema before running it |
POST | /api/analytics/insights/run | Run several series, a breakdown and a formula in one call |
POST | /api/analytics/insights/explain | Explain why a metric moved between two periods |
GET | /api/analytics/timeseries | Events and users over time |
GET | /api/analytics/summary | Headline numbers for a date range |
GET | /api/analytics/summary/compare | Compare a range against the one before it |
GET | /api/analytics/realtime | What is happening right now |
GET | /api/analytics/query/options | List the metrics and dimensions a query may use |
GET | /api/analytics/metrics | How every metric is defined |
GET | /api/analytics/events | Browse the raw event stream |
GET | /api/analytics/breakdown | Rank event types, or the values of one property |
GET | /api/analytics/acquisition | Where your users came from |
POST | /api/analytics/paths | Paths after (or before) an event |
GET | /api/analytics/funnel | Run a simple funnel from a list of event names |
POST | /api/analytics/funnel | Run a funnel with per-step filters |
GET | /api/analytics/retention | Weekly retention cohorts |
GET | /api/analytics/retention/v2 | Retention cohorts, with the mode and events you choose |
GET | /api/analytics/lifecycle | New, current, resurrected and dormant users per period |
POST | /api/analytics/cohorts/refresh | Materialise a cohort (internal) |
POST | /api/analytics/cohorts/preview | Count how many users a cohort definition would match |
GET | /api/analytics/users | List users active in a date range |
GET | /api/analytics/users/{userId} | Profile one user |
GET | /api/analytics/groups | Roll usage up by account |
GET | /api/analytics/groups/keys | List the group keys you are sending |
GET | /api/analytics/cohorts/{id}/status | Check whether a cohort has been built |
GET | /api/analytics/paths | Where people go next |
GET | /api/analytics/sessions | List sessions |
GET | /api/analytics/sessions/{sessionId}/replay | Fetch one session's replay |
GET | /api/analytics/sessions/summary | Session engagement totals |
GET | /api/analytics/heatmap | Heatmap for one page |
GET | /api/analytics/heatmap/pages | List the pages with click data |
GET | /api/analytics/experience | Frustration and feedback in one view |
GET | /api/analytics/experiments/{key}/results | Read one experiment's results |
GET | /api/analytics/experiments/sample-size | How many users an experiment on this goal needs |
GET | /api/analytics/schema/discover | Discover the schema you are actually sending |
GET | /api/analytics/properties | List the event property names in use |
GET | /api/analytics/failed-events | See the events the pipeline refused |
GET | /api/analytics/api/timeseries | API requests, errors and latency over time |
GET | /api/analytics/api/summary | Totals for your own API traffic |
GET | /api/analytics/api/status | Split API responses by status class |
GET | /api/analytics/api/routes | Rank your API routes |
POST | /api/analytics/durations | Durations: one row per unit with how long it ran, whether it failed, and its readings |
POST | /api/analytics/anomalies | Find the days a series did something unusual |
POST | /api/analytics/churn/frame | |
GET | /api/analytics/entities/account-keys | The extra account property names this workspace registered |
PUT | /api/analytics/entities/account-keys | Register extra account property names for this workspace |
GET | /api/analytics/entities/fleet | The fleet: one row per asset with alarms, excursions, trips, last reading and last work order |
GET | /api/analytics/entities/fleet/{accountId}/trend | One asset's condition by day: readings, alarms, excursions and trips |
GET | /api/analytics/entities/accounts | Accounts, ranked by activity, with resolved properties |
GET | /api/analytics/entities/accounts/{accountId} | One account: profile, properties, users, event mix |
POST | /api/analytics/explore/sql | Run a read-only SQL statement over the workspace's own tables |
POST | /api/analytics/explore/report | Run a report: one measure pivoted by a row dimension and time or a second dimension |
POST | /api/analytics/explore/prepare | Rewrite a BI tool's statement for one workspace (internal, warehouse-gateway) |
POST | /api/analytics/explore/export | Write one table of the workspace to the customer's bucket as Parquet (internal) |
POST | /api/analytics/explore/export/probe | Test a bucket: write a one-row probe file (internal) |
GET | /api/analytics/explore/sql/schema | Tables and columns the SQL explorer may read |
POST | /api/analytics/forecast | Forecast the series a spec produces |
POST | /api/analytics/releases/impact | Assess one release against one metric |
GET | /api/analytics/releases | Releases seen in this workspace |
Core reporting
Headline numbers, time series, breakdowns, acquisition and the ad-hoc query builder.
POST /api/analytics/query
Run one metric, optionally grouped
Returns a two- or three-column table for a single metric: bucket and value, plus the group value when you pass groupBy. Use it to back one chart on a dashboard. Metrics and dimensions are allow-listed — ask GET /query/options for the current lists, and anything outside them is refused with 400 rather than interpreted. When you need several series or a formula, use POST /insights/run.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. |
Request body (required)
metric is required and must be one from GET /query/options. groupBy is an allow-listed dimension, interval the bucket width, filters a list of {"property":…,"op":…,"value":…} clauses, limit the number of groups, and from/to behave as elsewhere.
json
{
"metric": "users",
"interval": "day",
"groupBy": "country",
"limit": 5,
"from": "2026-09-01T00:00:00Z",
"to": "2026-09-08T00:00:00Z"
}| Response | Meaning |
|---|---|
200 | The result table and the settings that produced it. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"columns": [
"bucket",
"group",
"value"
],
"rows": [
[
"2026-09-01T00:00:00Z",
"DE",
121
],
[
"2026-09-01T00:00:00Z",
"FR",
88
],
[
"2026-09-02T00:00:00Z",
"DE",
114
]
],
"metric": "users",
"groupBy": "country",
"interval": "day"
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/insights/validate
Check an insight spec against your real schema before running it
Dry-runs an InsightSpec — the body POST /insights/run takes — against the events and properties this workspace actually sends (what GET /schema/discover reports for your project scope) and the measures, filter ops and intervals the engine accepts. Nothing is queried. It exists so a generated spec (from the Ask-in-English model, or any tool) can be checked and repaired before it runs: every error names the JSON path, what is wrong, and where a known name is close, the nearest one (signup → signup_completed). A property is checked on the event it is used with, so plan being sent on subscribed does not make it valid on report_exported.
Unambiguous model quirks are normalised and the corrected spec is returned as normalised — run that, not what you sent: a one-element array where a string was meant, a YYYY-MM-DD date (read as midnight UTC), compare: true (previous period), filter ops such as =/!=/>=, measure and interval spellings with one meaning (count, unique_users, daily), a bare breakdown name that every series' event carries (plan → property:plan), and eq with a list (→ in). Anything with more than one reading — a multi-element array, "2026-09", "last week", an unknown measure or field — is an error, never a guess. Events from another project are unknown here and are refused, exactly as /insights/run would refuse to read them.
Request body (required)
The InsightSpec to check, in the same shape POST /insights/run accepts. Unknown fields at any level are errors.
json
{
"series": [
{
"event": "report_export",
"measure": [
"uniques"
],
"filters": [
{
"property": "template",
"op": "=",
"value": "blank"
}
]
}
],
"breakdown": "country",
"interval": "daily",
"from": "2026-09-01",
"to": "2026-09-08"
}| Response | Meaning |
|---|---|
200 | Always 200, whether or not the spec is valid; read valid. errors is empty and normalised is set when valid; otherwise errors lists every problem found (not just the first) and normalised is null. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"valid": false,
"errors": [
{
"path": "$.series[0].event",
"problem": "Unknown event 'report_export'; it has not been seen in this workspace",
"suggestion": "Did you mean 'report_exported'?"
},
{
"path": "$.series[0].filters[0].property",
"problem": "Property 'template' does not exist on event 'report_export'",
"suggestion": "Did you mean 'format' (on 'report_exported')?"
}
],
"normalised": null
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/insights/run
Run several series, a breakdown and a formula in one call
Returns a table of buckets and one column per series, with totals, an optional comparison period, and meta describing how the query ran. Use it to build an explore view or a saved chart: you can ask for several metrics at once, split them by a dimension, derive a new column with a formula over the series letters (A, B, …), and compare against the previous period — all without issuing several requests. meta.truncated tells you the breakdown was cut at limitApplied, and meta.sql is the exact statement that ran (identifiers resolved, values shown as ? binds) so a result can always show its work. To check a spec without running it, use POST /insights/validate.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. Ignored when the body sets tz. |
Request body (required)
series is required: one to eight {"name":…,"event":…,"measure":…,"property":…,"filters":[…]} entries, named A, B, … by default. event is an event type (* or omitted for all); measure is one of uniques (default), totals, sessions, per_user, unique_groups, sum, avg, min, max, p50, p90, p95, p99 — the numeric ones aggregate property. Each filter is {"property":…,"op":…,"value":…} with op one of eq, neq, contains, not_contains, exists, not_exists, gt, gte, lt, lte, in. breakdown is up to two of an allow-listed dimension (GET /query/options), property:<name> or group:<key>; interval is hour/day/week/month or null for totals; compare is previous_period or previous_year; formula derives a column from the series names; filters holds the global segment and cohort; limit caps breakdown rows (1-100); from/to/tz behave as elsewhere.
json
{
"series": [
{
"name": "A",
"event": "report_exported",
"measure": "uniques"
},
{
"name": "B",
"event": "report_exported",
"measure": "totals",
"filters": [
{
"property": "format",
"op": "eq",
"value": "csv"
}
]
}
],
"breakdown": [
"country"
],
"formula": "B / A",
"interval": "day",
"compare": "previous_period",
"from": "2026-09-01T00:00:00Z",
"to": "2026-09-08T00:00:00Z",
"tz": "Europe/Berlin"
}| Response | Meaning |
|---|---|
200 | The result table, its totals and the query metadata. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"columns": [
"bucket",
"A",
"B"
],
"rows": [
[
"2026-09-01T00:00:00Z",
364,
2761
],
[
"2026-09-02T00:00:00Z",
336,
2545
]
],
"totals": {
"A": 2113,
"B": 18420
},
"previous": {
"totals": {
"A": 1902,
"B": 16044
}
},
"meta": {
"queryMs": 456,
"truncated": false,
"limitApplied": 10,
"asOf": "2026-09-08T00:00:00Z",
"timezone": "UTC",
"sql": "WITH (event_type = ?) AS c0, (event_type = ?) AS c1 SELECT toStartOfDay(event_time, 'UTC') AS bucket, uniqExactIf(user_id, (c0) AND user_id != '') AS s0, countIf(c1) AS s1 FROM events FINAL WHERE tenant_id = ? AND event_time >= toDateTime64(?, 3, 'UTC') AND event_time < toDateTime64(?, 3, 'UTC') AND event_type IN (?, ?) GROUP BY bucket ORDER BY bucket"
}
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/insights/explain
Explain why a metric moved between two periods
Root-cause analysis without a model. Give it the metric that moved — an event and a measure — and the two windows, and it scans the breakdown dimensions (country, device, platform, OS, browser, app version, acquisition source/medium/campaign, channel, referring domain, plan, app) with one grouped query per dimension over both windows, then ranks the segments by their share of the change.
Read the honesty rules before you read the numbers. Every segment's own before/after change is tested — a two-proportion z-test for conversion, a two-count Poisson z-test for uniques/totals/sessions — and because scanning many segments guarantees false positives, all p-values are Benjamini–Hochberg corrected across every segment tested; qValue is what to read, testing.segmentsTested says how many there were. A segment with fewer than minSegmentSize users in either window (default 50, per deployment explain.min-segment-size) is listed under tooSmall with no p-value at all. Each dimension also gets a uniformity chi-square: when no dimension's mix changed, the headline is moved_everywhere — the metric moved in every segment at once, which is a real answer. Every result is marked exploratory: true and carries caveats in plain words: this is observational data, and a segment that moved with the metric is a hypothesis about the cause, never a demonstration of it. dimensions lists what was scanned and what was skipped and why; meta.sql is every statement that ran.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. Ignored when the body sets tz. |
Request body (required)
metric is required: {"event":…,"measure":…,"filters":[…]} where event is an event type, * for any product event, or $conversion for the project's own conversion events; measure is uniques (default), totals, sessions or conversion (users doing the event ÷ active users); filters are {"property":…,"op":…,"value":…} as in /insights/run. current is required ({from,to}, ISO-8601); baseline defaults to the period of the same length immediately before. dimensions (optional, at most 20) picks the breakdowns to scan from the allow-list, property:<name> or group:<key>; minSegmentSize overrides the thin-segment threshold; tz is echoed in meta.
json
{
"metric": {
"event": "signup_completed",
"measure": "conversion"
},
"current": {
"from": "2026-09-01T00:00:00Z",
"to": "2026-09-08T00:00:00Z"
},
"baseline": {
"from": "2026-08-25T00:00:00Z",
"to": "2026-09-01T00:00:00Z"
},
"dimensions": [
"country",
"device",
"utm_source"
],
"minSegmentSize": 50
}| Response | Meaning |
|---|---|
200 | The headline, the overall test, the ranked contributors, the segments too small to assess, the dimensions scanned or skipped, the testing summary, the caveats and the SQL. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"metric": {
"event": "signup_completed",
"measure": "conversion"
},
"current": {
"from": "2026-09-01T00:00:00Z",
"to": "2026-09-08T00:00:00Z"
},
"baseline": {
"from": "2026-08-25T00:00:00Z",
"to": "2026-09-01T00:00:00Z"
},
"headline": {
"kind": "driven_by_segments",
"text": "Most of the change sits in Device = ios (71% of the change). 1 of 9 segments tested moved by more than noise would explain after correction. This is exploratory: these segments moved with the metric; whether they caused the move is a hypothesis to check."
},
"overall": {
"before": 0.1483,
"after": 0.1212,
"change": -0.0271,
"changePct": -18.27,
"sizeBefore": 4120,
"sizeAfter": 4088,
"pValue": 0.00042,
"test": "two-proportion z-test",
"distinguishableFromNoise": true
},
"contributors": [
{
"dimension": "device",
"dimensionLabel": "Device",
"segment": "ios",
"before": 0.161,
"after": 0.098,
"change": -0.063,
"changePct": -39.1,
"sizeBefore": 1650,
"sizeAfter": 1702,
"contribution": 0.71,
"rateEffect": -0.0252,
"mixEffect": 0.0011,
"shareBefore": 0.4,
"shareAfter": 0.416,
"pValue": 0.0000031,
"qValue": 0.000028,
"status": "significant"
},
{
"dimension": "country",
"dimensionLabel": "Country",
"segment": "DE",
"before": 0.15,
"after": 0.14,
"change": -0.01,
"changePct": -6.7,
"sizeBefore": 900,
"sizeAfter": 870,
"contribution": 0.08,
"pValue": 0.54,
"qValue": 0.81,
"status": "not_significant"
}
],
"tooSmall": [
{
"dimension": "utm_source",
"dimensionLabel": "Acquisition source",
"segment": "newsletter",
"before": 0.2,
"after": 0.05,
"sizeBefore": 30,
"sizeAfter": 40,
"status": "too_small",
"note": "Fewer than 50 active users in at least one window (30 before, 40 after); no p-value is computed from that little."
}
],
"dimensions": [
{
"key": "country",
"label": "Country",
"scanned": true,
"segments": 6,
"uniformity": {
"checked": true,
"chiSquare": 3.1,
"degreesOfFreedom": 5,
"pValue": 0.68,
"mixChanged": false
}
},
{
"key": "device",
"label": "Device",
"scanned": true,
"segments": 3,
"uniformity": {
"checked": true,
"chiSquare": 21.4,
"degreesOfFreedom": 2,
"pValue": 0.000022,
"mixChanged": true
}
},
{
"key": "utm_source",
"label": "Acquisition source",
"scanned": false,
"skipped": "not sent: no event in either window carries a value for this dimension"
}
],
"testing": {
"segmentsScanned": 10,
"segmentsTested": 9,
"segmentsTooSmall": 1,
"significantAfterCorrection": 1,
"correction": "benjamini_hochberg",
"threshold": 0.05,
"minSegmentSize": 50,
"expectedFalsePositivesUncorrected": 0.45
},
"exploratory": true,
"caveats": [
"Exploratory, not causal. …",
"Many segments were tested (9 here) …"
],
"meta": {
"queryMs": 212,
"statements": 4,
"asOf": "2026-09-08T09:00:00Z",
"timezone": "UTC",
"sql": "WITH (event_type = ?) AS c, … ;\n\nWITH … GROUP BY dim …"
}
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/timeseries
Events and users over time
Returns one row per time bucket with the number of events and the number of distinct users in it. Use it for the trend line on a dashboard. Buckets with no activity are omitted rather than returned as zero, so plot against your own axis if you need a continuous line.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
interval | query | string | no | Bucket width: hour, day, week or month. |
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One row per non-empty bucket, oldest first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"bucket": "2026-09-01T00:00:00Z",
"events": 2761,
"users": 364
},
{
"bucket": "2026-09-02T00:00:00Z",
"events": 2545,
"users": 336
},
{
"bucket": "2026-09-03T00:00:00Z",
"events": 2890,
"users": 372
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/summary
Headline numbers for a date range
Returns the totals you would put at the top of a dashboard: events, unique users, sessions, conversions, revenue and conversion rate for the window, plus dataFreshAsOf — the timestamp of the newest event we have, so you can tell an empty day from a stalled pipeline. Use it for an overview tile or a daily digest. conversionRate is null and hasConversions is false when the range contains no conversion events, rather than a misleading zero.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | Totals for the window. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"totalEvents": 18420,
"uniqueUsers": 2113,
"sessions": 4780,
"orders": 95,
"revenue": 40120.64,
"conversionRate": 34.6,
"hasConversions": true,
"dataFreshAsOf": "2026-09-07T18:51:24.924Z"
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/summary/compare
Compare a range against the one before it
Returns the same headline numbers as /summary for your window and for the immediately preceding window of equal length, with the percentage change between them. Use it for the "up 12% week on week" line. A changePct entry is null when the previous period had nothing to divide by; previousFrom and previousTo tell you exactly which window was compared.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | Both windows and the change between them. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"current": {
"totalEvents": 18420,
"uniqueUsers": 2113,
"sessions": 4780,
"orders": 95,
"revenue": 40120.64,
"conversionRate": 34.6,
"hasConversions": true,
"dataFreshAsOf": "2026-09-07T18:51:24.924Z"
},
"previous": {
"totalEvents": 16044,
"uniqueUsers": 1902,
"sessions": 4211,
"orders": 81,
"revenue": 33940,
"conversionRate": 31.1,
"hasConversions": true,
"dataFreshAsOf": "2026-08-31T23:12:02.010Z"
},
"changePct": {
"totalEvents": 14.81,
"uniqueUsers": 11.09,
"sessions": 13.51,
"orders": 17.28,
"revenue": 18.21,
"conversionRate": 11.25
},
"previousFrom": "2026-08-25T00:00:00Z",
"previousTo": "2026-09-01T00:00:00Z"
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/realtime
What is happening right now
Returns the last 30 minutes: active users in the last 5 minutes, events and users over the half hour, a per-minute series, the busiest events and pages, and ingestLagSeconds — how far behind the newest event is. Use it to confirm an SDK you just installed is delivering, and to spot a pipeline stall: a large and growing ingestLagSeconds means events are not arriving, not that nobody is using your product. Takes no date range.
| Response | Meaning |
|---|---|
200 | A rolling 30-minute window, computed at asOf. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"asOf": "2026-09-07T12:00:03.412Z",
"activeUsers5m": 37,
"events30m": 941,
"users30m": 204,
"perMinute": [
{
"bucket": "2026-09-07T11:58:00Z",
"events": 31,
"users": 12
},
{
"bucket": "2026-09-07T11:59:00Z",
"events": 28,
"users": 14
}
],
"topEvents": [
{
"key": "report_exported",
"count": 312
},
{
"key": "project_created",
"count": 140
}
],
"topPages": [
{
"key": "/reports",
"count": 286
},
{
"key": "/projects/new",
"count": 133
}
],
"lastEventAt": "2026-09-07T12:00:01.880Z",
"lastReceivedAt": "2026-09-07T12:00:02.301Z",
"ingestLagSeconds": 2
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/query/options
List the metrics and dimensions a query may use
Returns the allow-lists that POST /query and POST /insights/run validate against: the metric keys, the dimensions you may group or break down by, the bucket intervals, and the chart types the product renders. Call it to build a query UI, or to check a metric name before you send it — anything not on these lists is refused with 400. Takes no parameters and needs no date range.
| Response | Meaning |
|---|---|
200 | The current allow-lists. |
200
json
{
"metrics": [
"api_errors",
"api_p50",
"api_p95",
"api_p99",
"api_requests",
"events",
"orders",
"revenue",
"sessions",
"users"
],
"dimensions": [
"appVersion",
"browser",
"channel",
"country",
"device",
"deviceModel",
"environment",
"eventType",
"method",
"os",
"page",
"platform",
"referring_domain",
"route",
"source",
"statusClass",
"utm_campaign",
"utm_medium",
"utm_source"
],
"intervals": [
"hour",
"day",
"week",
"month"
],
"chartTypes": [
"line",
"area",
"bar",
"pie",
"number",
"table"
]
}GET /api/analytics/metrics
How every metric is defined
Returns the definition of every metric this API can compute: its key, its label, the sentence that defines it, its denominator where it is a rate, and the caveats that apply. Use it to label your own charts with the same wording we use, so a number means the same thing wherever it is shown. Takes no parameters and needs no date range; the catalogue is the same for every workspace.
| Response | Meaning |
|---|---|
200 | The full metric catalogue. |
200
json
[
{
"key": "unique_users",
"label": "Unique users",
"definition": "Distinct user ids with at least one product event in the range (API telemetry excluded).",
"denominator": null,
"notes": "Users without an identity (empty user id) are not counted."
},
{
"key": "conversion_rate",
"label": "Conversion rate",
"definition": "Users who did one of the project's conversion events ÷ all unique users, in the range.",
"denominator": "unique_users",
"notes": "Shown only when the range contains conversion events."
}
]GET /api/analytics/events
Browse the raw event stream
Returns one page of individual events, newest first, with their id, type, timestamp, user, session, environment and properties. Use it to debug an integration — confirm the event you just fired arrived with the properties you expected — or to show a live feed. Filter to one event type or one user to keep the page useful.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
eventType | query | string | no | Only events of this exact type. Omit for all of them. |
userId | query | string | no | Only events from this user id. Omit for all users. |
page | query | integer (int32) | no | Zero-based page number. |
size | query | integer (int32) | no | Rows per page. Maximum 500. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One page of events, newest first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"items": [
{
"eventId": "6a54b21c-ab74-4cf3-b660-2f6c78c675c5",
"eventType": "report_exported",
"eventTime": "2026-09-07T18:51:24.924Z",
"userId": "u_2841",
"sessionId": "s_91f0",
"environment": "prod",
"properties": {
"format": "csv",
"rows": 1240
}
},
{
"eventId": "1df36e3e-b445-44d1-9e11-0556a21f73d9",
"eventType": "project_created",
"eventTime": "2026-09-07T18:26:04.154Z",
"userId": "u_2907",
"sessionId": "s_88ab",
"environment": "prod",
"properties": {
"template": "blank"
}
}
],
"total": 18420,
"page": 0,
"size": 2
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/breakdown
Rank event types, or the values of one property
Returns the top values and their counts. Without property you get the most frequent event types, which answers "what are people doing?". With property you get the most frequent values of that event property — pass eventType too to restrict it to one event, for example the format property of report_exported. Use it for bar charts and top-N tables.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
property | query | string | no | Event property to break down by, as sent in the event's properties. Omit to rank event types instead. Names are 1-64 characters from A-Z a-z 0-9 _ . $ -. |
eventType | query | string | no | Only count this event type. Ignored unless property is set. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | Values ranked by count, highest first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"key": "report_exported",
"count": 4451
},
{
"key": "project_created",
"count": 2087
},
{
"key": "signup_completed",
"count": 1233
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/acquisition
Where your users came from
Returns acquisition performance grouped by the dimension you choose, with users, sessions, converting users, conversions, revenue and conversion rate for each. Use it to compare channels or campaigns on outcomes rather than on clicks. Traffic with no attribution information is grouped under (direct).
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
dimension | query | string | no | What to group by: channel (a grouping derived from source and medium), source, medium, source_medium, campaign, term, content, referring_domain or landing_page. |
model | query | string | no | Attribution model: first_touch credits the user's first known source, last_touch credits the most recent one before the conversion. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One row per dimension value, ranked by users. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"key": "(direct)",
"users": 1204,
"sessions": 2810,
"convertingUsers": 369,
"conversions": 491,
"revenue": 29670.64,
"conversionRate": 30.65
},
{
"key": "Organic Search",
"users": 486,
"sessions": 902,
"convertingUsers": 121,
"conversions": 140,
"revenue": 8120,
"conversionRate": 24.9
},
{
"key": "Paid Search",
"users": 133,
"sessions": 214,
"convertingUsers": 52,
"conversions": 58,
"revenue": 4320,
"conversionRate": 39.1
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Funnels & retention
Step-by-step conversion, retention cohorts and the lifecycle split.
POST /api/analytics/paths
Paths after (or before) an event
One time-ordered sequence per person in the range; the first occurrence of the event is the start (direction: after) or the last is the destination (before), and up to six transitions are counted per step. Nodes and links are laid out for a Sankey with each node pruned to its top destinations and the rest folded into (other); (end) is a sequence that stopped. The raw transitions travel with the answer. filters.segment / filters.cohort narrow the people, as for a funnel.
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/funnel
Run a simple funnel from a list of event names
Returns an ordered conversion funnel for a comma-separated list of events: users at each step, conversion from the first and previous step, drop-off, and the median time from the previous step. Use it for a quick funnel you can express in a URL. When you need per-step filters, a breakdown or the conversion trend, use POST /funnel instead.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
steps | query | string | yes | The funnel steps: event names in order, comma-separated. Required, at least one. |
window | query | integer (int64) | no | Conversion window in seconds, measured from each user's first step. Defaults to 604800 (7 days), and is clamped to at least 60 seconds and at most the length of the date range. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One entry per step, in the order you asked for them. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"eventType": "signup_completed",
"users": 720,
"conversionFromFirst": 100,
"conversionFromPrevious": null,
"dropOff": 0,
"medianSecondsFromPrevious": null
},
{
"eventType": "project_created",
"users": 412,
"conversionFromFirst": 57.22,
"conversionFromPrevious": 57.22,
"dropOff": 308,
"medianSecondsFromPrevious": 1830
},
{
"eventType": "invited_teammate",
"users": 140,
"conversionFromFirst": 19.44,
"conversionFromPrevious": 33.98,
"dropOff": 272,
"medianSecondsFromPrevious": 101760
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/funnel
Run a funnel with per-step filters
Returns an ordered conversion funnel: how many users reached each step, the conversion from the first and previous steps, the drop-off, the median time between steps, the distribution of time to convert, an optional breakdown, and the conversion trend over time. Use this rather than GET /funnel when a step needs its own property filter, or when you want the trend and the time-to-convert percentiles. Steps count in order and a user may never skip one; the conversion window is measured from each user's first step.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. Ignored when the body sets tz. |
Request body (required)
steps is required and ordered. Each step is {"event":"name","filters":[{"property":…,"op":…,"value":…}]}. windowSeconds is the conversion window (default 7 days, clamped to the range). breakdown is an event property to split the funnel by, interval the bucket for the trend, limit the number of breakdown rows, and from/to/tz behave as elsewhere.
json
{
"steps": [
{
"event": "signup_completed"
},
{
"event": "project_created",
"filters": [
{
"property": "template",
"op": "eq",
"value": "blank"
}
]
},
{
"event": "invited_teammate"
}
],
"windowSeconds": 604800,
"interval": "week",
"from": "2026-08-17T00:00:00Z",
"to": "2026-09-07T00:00:00Z"
}| Response | Meaning |
|---|---|
200 | The funnel, its trend and the time-to-convert distribution. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"steps": [
{
"eventType": "signup_completed",
"label": "signup_completed",
"users": 720,
"conversionFromFirst": 100,
"conversionFromPrevious": null,
"dropOff": 0,
"medianSecondsFromPrevious": null
},
{
"eventType": "project_created",
"label": "project_created",
"users": 412,
"conversionFromFirst": 57.22,
"conversionFromPrevious": 57.22,
"dropOff": 308,
"medianSecondsFromPrevious": 1830
},
{
"eventType": "invited_teammate",
"label": "invited_teammate",
"users": 140,
"conversionFromFirst": 19.44,
"conversionFromPrevious": 33.98,
"dropOff": 272,
"medianSecondsFromPrevious": 101760
}
],
"timeToConvertSeconds": {
"p25": 66390,
"p50": 101760,
"p75": 467985,
"p90": 549264
},
"breakdown": [],
"trend": [
{
"bucket": "2026-08-17",
"entered": 210,
"converted": 70,
"conversion": 33.33
},
{
"bucket": "2026-08-24",
"entered": 400,
"converted": 60,
"conversion": 15
}
],
"windowSeconds": 604800,
"timezone": "UTC",
"queryMs": 125
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/retention
Weekly retention cohorts
Returns a weekly retention grid as bare [cohortStart, weekNumber, users] triples — the compact shape a heatmap wants. A user joins the cohort of the week they were first seen, and counts in week n if they were active in that week. Use /retention/v2 when you need a different granularity, a different counting mode, or specific start and return events; it returns the same grid under rows along with the settings.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
weeks | query | integer (int32) | no | How many weeks after its own to follow each cohort. 1-52. |
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. Week boundaries move with it. |
| Response | Meaning |
|---|---|
200 | [cohortStart, weekNumber, users] triples. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
[
"2026-08-17",
0,
141
],
[
"2026-08-17",
1,
136
],
[
"2026-08-17",
2,
96
],
[
"2026-08-24",
0,
135
],
[
"2026-08-24",
1,
120
],
[
"2026-08-31",
0,
135
]
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/retention/v2
Retention cohorts, with the mode and events you choose
Returns a retention grid as [cohortStart, periodNumber, users] triples, together with the settings that produced it. Prefer this over GET /retention: you choose the granularity, how many periods to follow, whether a returning user counts for every later period or only that one, and which events start and end the cohort. Use it to answer "do the people who create a project come back?" rather than only "do people come back?".
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
periods | query | integer (int32) | no | How many periods after the cohort's own to follow. 1-52. |
granularity | query | string | no | Cohort and period width: day, week or month. |
mode | query | string | no | How a return is counted. bucket: the user was active in exactly that period. unbounded: the user was active in that period or any later one, so the curve never rises. return_on: the user's activity fell on that period boundary. |
startEvent | query | string | no | The event that puts a user into a cohort. Defaults to any product event. |
returnEvent | query | string | no | The event that counts as coming back. Defaults to any product event. |
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | The retention grid and the settings behind it. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"granularity": "week",
"mode": "bucket",
"periods": 4,
"timezone": "UTC",
"startEvent": "signup_completed",
"returnEvent": "report_exported",
"rows": [
[
"2026-08-17",
0,
141
],
[
"2026-08-17",
1,
136
],
[
"2026-08-17",
2,
96
],
[
"2026-08-24",
0,
135
],
[
"2026-08-24",
1,
120
],
[
"2026-08-31",
0,
135
]
]
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/lifecycle
New, current, resurrected and dormant users per period
Returns, for each period, how many users were new (first ever seen), current (active and active last period too), resurrected (active after being away) and dormant (active before but not now). Use it to see whether growth comes from acquisition or from winning people back, and to watch churn build up in the dormant column before it shows in the totals.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
granularity | query | string | no | Period width: day, week or month. |
tz | query | string | no | IANA timezone used to place bucket boundaries, e.g. Europe/Berlin or America/New_York. A day then starts at local midnight instead of 00:00 UTC. Timestamps in the request and the response stay UTC. Defaults to UTC. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One row per period, oldest first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"bucket": "2026-08-17",
"new": 141,
"current": 0,
"resurrected": 0,
"dormant": 0
},
{
"bucket": "2026-08-24",
"new": 135,
"current": 136,
"resurrected": 4,
"dormant": 5
},
{
"bucket": "2026-08-31",
"new": 135,
"current": 214,
"resurrected": 12,
"dormant": 57
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Users & cohorts
Individual users, their event history, account-level rollups and materialised cohorts.
POST /api/analytics/cohorts/refresh
Materialise a cohort (internal)
Recomputes a cohort's membership from its definition and stores it, then returns the new size and timestamp. This endpoint is not part of the public API: it is called by the control plane after someone saves a cohort, and it authenticates with an internal X-Gateway-Secret header rather than your token. It is documented so operators can see what runs. A refresh writes a new generation before removing the old one, so a failed refresh leaves the previous membership readable rather than emptying the cohort.
A cohort definition is a tree. A group is {"op":"AND"|"OR","children":[…]}, and a condition is one of:
{"type":"did","event":"report_exported","atLeast":1,"withinDays":30,"filters":[…]}— the user did the event.eventmay be"*"for any product event.atLeastdefaults to 1,withinDayscounts back from now (0 or absent means no time limit, and it is capped at 3650).{"type":"did_not_do", …}— the same shape, negated.{"type":"user_property","name":"plan","op":"eq","value":"team-pro"}—opis one ofeq,neq,contains,not_contains,exists,not_exists,gt,gte,lt,lte,in.{"type":"in_cohort","cohortId":"power-users","negate":false}— membership of another cohort.
A filters entry is {"property":…,"op":…,"value":…}. Names and cohort ids are 1-64 characters from A-Z a-z 0-9 _ . $ -. The tree may nest 3 deep and hold at most 20 conditions; anything more, an unknown type, or an op outside those lists is refused with 400.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
X-Gateway-Secret | header | string | yes | The internal shared secret. Compared in constant time; a wrong or missing value answers 401 with an empty body. |
Request body (required)
tenantId, cohortId and definition are required. projectId and appIds scope the computation to one project; an empty appIds computes across the whole workspace.
json
{
"tenantId": "ten_4c1f9a20",
"cohortId": "power-users",
"projectId": "prj_8821",
"appIds": [
"app_2dcbe92b"
],
"definition": {
"type": "did",
"event": "report_exported",
"atLeast": 3,
"withinDays": 30
}
}| Response | Meaning |
|---|---|
200 | The cohort as it now stands. |
401 | The X-Gateway-Secret header was missing or wrong. The body is empty. |
200
json
{
"cohortId": "power-users",
"size": 1284,
"computedAt": "2026-09-07T01:43:46.873Z"
}POST /api/analytics/cohorts/preview
Count how many users a cohort definition would match
Returns only the number of users the definition selects right now. Use it while someone is building a cohort, so they see the audience size change as they add conditions, without materialising anything. Nothing is stored and no cohort is created — call the refresh endpoint for that. The count is evaluated under your own project scope, so it is the audience you can see.
A cohort definition is a tree. A group is {"op":"AND"|"OR","children":[…]}, and a condition is one of:
{"type":"did","event":"report_exported","atLeast":1,"withinDays":30,"filters":[…]}— the user did the event.eventmay be"*"for any product event.atLeastdefaults to 1,withinDayscounts back from now (0 or absent means no time limit, and it is capped at 3650).{"type":"did_not_do", …}— the same shape, negated.{"type":"user_property","name":"plan","op":"eq","value":"team-pro"}—opis one ofeq,neq,contains,not_contains,exists,not_exists,gt,gte,lt,lte,in.{"type":"in_cohort","cohortId":"power-users","negate":false}— membership of another cohort.
A filters entry is {"property":…,"op":…,"value":…}. Names and cohort ids are 1-64 characters from A-Z a-z 0-9 _ . $ -. The tree may nest 3 deep and hold at most 20 conditions; anything more, an unknown type, or an op outside those lists is refused with 400.
Request body (required)
The definition, either bare or wrapped as {"definition": …}. Both are accepted.
json
{
"definition": {
"type": "did",
"event": "report_exported",
"atLeast": 3,
"withinDays": 30
}
}| Response | Meaning |
|---|---|
200 | The number of users the definition matches. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"size": 1284
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/users
List users active in a date range
Returns one page of the users who did anything in the window, each with when you first and last saw them, how many events and sessions they produced, their revenue and their latest user properties. Use it for a searchable people list, or to export an audience. Sort and search happen in the warehouse, so paging through a large workspace stays cheap.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
q | query | string | no | Case-insensitive substring match on the user id. Omit to list everyone. |
sort | query | string | no | Sort order: lastSeen, firstSeen, events, sessions or revenue. All descending. |
page | query | integer (int32) | no | Zero-based page number. |
size | query | integer (int32) | no | Rows per page. Maximum 500. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One page of users, newest activity first by default. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"items": [
{
"userId": "u_2841",
"firstSeen": "2026-08-14T09:02:11.004Z",
"lastSeen": "2026-09-07T17:33:59.521Z",
"events": 42,
"sessions": 9,
"revenue": 147,
"platform": "web",
"country": "DE",
"properties": {
"plan": "team-pro",
"role": "admin",
"seats": 3
}
},
{
"userId": "u_2907",
"firstSeen": "2026-09-01T11:20:28.642Z",
"lastSeen": "2026-09-07T10:04:00.100Z",
"events": 10,
"sessions": 2,
"revenue": 0,
"platform": "web",
"country": "FR",
"properties": {
"plan": "starter"
}
}
],
"total": 2113,
"page": 0,
"size": 2
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/users/{userId}
Profile one user
Returns everything you know about a single user: their row from the people list, the event types they fired in the window with counts, and their daily activity. Use it to build a person page, or to answer a support question about what someone actually did. Answers 404 when no such user has ever been seen in this workspace.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
userId | path | string | yes | The user id your SDK reported via identify(). Exact match, case-sensitive. |
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
| Response | Meaning |
|---|---|
200 | The user, their event mix and their activity by day. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
404 | No user with this id has ever been seen in this workspace. The body is empty. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"user": {
"userId": "u_2841",
"firstSeen": "2026-08-14T09:02:11.004Z",
"lastSeen": "2026-09-07T17:33:59.521Z",
"events": 42,
"sessions": 9,
"revenue": 147,
"platform": "web",
"country": "DE",
"properties": {
"plan": "team-pro",
"role": "admin",
"seats": 3
}
},
"eventTypes": [
{
"key": "report_exported",
"count": 18
},
{
"key": "project_created",
"count": 11
},
{
"key": "plan_upgraded",
"count": 1
}
],
"activity": [
{
"bucket": "2026-09-06T00:00:00Z",
"events": 5,
"users": 1
},
{
"bucket": "2026-09-07T00:00:00Z",
"events": 9,
"users": 1
}
]
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/groups
Roll usage up by account
Returns one row per value of a group key — one per customer account when you group by orgId — with its users, sessions, events, conversions, revenue, and when it was first and last active. Use it for account health in a B2B product: which workspaces have gone quiet, which are growing. Call /groups/keys first if you do not know which keys you send.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
key | query | string | yes | The group key to roll up by, as sent in the event's groups. Required. Use /groups/keys to list the ones you send. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One row per group value, ranked by activity. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"group": "acct-42",
"users": 31,
"sessions": 180,
"events": 2044,
"orders": 3,
"revenue": 441,
"firstSeen": "2026-08-11T17:42:44.076Z",
"lastSeen": "2026-09-07T09:12:04.171Z"
},
{
"group": "acct-77",
"users": 12,
"sessions": 64,
"events": 701,
"orders": 1,
"revenue": 49,
"firstSeen": "2026-08-19T08:02:44.196Z",
"lastSeen": "2026-09-05T14:20:44.215Z"
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/groups/keys
List the group keys you are sending
Returns the names of the group dimensions your events carry — orgId, plan, workspaceId and the like — with how many distinct values each has. Call it before /groups to discover which key to roll up by, or to check that a group you just started sending is arriving.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
| Response | Meaning |
|---|---|
200 | Group keys and their distinct-value counts. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"key": "orgId",
"count": 412
},
{
"key": "plan",
"count": 4
},
{
"key": "workspaceId",
"count": 388
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/cohorts/{id}/status
Check whether a cohort has been built
Returns the cohort's current membership count and when it was last materialised. Call it before scoping a read with ?cohort=: a computedAt of null means the cohort has never been built, and any read scoped to it will be refused with 409 rather than answered against everyone. An unknown cohort id is not an error here — it answers 200 with size 0 and computedAt null, which is the same thing from a caller's point of view.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | The cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. |
| Response | Meaning |
|---|---|
200 | The cohort's size and when it was last built. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"cohortId": "power-users",
"size": 1284,
"computedAt": "2026-09-07T01:43:46.873Z"
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Sessions & replay
Session lists, session replay, heatmaps, navigation paths and experience signals.
GET /api/analytics/paths
Where people go next
Returns the most common step-to-step transitions as {from, to, count}, ranked by how often they happen. Pass start to see only what follows one event, which turns it into a next-step analysis for a screen you care about. Use it to find the paths you did not design for, and the loops where the same step repeats. Transitions are between consecutive events in a session, so a single row is one hop, not a whole journey.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
start | query | string | no | Only transitions leaving this event. Omit to rank every transition. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | Transitions ranked by frequency. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"from": "project_created",
"to": "report_exported",
"count": 1811
},
{
"from": "report_exported",
"to": "report_exported",
"count": 902
},
{
"from": "signup_completed",
"to": "project_created",
"count": 614
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/sessions
List sessions
Returns one page of sessions with their user, start and end, duration, how many events, pages and clicks they contained, the frustration signals in them, the entry and last page, and whether a replay was recorded. Use it to build a session list, or set frustrated=true to jump straight to the sessions worth watching. frustration is a 0-100 score weighing rage clicks most heavily and a single JavaScript error least; it is our own summary, not a raw count.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
userId | query | string | no | Only this user's sessions. Omit for everyone's. |
withReplay | query | boolean | no | true returns only sessions that have a replay recording you can play back. |
frustrated | query | boolean | no | true returns only sessions with at least one frustration signal — a rage click, a dead click, an error click or a JavaScript error. |
page | query | integer (int32) | no | Zero-based page number. |
size | query | integer (int32) | no | Rows per page. Maximum 200. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | One page of sessions, most recent first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"items": [
{
"sessionId": "s_91f0",
"userId": "u_2841",
"startedAt": "2026-09-07T17:28:11.100Z",
"endedAt": "2026-09-07T17:33:59.521Z",
"durationMs": 348421,
"events": 24,
"pages": 6,
"clicks": 31,
"rageClicks": 2,
"deadClicks": 0,
"errorClicks": 1,
"errors": 0,
"frustration": 38,
"entryPage": "/reports",
"lastPage": "/reports/new",
"device": "desktop",
"country": "DE",
"nps": 0,
"hasReplay": true,
"replayChunks": 7
}
],
"total": 4768,
"page": 0,
"size": 1
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/sessions/{sessionId}/replay
Fetch one session's replay
Returns everything needed to play a session back: the ordered recording chunks with their page, URL, start and end in epoch milliseconds and their raw recorded events, plus a timeline of the product events that happened during the session so you can seek to one. Use it to power a replay player, or to attach a recording to a bug report. Each chunk's events is a JSON string you parse yourself, so a large recording is not re-serialised on its way to you. Takes no date range — the session id is enough. Answers 404 when no chunks and no timeline exist for that id, which is also what you get for a session belonging to another workspace.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sessionId | path | string | yes | The session id, as reported by the SDK and returned by GET /sessions. |
| Response | Meaning |
|---|---|
200 | The recording chunks and the session's event timeline. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
404 | No replay and no events exist for this session id in your workspace. The body is empty. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"sessionId": "s_91f0",
"chunks": [
{
"seq": 0,
"url": "https://app.example.com/reports",
"page": "/reports",
"startTs": 1789147691100,
"endTs": 1789147721100,
"eventCount": 412,
"events": "[{\"type\":2,\"timestamp\":1789147691100}]"
}
],
"timeline": [
{
"eventType": "report_exported",
"at": 1789147712004,
"properties": "{\"format\":\"csv\",\"rows\":1240}"
}
]
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/sessions/summary
Session engagement totals
Returns how sessions behaved in the window: how many there were, how many were engaged, the engagement and bounce rates, average and engaged duration, pages and events per session, and the split between new and returning users. Use it to judge the quality of traffic rather than its volume. sessionlessSessions counts activity from server-side SDKs that report no session id; they are grouped per user and per day so they are neither dropped nor double-counted.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | Engagement totals for the window. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"sessions": 4768,
"sessionlessSessions": 112,
"totalSessions": 4880,
"engagedSessions": 3214,
"engagementRate": 65.86,
"bounceRate": 34.14,
"avgDurationSeconds": 287.6,
"avgEngagedDurationSeconds": 401.3,
"pagesPerSession": 3.91,
"eventsPerSession": 8.21,
"newUsers": 1811,
"returningUsers": 302
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/heatmap
Heatmap for one page
Returns three views of one page: points, a click density grid where x and y are fractions of the viewport (0-1, origin top-left) so they scale to any width; elements, the most-clicked CSS selectors with their visible text and their rage and dead click counts; and scrollDepth, how many sessions reached each depth. Use it to see what people try to click and how far down they get. Ask GET /heatmap/pages for the page names that have data.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
page | query | string | yes | The page path to build the heatmap for, exactly as your SDK reports it in the page property. Required. |
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
| Response | Meaning |
|---|---|
200 | Click points, ranked elements and scroll depth for the page. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"page": "/reports",
"sessions": 3110,
"points": [
{
"x": 0.42,
"y": 0.18,
"n": 812,
"rage": 12,
"dead": 3
},
{
"x": 0.77,
"y": 0.64,
"n": 244,
"rage": 40,
"dead": 31
}
],
"elements": [
{
"selector": "button.export",
"text": "Export CSV",
"clicks": 4210,
"rage": 18,
"dead": 2
},
{
"selector": "div.chart-legend",
"text": "Last 30 days",
"clicks": 611,
"rage": 140,
"dead": 88
}
],
"scrollDepth": [
{
"depth": 25,
"sessions": 3080
},
{
"depth": 50,
"sessions": 2110
},
{
"depth": 75,
"sessions": 902
},
{
"depth": 100,
"sessions": 311
}
]
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/heatmap/pages
List the pages with click data
Returns the pages that have click activity, ranked by clicks, with the sessions that produced them and how many of those clicks were rage clicks. Call it to populate a page picker before asking for a heatmap, and to see at a glance where frustration is concentrated. Empty until the browser SDK is capturing clicks.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | Pages ranked by click count. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"page": "/reports",
"clicks": 18402,
"sessions": 3110,
"rageClicks": 142
},
{
"page": "/projects/new",
"clicks": 6011,
"sessions": 1802,
"rageClicks": 88
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/experience
Frustration and feedback in one view
Returns the experience signals for the window: rage, dead and error clicks, JavaScript errors, how many sessions were frustrated, the pages where that happened, the most common error messages, and the survey side — response count, average score, NPS, the score distribution, recent comments and a sentiment split. Use it as the landing view for "where is the product hurting?". nps and avgScore are null rather than 0 when nobody has answered, so an unanswered survey never reads as a terrible one.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
segment | query | string | no | Narrows the read to events matching an ad-hoc filter, as a URL-encoded JSON object {"eventTypes":[…],"properties":[{"name":…,"op":…,"value":…}]}. Every clause is combined with AND. op is one of eq, neq, contains, exists, not_exists and defaults to eq; a property name is 1-64 characters from A-Z a-z 0-9 _ . $ -. Anything else is rejected with 400. Omit for no filter. |
cohort | query | string | no | Narrows the read to the members of a materialised cohort, by cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -. A cohort that has never been built is refused with 409, never silently ignored. Omit to read every user in scope. |
| Response | Meaning |
|---|---|
200 | Frustration signals, top errors and survey results. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
409 | You scoped the read to a cohort with ?cohort=, and that cohort has never been materialised. The read is refused rather than answered against everyone. Build the cohort, then retry. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"rageClicks": 412,
"deadClicks": 188,
"errorClicks": 64,
"errors": 211,
"frustratedSessions": 390,
"sessions": 4768,
"feedbackCount": 142,
"npsResponses": 120,
"avgScore": 8.1,
"nps": 42,
"byPage": [
{
"page": "/reports",
"rageClicks": 142,
"deadClicks": 88,
"errorClicks": 20,
"errors": 60
}
],
"scores": [
{
"score": 9,
"count": 48
},
{
"score": 8,
"count": 31
},
{
"score": 3,
"count": 6
}
],
"comments": [
{
"at": "2026-09-07T11:02:14.004Z",
"userId": "u_2841",
"sessionId": "s_91f0",
"score": 9,
"page": "/reports",
"comment": "Exports are much faster now.",
"sentiment": "positive",
"sentimentScore": 0.7
}
],
"topErrors": [
{
"message": "TypeError: t.rows is undefined",
"count": 140,
"sessions": 88,
"lastSeen": "2026-09-07T16:40:02.100Z"
}
],
"sentiment": {
"positive": 91,
"negative": 18,
"neutral": 33
}
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Experiments
A/B test results with significance testing and data-quality checks.
GET /api/analytics/experiments/{key}/results
Read one experiment's results
Returns, for each variant of the experiment, how many users were exposed, how many converted on the goal you name, the conversion rate, the uplift against control, and — only when the data supports it — a p-value, a z score, a 95% confidence interval on the absolute difference, and whether the result is significant. Use it to decide whether to ship a variant.
Read the honesty rules before you read the numbers. The control is the first variant of the definition, never inferred from the data. With fewer than 100 exposed users or fewer than 5 conversions in either arm, the variant is marked notEnoughData with a sentence saying why, and no p-value is returned at all rather than a weak one. uplift is null when control converted nobody, because relative uplift over zero is not a number. sampleRatioMismatch reports whether traffic actually split the way the weights say; when it fires at p < 0.001 it invalidates everything else here, so check it first. dataQuality.usersWithMultipleVariants counts users who sent exposures for more than one variant — each is counted under their first — and variantsNotInDefinition lists variants seen in the data but absent from the definition; those are reported, never silently dropped, and never used as a comparison arm.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
key | path | string | yes | The experiment key exactly as you defined it and as your SDK passes it to variant(). At most 200 characters. |
goal | query | string | yes | The event that counts as a conversion for this analysis, e.g. signup_completed. A user converts at most once. Required, at most 200 characters. |
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. Exposures and conversions are both counted inside it, so a range shorter than your experiment understates both arms. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
revenue | query | boolean | no | true adds revenuePerUser per arm and a revenue comparison against control, using the revenue on the goal events. Leave false when the goal has no money attached. |
| Response | Meaning |
|---|---|
200 | The experiment's arms, its significance testing and its data-quality checks. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
404 | No experiment with this key is defined in your workspace. Check the key, and that the experiment is saved. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
503 | We could not reach the service that holds experiment definitions, so we cannot tell whether this experiment exists. Retry; do not read this as "no such experiment". |
200
json
{
"control": "control",
"variants": [
{
"variant": "control",
"isControl": true,
"weight": 50,
"exposedUsers": 4120,
"convertedUsers": 611,
"conversionRate": 0.1483
},
{
"variant": "new-onboarding",
"isControl": false,
"weight": 50,
"exposedUsers": 4088,
"convertedUsers": 742,
"conversionRate": 0.1815,
"uplift": 0.2239,
"absoluteDifference": 0.0332,
"notEnoughData": false,
"z": 4.09,
"pValue": 0.0000432,
"significant": true,
"confidenceInterval": {
"level": 0.95,
"metric": "absolute difference in conversion rate (variant - control)",
"low": 0.0173,
"high": 0.0491
}
}
],
"sampleRatioMismatch": {
"observed": {
"control": 4120,
"new-onboarding": 4088
},
"expected": {
"control": 4104,
"new-onboarding": 4104
},
"checked": true,
"chiSquare": 0.1248,
"degreesOfFreedom": 1,
"pValue": 0.7239,
"threshold": 0.001,
"mismatch": false,
"explanation": "The observed split is consistent with the configured weights (p = 0.7239)."
},
"dataQuality": {
"usersWithMultipleVariants": 0,
"variantsNotInDefinition": []
},
"caveats": [
"A p-value is not the probability that this variant is better. It is the probability of seeing a difference at least this large if the two variants were in truth identical. p = 0.03 does not mean a 97% chance the variant wins."
],
"experiment": "onboarding-v2",
"goal": "signup_completed",
"from": "2026-09-01T00:00:00Z",
"to": "2026-09-08T00:00:00Z"
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/experiments/sample-size
How many users an experiment on this goal needs
Sizes an experiment from the goal's real baseline in your workspace over the window: users per arm and in total to detect the relative lift you name (mde, e.g. 0.10 for +10%) with a two-sided test at alpha and power, and how many days that is at the window's daily active users.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
goal | query | string | yes | |
mde | query | number (double) | no | |
alpha | query | number (double) | no | |
power | query | number (double) | no | |
arms | query | integer (int32) | no | |
from | query | string | no | |
to | query | string | no |
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Data quality
What you are actually tracking, and what the pipeline refused.
GET /api/analytics/schema/discover
Discover the schema you are actually sending
Returns every event type seen in the window with its volume, distinct users, when it was first and last seen, and the properties it carries with an inferred type and a count. Use it to build a tracking plan from reality rather than from memory, to spot a typo'd event name that only fired twice, or to catch a property whose type changed after a release. Types are inferred from the values present, so a property sent as both a number and a string shows up as String.
No sample values are ever returned — only names, inferred types and counts — so the response is safe to place in a prompt. Pass shape=names to drop the counts and timestamps too and get exactly [{"eventType":…,"properties":[{"name":…,"type":…}]}], which is what the Ask-in-English orchestrator sends to the model and what POST /insights/validate validates against. The window defaults to the last 90 days for shape=names (an event not seen this week is still real) and to 30 days otherwise.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
shape | query | full | names | no |
| Response | Meaning |
|---|---|
200 | One entry per event type, busiest first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"eventType": "report_exported",
"events": 4451,
"users": 1179,
"firstSeen": "2026-08-19T13:09:20.371Z",
"lastSeen": "2026-09-07T18:26:04.154Z",
"sdk": "browser",
"properties": [
{
"name": "format",
"type": "String",
"count": 4451
},
{
"name": "rows",
"type": "Int64",
"count": 4448
},
{
"name": "scheduled",
"type": "Bool",
"count": 1120
}
]
},
{
"eventType": "project_created",
"events": 2087,
"users": 903,
"firstSeen": "2026-08-19T12:40:20.371Z",
"lastSeen": "2026-09-07T08:39:03.968Z",
"sdk": "browser",
"properties": [
{
"name": "template",
"type": "String",
"count": 2087
}
]
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/properties
List the event property names in use
Returns the property names your events carry in the window, most common first, as a plain array of strings. Use it to populate a filter or breakdown picker without asking the user to remember what they instrumented. For the types and per-event counts behind these names, use /schema/discover.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
| Response | Meaning |
|---|---|
200 | Property names, most common first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
"format",
"rows",
"template",
"plan",
"country",
"device",
"utm_source",
"utm_medium"
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/failed-events
See the events the pipeline refused
Returns the events that failed on the way in, newest first, each with when it failed, which stage rejected it, the reason, and the original payload. Nothing is dropped silently, so this is where to look when your SDK reports success but a number does not move. Common causes are a missing required field and a property whose type conflicts with what the column already holds. The payload is returned as a JSON string, not a parsed object.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
| Response | Meaning |
|---|---|
200 | Quarantined events, newest first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"failedAt": "2026-09-07T18:14:48.454Z",
"eventType": "",
"messageId": "m-9f21",
"stage": "validation",
"reason": "Missing required fields tenantId/appId/environment/eventType",
"payload": "{\"eventId\":\"e-9f21\",\"environment\":\"prod\",\"eventType\":\"\"}"
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}API traffic
Traffic through your own API, as recorded by the request-tracking middleware.
GET /api/analytics/api/timeseries
API requests, errors and latency over time
Returns one row per time bucket with requests, errors and latency percentiles in milliseconds. Use it to see when a latency regression started, and whether it lines up with a deploy. Buckets with no traffic are omitted. Empty until the request-tracking middleware is installed.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
interval | query | string | no | Bucket width: hour, day, week or month. |
| Response | Meaning |
|---|---|
200 | One row per non-empty bucket, oldest first. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"bucket": "2026-09-07T10:00:00Z",
"requests": 8120,
"errors": 31,
"p50Ms": 21,
"p95Ms": 140,
"p99Ms": 510
},
{
"bucket": "2026-09-07T11:00:00Z",
"requests": 9044,
"errors": 204,
"p50Ms": 38,
"p95Ms": 410,
"p99Ms": 1620
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/api/summary
Totals for your own API traffic
Returns request count, error count, error rate, p50/p95/p99 latency in milliseconds and the number of distinct callers, from the api_request events our request-tracking middleware sends. Use it to watch your own API next to your product analytics. Every field is null or 0 if you have not installed the middleware; this is not an error.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
| Response | Meaning |
|---|---|
200 | API traffic totals for the window. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
{
"requests": 184201,
"errors": 912,
"errorRate": 0.5,
"p50Ms": 24,
"p95Ms": 180,
"p99Ms": 640,
"uniqueCallers": 311
}400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/api/status
Split API responses by status class
Returns how many API responses fell into each status class — 2xx, 3xx, 4xx, 5xx — in the window. Use it for an at-a-glance health split, and to tell a client problem (4xx) from yours (5xx). Empty until the request-tracking middleware is installed.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
| Response | Meaning |
|---|---|
200 | One entry per status class present. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"key": "2xx",
"count": 182401
},
{
"key": "4xx",
"count": 1488
},
{
"key": "5xx",
"count": 312
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/api/routes
Rank your API routes
Returns your busiest API routes with their request and error counts and latency percentiles in milliseconds. Use it to find the endpoint that is slow or failing. Routes are the templated paths the middleware reports, so ids do not fragment the ranking. Empty until the middleware is installed.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | no | Start of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to. |
to | query | string | no | End of the window, exclusive. ISO-8601 instant in UTC. Defaults to now. |
limit | query | integer (int32) | no | Maximum rows to return, highest first. |
| Response | Meaning |
|---|---|
200 | Routes ranked by request count. |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
200
json
[
{
"route": "GET /v1/reports",
"requests": 90411,
"errors": 140,
"errorRate": 0.15,
"p50Ms": 18,
"p95Ms": 121,
"p99Ms": 410
},
{
"route": "POST /v1/projects",
"requests": 21044,
"errors": 611,
"errorRate": 2.9,
"p50Ms": 54,
"p95Ms": 390,
"p99Ms": 1210
}
]400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Anomalies
POST /api/analytics/durations
Durations: one row per unit with how long it ran, whether it failed, and its readings
Builds the time-to-event table POST /api/ml/failure-risk takes, from the event stream, under a rule the request states and the answer repeats: a unit is a group value; its clock starts at its first event (or its first startEvent); it fails at its first failureEvent after that; a unit with none is censored at the window's end — silence is never failure. Covariates are aggregated between each unit's start and end. Every value is bound.
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/anomalies
Find the days a series did something unusual
Fits the series' own trend and weekly shape, measures its ordinary spread robustly, and reports the days that fall outside it. No threshold is configured: the normal range is derived from the data. The cut-off is corrected for the number of days tested, so fewer than one false flag is expected across the window.
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}churn-controller
POST /api/analytics/churn/frame
Request body
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Entities
GET /api/analytics/entities/account-keys
The extra account property names this workspace registered
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}PUT /api/analytics/entities/account-keys
Register extra account property names for this workspace
The account-property views carry a built-in list of names; a workspace whose account facts use other names adds them here (the ontology's Entities page does this on save). New names are backfilled from the events already on disk and carried by the views for every event that arrives afterwards. Names already built in are accepted and ignored.
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/entities/fleet
The fleet: one row per asset with alarms, excursions, trips, last reading and last work order
Counted from the industrial pack's own events (sensor_reading, high_vibration_alarm, temperature_excursion, unplanned_shutdown, release_deployed) over days (1–365, default 30), ordered by alarms. The last work order is looked up over a year.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
days | query | integer (int32) | no |
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/entities/fleet/{accountId}/trend
One asset's condition by day: readings, alarms, excursions and trips
Daily counts over days (1–365, default 30), one row per day including empty days, so a gap in readings shows as a gap and not as a missing point.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
accountId | path | string | yes | |
days | query | integer (int32) | no |
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/entities/accounts
Accounts, ranked by activity, with resolved properties
Reads the account tables materialised from events carrying groups.account_id (migration 009). sort is one of events (default), users, first_seen, last_seen; q filters account ids; limit ≤ 500.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sort | query | string | no | |
limit | query | integer (int32) | no | |
q | query | string | no |
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/entities/accounts/{accountId}
One account: profile, properties, users, event mix
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
accountId | path | string | yes |
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Explore
POST /api/analytics/explore/sql
Run a read-only SQL statement over the workspace's own tables
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
X-Explore-Max-Rows | header | integer (int32) | no | |
X-Explore-Max-Seconds | header | integer (int32) | no |
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/explore/report
Run a report: one measure pivoted by a row dimension and time or a second dimension
Compiles to an insight (same engine, same scoping) and returns the grid with row, column and grand totals, the previous period when compare is set, and the SQL that produced it.
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/explore/prepare
Rewrite a BI tool's statement for one workspace (internal, warehouse-gateway)
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/explore/export
Write one table of the workspace to the customer's bucket as Parquet (internal)
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}POST /api/analytics/explore/export/probe
Test a bucket: write a one-row probe file (internal)
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/explore/sql/schema
Tables and columns the SQL explorer may read
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Forecast
POST /api/analytics/forecast
Forecast the series a spec produces
Runs the spec at daily granularity, fits a trend and weekly profile, backtests it on data the model never saw, and returns the forecast with a 95% interval — or refuses, with the reason, when the history is too short or too irregular to predict.
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}Releases
POST /api/analytics/releases/impact
Assess one release against one metric
Compares the window after the deploy with the same clock window one week earlier, overall and per segment, and returns a verdict with the standard Evidence object (docs/EVIDENCE-SPEC.md). not_enough_data is a verdict, not an error.
Request body (required)
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}GET /api/analytics/releases
Releases seen in this workspace
From the releases table, which a release_deployed event or the deploys connector populates (docs/ONTOLOGY-SPEC.md).
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
days | query | integer (int32) | no | |
limit | query | integer (int32) | no |
| Response | Meaning |
|---|---|
200 | OK |
400 | Your parameters do not make sense: from is not before to, a timestamp is not ISO-8601, a cohort id has illegal characters, or a required body field is missing. |
401 | No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one. |
403 | The request carries no project scope. This happens when a token is not bound to a project, or when something between you and us stripped the scope the gateway stamps on. Reads are refused rather than silently widened to the whole workspace, so you can never see a project you were not granted. |
429 | Your workspace has spent its request budget for the current minute. The gateway budgets per workspace across every authenticated call, reads included — a busy dashboard and a batch export share the same budget. Wait for the number of seconds in Retry-After and try again; retrying immediately only spends the next window too. |
400
json
{
"error": "Bad Request",
"message": "'from' must be before 'to'"
}401
json
{
"error": "Unauthorized",
"message": "Missing X-Tenant-Id; the caller must be an authenticated tenant"
}429
json
{
"error": "rate_limited",
"message": "Too many requests for this workspace; retry after 12s"
}