# Analytics API

The Analytics area publishes 18 REST endpoints. Paths are relative to `https://api.lessly.com`; see [Authentication](/reference/openapi#authentication) for the required header.

### `GET` `/analytics/catalog`

Read the analytics semantic catalog: every entity this product may chart, with its declared metrics, dimensions and person-graph identity kinds. Call this FIRST to discover the exact `entity`, `metric` and `dimension` names analytics_metric_query and analytics_funnel_run accept — those names are resolved against this catalog and a name that is not in it is rejected.

MCP tool: `analytics_catalog_get`

**Parameters**

No parameters.

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| cycle | object | null | • | — |
| entities | object[] | • | — |
| product_id | string | • | — |
| generated_at | string | • | — |

### `GET` `/analytics/dashboards`

List the saved dashboards of the current product, newest first. Narrow by an exact tag or by a case-insensitive substring of the name or slug. Returns definitions, not results.

MCP tool: `analytics_dashboard_list`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| tag | query | string | — | Exact tag the dashboard must carry |
| q | query | string | — | Case-insensitive substring of the name or slug |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| dashboards | object[] | • | — |

### `POST` `/analytics/dashboards`

Save a named dashboard for the current product: up to forty tiles, each either a reference to a saved insight by its uuid (from analytics_insight_list) or a block of markdown, each placed on a twelve-column grid. The slug is the address the dashboard is read by and cannot be changed later. Stores the arrangement only; it does not run it. Every insight a tile names must already exist in this product, so a stored dashboard always resolves.

MCP tool: `analytics_dashboard_create`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| slug | body | string | • | — |
| name | body | string | • | — |
| tiles | body | object[] | • | — |
| tags | body | string[] | — | — |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| name | string | • | — |
| slug | string | • | — |
| tags | string[] | — | — |
| tiles | object[] | • | — |
| created_at | string (date-time) | • | — |
| created_by | string | null | • | — |
| product_id | string (uuid) | • | — |
| updated_at | string (date-time) | • | — |

### `DELETE` `/analytics/dashboards/:id`

Permanently delete one saved dashboard of the current product. The insights its tiles referred to are not deleted.

MCP tool: `analytics_dashboard_delete`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The dashboard's uuid; slugs are not accepted here |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| deleted | boolean one of: true | • | — |

### `PATCH` `/analytics/dashboards/:id`

Change fields of a saved dashboard of the current product. Only the fields supplied are changed; the rest keep their stored values. Supplying tiles replaces the whole arrangement, so send the tiles you want to keep along with the new ones. The slug cannot be changed — create a new dashboard instead.

MCP tool: `analytics_dashboard_update`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| name | body | string | — | — |
| tiles | body | object[] | — | — |
| tags | body | string[] | — | — |
| id | path | string (uuid) | • | The dashboard's uuid; slugs are not accepted here |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| name | string | • | — |
| slug | string | • | — |
| tags | string[] | — | — |
| tiles | object[] | • | — |
| created_at | string (date-time) | • | — |
| created_by | string | null | • | — |
| product_id | string (uuid) | • | — |
| updated_at | string (date-time) | • | — |

### `GET` `/analytics/dashboards/:idOrSlug`

Read one saved dashboard of the current product by its slug or its id. Returns the tiles and their layout, not results.

MCP tool: `analytics_dashboard_get`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| idOrSlug | path | string | • | The dashboard's slug, or its uuid |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| name | string | • | — |
| slug | string | • | — |
| tags | string[] | — | — |
| tiles | object[] | • | — |
| created_at | string (date-time) | • | — |
| created_by | string | null | • | — |
| product_id | string (uuid) | • | — |
| updated_at | string (date-time) | • | — |

### `POST` `/analytics/dashboards/:idOrSlug/run`

Run every tile of a saved dashboard of the current product and return one entry per tile, in board order: the tile itself, and what it produced. An insight tile carries exactly what analytics_insight_run answers for that insight; a text tile carries a null result, because its content is the tile. `from` and `to` override the window of every insight tile for this call only, changing nothing stored. A tile that fails carries an error with an HTTP status and does not stop the rest of the board, so check each tile's result before reading numbers.

MCP tool: `analytics_dashboard_run`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| from | body | string (date-time) | — | Inclusive window start; overrides the stored window |
| to | body | string (date-time) | — | Inclusive window end; overrides the stored window |
| idOrSlug | path | string | • | The dashboard's slug, or its uuid |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| tiles | object[] | • | — |
| dashboard | object | • | — |

### `POST` `/analytics/funnels/run`

Run an ordered conversion funnel over the analytics semantic layer for the current product. Each step names a semantic entity (optionally narrowed by declared-dimension filters) and is counted in resolved persons from the identity graph, so a person is counted once per step no matter how many rows or identifiers they produced. Deterministic: entity, dimension and metric names are resolved against the semantic catalog, never generated. A step whose entity has a `propertiesColumn` in the catalog may also filter on `props.<key>`, compared as a string. Prefer this over analytics_query_run whenever the question is a conversion between ordered steps.

MCP tool: `analytics_funnel_run`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| steps | body | object[] | • | Ordered funnel steps, 2 to 5 |
| from | body | string | • | Inclusive first day (UTC) |
| to | body | string | • | Inclusive last day (UTC) |
| window_days | body | integer | — | Maximum days between two CONSECUTIVE steps for the later one to count |
| grain | body | string one of: day, week, month | — | When set, also return per-step persons bucketed by step-entry time |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| meta | object | • | — |
| steps | object[] | • | — |
| series | object[] | — | — |

### `GET` `/analytics/insights`

List the saved insights of the current product, newest first. Narrow by an exact tag or by a case-insensitive substring of the name or slug. Returns definitions, not results.

MCP tool: `analytics_insight_list`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| tag | query | string | — | Exact tag the insight must carry |
| q | query | string | — | Case-insensitive substring of the name or slug |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| insights | object[] | • | — |

### `POST` `/analytics/insights`

Save a named insight for the current product: one to eight series over the analytics semantic layer, an optional formula combining them by key, a time window and how to display it. The slug is the address the insight is read by and cannot be changed later. Stores the definition only; it does not run it. Series requests are validated with the same schemas analytics_metric_query and analytics_funnel_run accept, so a stored insight always runs.

MCP tool: `analytics_insight_create`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| slug | body | string | • | — |
| name | body | string | • | — |
| description | body | string | — | — |
| kind | body | string one of: trend, funnel, number, retention | • | — |
| series | body | object[] | • | — |
| formula | body | string | null | — | — |
| window | body | object | • | — |
| grain | body | string one of: day, week, month | — | — |
| display | body | string one of: line, bar, bold_number, table | — | — |
| tags | body | string[] | — | — |
| target | body | object | null | — | — |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| kind | string one of: trend, funnel, number, retention | • | — |
| name | string | • | — |
| slug | string | • | — |
| tags | string[] | — | — |
| grain | string one of: day, week, month | — | — |
| series | object[] | • | — |
| target | object | null | — | — |
| window | object | • | — |
| display | string one of: line, bar, bold_number, table | — | — |
| formula | string | null | — | — |
| created_at | string (date-time) | • | — |
| created_by | string | null | • | — |
| product_id | string (uuid) | • | — |
| updated_at | string (date-time) | • | — |
| description | string | — | — |

### `DELETE` `/analytics/insights/:id`

Permanently delete one saved insight of the current product. Dashboards referring to it will no longer resolve it.

MCP tool: `analytics_insight_delete`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| id | path | string (uuid) | • | The insight's uuid; slugs are not accepted here |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| deleted | boolean one of: true | • | — |

### `PATCH` `/analytics/insights/:id`

Change fields of a saved insight of the current product. Only the fields supplied are changed; the rest keep their stored values. The slug cannot be changed — create a new insight instead.

MCP tool: `analytics_insight_update`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| name | body | string | — | — |
| description | body | string | — | — |
| kind | body | string one of: trend, funnel, number, retention | — | — |
| series | body | object[] | — | — |
| formula | body | string | null | — | — |
| window | body | object | — | — |
| grain | body | string one of: day, week, month | — | — |
| display | body | string one of: line, bar, bold_number, table | — | — |
| tags | body | string[] | — | — |
| target | body | object | null | — | — |
| id | path | string (uuid) | • | The insight's uuid; slugs are not accepted here |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| kind | string one of: trend, funnel, number, retention | • | — |
| name | string | • | — |
| slug | string | • | — |
| tags | string[] | — | — |
| grain | string one of: day, week, month | — | — |
| series | object[] | • | — |
| target | object | null | — | — |
| window | object | • | — |
| display | string one of: line, bar, bold_number, table | — | — |
| formula | string | null | — | — |
| created_at | string (date-time) | • | — |
| created_by | string | null | • | — |
| product_id | string (uuid) | • | — |
| updated_at | string (date-time) | • | — |
| description | string | — | — |

### `GET` `/analytics/insights/:idOrSlug`

Read one saved insight of the current product by its slug or its id. Returns the definition, not results.

MCP tool: `analytics_insight_get`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| idOrSlug | path | string | • | The insight's slug, or its uuid |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| id | string (uuid) | • | — |
| kind | string one of: trend, funnel, number, retention | • | — |
| name | string | • | — |
| slug | string | • | — |
| tags | string[] | — | — |
| grain | string one of: day, week, month | — | — |
| series | object[] | • | — |
| target | object | null | — | — |
| window | object | • | — |
| display | string one of: line, bar, bold_number, table | — | — |
| formula | string | null | — | — |
| created_at | string (date-time) | • | — |
| created_by | string | null | • | — |
| product_id | string (uuid) | • | — |
| updated_at | string (date-time) | • | — |
| description | string | — | — |

### `POST` `/analytics/insights/:idOrSlug/run`

Run a saved insight of the current product and return its numbers: every series executed over the resolved window and aligned onto the same buckets, the formula evaluated bucket by bucket, a total per series, and whether the target was met. The window comes from the stored definition — relative windows and the current cycle are resolved at call time — and `from` and `to` override it for this call only, changing nothing stored. Use this rather than rebuilding the insight out of analytics_metric_query calls: the alignment and the formula are what make the numbers comparable.

MCP tool: `analytics_insight_run`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| from | body | string (date-time) | — | Inclusive window start; overrides the stored window |
| to | body | string (date-time) | — | Inclusive window end; overrides the stored window |
| idOrSlug | path | string | • | The insight's slug, or its uuid |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| extra | object | — | — |
| total | object | • | — |
| series | object | • | — |
| window | object | • | — |
| formula | object[] | null | • | — |
| insight | object | null | • | — |
| target_status | string | null one of: met, missed | • | — |

### `POST` `/analytics/metrics/query`

Compute declared metrics from the analytics semantic layer for the current product, bucketed by time and optionally grouped by one declared dimension. Deterministic: entity, metric and dimension names are resolved against the semantic catalog, never generated. Prefer this over analytics_query_run whenever the question is a metric over time — no LLM, no SQL generation. Where the catalog gives an entity a `propertiesColumn`, `group_by` and a filter dimension also accept `props.<key>` to read one raw event property; property values always compare as strings.

MCP tool: `analytics_metric_query`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| entity | body | string | • | Semantic view name from the catalog |
| metrics | body | string[] | • | Declared metric names to compute |
| grain | body | string one of: day, week, month | • | Time bucket size |
| from | body | string | • | Inclusive first day (UTC) |
| to | body | string | • | Inclusive last day (UTC) |
| group_by | body | string | — | One declared dimension, or `props.<key>`, to group by |
| filters | body | object[] | — | Equality filters on declared dimensions, or on `props.<key>` event properties |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| meta | object | • | — |
| series | object[] | • | — |

### `POST` `/analytics/persons/timeline`

Return one resolved person's profile and their chronological activity across every identity-bearing semantic entity of the current product. Address the person either by person_id or by any single identifier (email, phone, users_id, anonymous_id), which is normalised exactly as the identity graph normalised it. Deterministic: no LLM, and only catalog-declared columns are returned. Prefer this over analytics_query_run whenever the question is about one specific person.

MCP tool: `analytics_person_timeline`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| person_id | body | string | — | Graph person id, when already known |
| identifier | body | object | — | Any single identifier belonging to the person |
| from | body | string | — | Inclusive first day (UTC); unbounded when omitted |
| to | body | string | — | Inclusive last day (UTC); unbounded when omitted |
| limit | body | integer | — | Maximum events to return, newest first |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| meta | object | • | — |
| events | object[] | • | — |
| person | object | • | — |
| truncated | boolean | • | — |

### `POST` `/analytics/query`

Answer a natural-language analytics question for the current product. Fallback surface: it routes the question to analytics_metric_query or analytics_funnel_run when it fits one of them and returns that tool's exact request and response in `structured`; it generates SQL only when neither fits. Call analytics_metric_query, analytics_funnel_run or analytics_person_timeline directly when you already know the shape of the question — those are deterministic and cheaper.

MCP tool: `analytics_query_run`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| q | body | string | • | Natural-language analytics question |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| sql | string | • | — |
| rows | object[] | • | — |
| answer | string | • | — |
| truncated | boolean | • | — |
| structured | object | — | — |
| explanation | string | • | — |

### `POST` `/analytics/retention/run`

Measure day-N retention over the analytics semantic layer for the current product: of the subjects whose FIRST matching cohort event falls in a bucket, how many produced a return event on day N (within `return_window_days` after it). The cohort key is `subject` — an event property (`props.<key>`, e.g. the customer product id) or `identity` for the entity's declared identity columns — and each subject is counted exactly once. Deterministic: entity and dimension names are resolved against the semantic catalog, never generated. A bucket whose day-N horizon has not elapsed yet is still returned, with `retained` and `rate` NULL rather than 0 — null means "not measurable yet", 0 means "measured, nobody came back", and a caller must not confuse them. `total` therefore covers the measurable buckets ONLY, so a half-elapsed cohort never drags the headline rate down; `meta.unmeasurable_buckets` says how many were left out.

MCP tool: `analytics_retention_run`

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| entity | body | string | • | Semantic view name from the catalog |
| subject | body | string | • | Cohort key: "identity" or `props.<key>` |
| cohort | body | object | — | The starting event; empty filters mean any row of the entity |
| return | body | object | — | The coming-back event; empty filters mean any row of the entity |
| day | body | integer | • | N: a subject is retained if it has a return event at least N days after its cohort event |
| return_window_days | body | integer | — | Length of the window that opens on day N; a return event must fall inside it |
| from | body | string | • | Inclusive first day of the cohort range (UTC) |
| to | body | string | • | Inclusive last day of the cohort range (UTC) |
| grain | body | string one of: day, week, month | • | Cohort bucket size, applied to each subject's FIRST cohort event |

**Response**

Returns `object`.

| Field | Type | Always | Description |
| --- | --- | --- | --- |
| meta | object | • | — |
| total | object | • | — |
| buckets | object[] | • | — |
