Skip to content

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 ​

EndpointWhat it does
POST/api/analytics/queryRun one metric, optionally grouped
POST/api/analytics/insights/validateCheck an insight spec against your real schema before running it
POST/api/analytics/insights/runRun several series, a breakdown and a formula in one call
POST/api/analytics/insights/explainExplain why a metric moved between two periods
GET/api/analytics/timeseriesEvents and users over time
GET/api/analytics/summaryHeadline numbers for a date range
GET/api/analytics/summary/compareCompare a range against the one before it
GET/api/analytics/realtimeWhat is happening right now
GET/api/analytics/query/optionsList the metrics and dimensions a query may use
GET/api/analytics/metricsHow every metric is defined
GET/api/analytics/eventsBrowse the raw event stream
GET/api/analytics/breakdownRank event types, or the values of one property
GET/api/analytics/acquisitionWhere your users came from
POST/api/analytics/pathsPaths after (or before) an event
GET/api/analytics/funnelRun a simple funnel from a list of event names
POST/api/analytics/funnelRun a funnel with per-step filters
GET/api/analytics/retentionWeekly retention cohorts
GET/api/analytics/retention/v2Retention cohorts, with the mode and events you choose
GET/api/analytics/lifecycleNew, current, resurrected and dormant users per period
POST/api/analytics/cohorts/refreshMaterialise a cohort (internal)
POST/api/analytics/cohorts/previewCount how many users a cohort definition would match
GET/api/analytics/usersList users active in a date range
GET/api/analytics/users/{userId}Profile one user
GET/api/analytics/groupsRoll usage up by account
GET/api/analytics/groups/keysList the group keys you are sending
GET/api/analytics/cohorts/{id}/statusCheck whether a cohort has been built
GET/api/analytics/pathsWhere people go next
GET/api/analytics/sessionsList sessions
GET/api/analytics/sessions/{sessionId}/replayFetch one session's replay
GET/api/analytics/sessions/summarySession engagement totals
GET/api/analytics/heatmapHeatmap for one page
GET/api/analytics/heatmap/pagesList the pages with click data
GET/api/analytics/experienceFrustration and feedback in one view
GET/api/analytics/experiments/{key}/resultsRead one experiment's results
GET/api/analytics/experiments/sample-sizeHow many users an experiment on this goal needs
GET/api/analytics/schema/discoverDiscover the schema you are actually sending
GET/api/analytics/propertiesList the event property names in use
GET/api/analytics/failed-eventsSee the events the pipeline refused
GET/api/analytics/api/timeseriesAPI requests, errors and latency over time
GET/api/analytics/api/summaryTotals for your own API traffic
GET/api/analytics/api/statusSplit API responses by status class
GET/api/analytics/api/routesRank your API routes
POST/api/analytics/durationsDurations: one row per unit with how long it ran, whether it failed, and its readings
POST/api/analytics/anomaliesFind the days a series did something unusual
POST/api/analytics/churn/frame
GET/api/analytics/entities/account-keysThe extra account property names this workspace registered
PUT/api/analytics/entities/account-keysRegister extra account property names for this workspace
GET/api/analytics/entities/fleetThe fleet: one row per asset with alarms, excursions, trips, last reading and last work order
GET/api/analytics/entities/fleet/{accountId}/trendOne asset's condition by day: readings, alarms, excursions and trips
GET/api/analytics/entities/accountsAccounts, ranked by activity, with resolved properties
GET/api/analytics/entities/accounts/{accountId}One account: profile, properties, users, event mix
POST/api/analytics/explore/sqlRun a read-only SQL statement over the workspace's own tables
POST/api/analytics/explore/reportRun a report: one measure pivoted by a row dimension and time or a second dimension
POST/api/analytics/explore/prepareRewrite a BI tool's statement for one workspace (internal, warehouse-gateway)
POST/api/analytics/explore/exportWrite one table of the workspace to the customer's bucket as Parquet (internal)
POST/api/analytics/explore/export/probeTest a bucket: write a one-row probe file (internal)
GET/api/analytics/explore/sql/schemaTables and columns the SQL explorer may read
POST/api/analytics/forecastForecast the series a spec produces
POST/api/analytics/releases/impactAssess one release against one metric
GET/api/analytics/releasesReleases 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.

ParameterInTypeRequiredDescription
tzquerystringnoIANA 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"
}
ResponseMeaning
200The result table and the settings that produced it.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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"
}
ResponseMeaning
200Always 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.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
tzquerystringnoIANA 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"
}
ResponseMeaning
200The result table, its totals and the query metadata.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
tzquerystringnoIANA 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
}
ResponseMeaning
200The 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.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
intervalquerystringnoBucket width: hour, day, week or month.
tzquerystringnoIANA 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.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One row per non-empty bucket, oldest first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200Totals for the window.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200Both windows and the change between them.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ResponseMeaning
200A rolling 30-minute window, computed at asOf.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ResponseMeaning
200The 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.

ResponseMeaning
200The 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
eventTypequerystringnoOnly events of this exact type. Omit for all of them.
userIdquerystringnoOnly events from this user id. Omit for all users.
pagequeryinteger (int32)noZero-based page number.
sizequeryinteger (int32)noRows per page. Maximum 500.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One page of events, newest first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
propertyquerystringnoEvent 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 _ . $ -.
eventTypequerystringnoOnly count this event type. Ignored unless property is set.
limitqueryinteger (int32)noMaximum rows to return, highest first.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200Values ranked by count, highest first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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).

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
dimensionquerystringnoWhat to group by: channel (a grouping derived from source and medium), source, medium, source_medium, campaign, term, content, referring_domain or landing_page.
modelquerystringnoAttribution model: first_touch credits the user's first known source, last_touch credits the most recent one before the conversion.
limitqueryinteger (int32)noMaximum rows to return, highest first.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One row per dimension value, ranked by users.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
stepsquerystringyesThe funnel steps: event names in order, comma-separated. Required, at least one.
windowqueryinteger (int64)noConversion 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.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One entry per step, in the order you asked for them.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
tzquerystringnoIANA 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"
}
ResponseMeaning
200The funnel, its trend and the time-to-convert distribution.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
weeksqueryinteger (int32)noHow many weeks after its own to follow each cohort. 1-52.
tzquerystringnoIANA 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.
ResponseMeaning
200[cohortStart, weekNumber, users] triples.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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?".

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
periodsqueryinteger (int32)noHow many periods after the cohort's own to follow. 1-52.
granularityquerystringnoCohort and period width: day, week or month.
modequerystringnoHow 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.
startEventquerystringnoThe event that puts a user into a cohort. Defaults to any product event.
returnEventquerystringnoThe event that counts as coming back. Defaults to any product event.
tzquerystringnoIANA 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200The retention grid and the settings behind it.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
granularityquerystringnoPeriod width: day, week or month.
tzquerystringnoIANA 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One row per period, oldest first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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. event may be "*" for any product event. atLeast defaults to 1, withinDays counts 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"} — op is one of eq, 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.

ParameterInTypeRequiredDescription
X-Gateway-SecretheaderstringyesThe 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
  }
}
ResponseMeaning
200The cohort as it now stands.
401The 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. event may be "*" for any product event. atLeast defaults to 1, withinDays counts 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"} — op is one of eq, 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
  }
}
ResponseMeaning
200The number of users the definition matches.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
qquerystringnoCase-insensitive substring match on the user id. Omit to list everyone.
sortquerystringnoSort order: lastSeen, firstSeen, events, sessions or revenue. All descending.
pagequeryinteger (int32)noZero-based page number.
sizequeryinteger (int32)noRows per page. Maximum 500.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One page of users, newest activity first by default.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
userIdpathstringyesThe user id your SDK reported via identify(). Exact match, case-sensitive.
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
ResponseMeaning
200The user, their event mix and their activity by day.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
404No user with this id has ever been seen in this workspace. The body is empty.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
keyquerystringyesThe group key to roll up by, as sent in the event's groups. Required. Use /groups/keys to list the ones you send.
limitqueryinteger (int32)noMaximum rows to return, highest first.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One row per group value, ranked by activity.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
ResponseMeaning
200Group keys and their distinct-value counts.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
idpathstringyesThe cohort id. 1-64 characters from A-Z a-z 0-9 _ . $ -.
ResponseMeaning
200The cohort's size and when it was last built.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
startquerystringnoOnly transitions leaving this event. Omit to rank every transition.
limitqueryinteger (int32)noMaximum rows to return, highest first.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200Transitions ranked by frequency.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
userIdquerystringnoOnly this user's sessions. Omit for everyone's.
withReplayquerybooleannotrue returns only sessions that have a replay recording you can play back.
frustratedquerybooleannotrue returns only sessions with at least one frustration signal — a rage click, a dead click, an error click or a JavaScript error.
pagequeryinteger (int32)noZero-based page number.
sizequeryinteger (int32)noRows per page. Maximum 200.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200One page of sessions, most recent first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
sessionIdpathstringyesThe session id, as reported by the SDK and returned by GET /sessions.
ResponseMeaning
200The recording chunks and the session's event timeline.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
404No replay and no events exist for this session id in your workspace. The body is empty.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200Engagement totals for the window.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
pagequerystringyesThe page path to build the heatmap for, exactly as your SDK reports it in the page property. Required.
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
ResponseMeaning
200Click points, ranked elements and scroll depth for the page.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
limitqueryinteger (int32)noMaximum rows to return, highest first.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200Pages ranked by click count.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
segmentquerystringnoNarrows 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.
cohortquerystringnoNarrows 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.
ResponseMeaning
200Frustration signals, top errors and survey results.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
409You 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.
429Your 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.

ParameterInTypeRequiredDescription
keypathstringyesThe experiment key exactly as you defined it and as your SDK passes it to variant(). At most 200 characters.
goalquerystringyesThe event that counts as a conversion for this analysis, e.g. signup_completed. A user converts at most once. Required, at most 200 characters.
fromquerystringnoStart 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.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
revenuequerybooleannotrue 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.
ResponseMeaning
200The experiment's arms, its significance testing and its data-quality checks.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
404No experiment with this key is defined in your workspace. Check the key, and that the experiment is saved.
429Your 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.
503We 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.

ParameterInTypeRequiredDescription
goalquerystringyes
mdequerynumber (double)no
alphaquerynumber (double)no
powerquerynumber (double)no
armsqueryinteger (int32)no
fromquerystringno
toquerystringno
ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
shapequeryfullnamesno
ResponseMeaning
200One entry per event type, busiest first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
limitqueryinteger (int32)noMaximum rows to return, highest first.
ResponseMeaning
200Property names, most common first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
limitqueryinteger (int32)noMaximum rows to return, highest first.
ResponseMeaning
200Quarantined events, newest first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
intervalquerystringnoBucket width: hour, day, week or month.
ResponseMeaning
200One row per non-empty bucket, oldest first.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
ResponseMeaning
200API traffic totals for the window.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
ResponseMeaning
200One entry per status class present.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
fromquerystringnoStart of the window, inclusive. ISO-8601 instant in UTC. Defaults to 30 days before to.
toquerystringnoEnd of the window, exclusive. ISO-8601 instant in UTC. Defaults to now.
limitqueryinteger (int32)noMaximum rows to return, highest first.
ResponseMeaning
200Routes ranked by request count.
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
daysqueryinteger (int32)no
ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
accountIdpathstringyes
daysqueryinteger (int32)no
ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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.

ParameterInTypeRequiredDescription
sortquerystringno
limitqueryinteger (int32)no
qquerystringno
ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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

ParameterInTypeRequiredDescription
accountIdpathstringyes
ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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

ParameterInTypeRequiredDescription
X-Explore-Max-Rowsheaderinteger (int32)no
X-Explore-Max-Secondsheaderinteger (int32)no

Request body (required)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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)

ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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).

ParameterInTypeRequiredDescription
daysqueryinteger (int32)no
limitqueryinteger (int32)no
ResponseMeaning
200OK
400Your 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.
401No usable credential. The gateway did not resolve your Authorization header to a tenant, so the request arrived without one.
403The 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.
429Your 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"
}

Tangent — product analytics.