---
title: Data queries
description: Overview KPIs, analytics, ad-hoc funnels and retention, the user directory, and value-user segments.
---

Retidal's data-query surface answers "what happened" — KPIs, event/visit breakdowns, attribution, funnels,
retention cohorts, and per-user detail. Every endpoint on this page is read-only (VIEWER+ is enough).

## Authentication

<Warning title="This is a console-session surface, with one scoped exception">
  Every endpoint below authenticates with the `trackly_session` session cookie — **not** a project API key.
  If you've used the ingestion or MCP surfaces, don't assume the project key/secret pair also opens these
  routes; it doesn't. The single exception is `GET /api/projects/{projectId}/metric-definitions/{metricId}/query`, covered under
  [Metric definitions](#metric-definitions) below, which also accepts a scoped API key with the
  `metrics:read` scope.
</Warning>

## Overview

```
GET /api/projects/{projectId}/overview?range=today
```

The dashboard's headline KPIs (visits, conversions, conversion rate, active links, total spend) with a
previous-period delta, plus a health block (`ingest`/`postback`/`reach`/`incentive` each `ok`/`warn`/
`crit`/`disabled`) and a 5-step funnel summary (clicks → visits → events → conversions → postback).
`range` defaults to `today`; other relative labels (`yesterday`, etc.) are accepted.

<Note>
  Percentages here are rounded to 1 decimal. `kpis[].value` for total spend is in **yuan** (already divided
  from the stored minor units) and can be fractional — this is one of the few aggregates that is *not* in
  minor units; check the field before assuming cents everywhere.
</Note>

## Event, visit, and attribution analytics

```
GET /api/projects/{projectId}/analytics/events?startDate=&endDate=
GET /api/projects/{projectId}/analytics/visits?startDate=&endDate=
GET /api/projects/{projectId}/analytics/attribution?startDate=&endDate=
```

<ParamField name="startDate" type="string">
  `YYYY-MM-DD`, inclusive. If either `startDate` or `endDate` is missing or invalid, the route silently
  falls back to a rolling default window instead of erroring — 30 days for events/visits/attribution, 7 days
  for growth (below).
</ParamField>
<ParamField name="endDate" type="string">
  `YYYY-MM-DD`, inclusive.
</ParamField>

`analytics/events` returns `byDay` / `byType` (top 15 event names) / `byDevice` (top 10 device types),
internal events excluded. `analytics/visits` is the same shape for sessions (by day/source/device/landing
page). `analytics/attribution` breaks down first-touch/last-touch attribution.

<Warning title="There is no /analytics/funnel?funnelId= endpoint">
  Older references to a `/analytics/funnel?funnelId=` query param style are wrong — ad-hoc funnel math is
  `POST /api/projects/{projectId}/funnel/calculate` (below); saved funnel definitions are plain CRUD at `/funnels`.
</Warning>

## Growth (AARRR)

```
GET /api/projects/{projectId}/growth?startDate=&endDate=
GET /api/projects/{projectId}/growth/metrics
GET /api/projects/{projectId}/growth/engagement
GET /api/projects/{projectId}/growth/activation
GET /api/projects/{projectId}/growth/revenue
```

Same `startDate`/`endDate` convention, default window 7 days when omitted/invalid.

## Ad-hoc funnels

<CodeGroup>
```text title="Endpoint"
POST /api/projects/{projectId}/funnel/calculate
```
```json title="Request body"
{ "steps": ["page_view", "click_register", "register_success"], "windowHours": 24 }
```
</CodeGroup>

All fields are optional (unset ones fall back to your saved growth definition), but `POST /api/projects/{projectId}/funnel/calculate`
needs at least 2 effective steps or you get `400`. Three request fields change the **shape** of the
response, and only one takes effect at a time:

| If you send | You get back | Precedence |
| --- | --- | --- |
| `comparePeriod: true` | `{ current, previous, change, _meta }` — current vs. previous period | Wins over `breakdownBy` if both are set |
| `breakdownBy` (a valid dimension) | `{ breakdown: [...], _meta }` — one funnel per distinct value | |
| neither | `{ steps: [...], overall, _meta }` — flat result | Default |

`breakdownBy`/`filterBy` only accept one of: `utmSource`, `deviceType`, `visitorId`, `userId`, `sessionId`,
`account` — anything else is silently normalized to "no breakdown," not an error. Step conversion rates are
raw floats 0–100 (not rounded); `avgTimeToNext` is in **seconds**.

Saved funnel definitions are separate CRUD at `/funnels` (create/update/delete require ADMIN; read is
VIEWER).

## Retention / cohorts

<CodeGroup>
```text title="Endpoint"
POST /api/projects/{projectId}/retention
```
```json title="Request body"
{ "startEvent": "sign_up", "returnEvent": "page_view", "granularity": "week", "cohortStart": "2026-06-01", "cohortEnd": "2026-07-01" }
```
</CodeGroup>

`granularity` is `day` | `week` | `month`; week buckets snap to Monday, month buckets to the 1st.
`retentionDays` (default `[0, 1, 3, 7, 14, 30]`) picks which intervals get computed.

<Note title="The retention matrix has dynamic keys">
  Each row's `retention` object is keyed by the **stringified interval you asked for** — so a
  `retentionDays: [0, 7, 30]` request produces keys `"0"`, `"7"`, `"30"`, not a fixed schema. Values are
  percentages 0–100 rounded to 2 decimals (unlike the funnel endpoint's unrounded rates). The top-level
  response is one of two shapes distinguished by a literal `segmented` boolean that's always present:
  `segmented: false` gives you a flat `matrix`/`curve`; `segmented: true` (only when `segmentBy` resolves to
  a valid dimension — same whitelist as `breakdownBy` above) gives you a `segments` array, each with its own
  `matrix`/`curve`.
</Note>

## Visitors, users, and value segments

```
GET /api/projects/{projectId}/visitors?page=&pageSize=&search=&startDate=&endDate=
GET /api/projects/{projectId}/visitors/{visitorId}?days=90        — full visitor timeline

GET /api/projects/{projectId}/users?locale=&hasEmail=&hasPhone=&paidStatus=&valueTier=&minLtv=&maxLtv=&sort=...
GET /api/projects/{projectId}/users/export.csv
GET /api/projects/{projectId}/users/{userId}/meta
GET /api/projects/{projectId}/users/{userId}/overview
GET /api/projects/{projectId}/users/{userId}/visits
GET /api/projects/{projectId}/users/{userId}/events
GET /api/projects/{projectId}/users/{userId}/profile
GET /api/projects/{projectId}/users/{userId}/value
GET /api/projects/{projectId}/users/{userId}/reach-history

GET /api/projects/{projectId}/value-users/summary
GET /api/projects/{projectId}/value-users/queues
GET /api/projects/{projectId}/value-users/queues/{queueKey}?limit=&offset=

GET  /api/projects/{projectId}/saved-audiences
POST /api/projects/{projectId}/saved-audiences     (EDITOR)
```

The user directory (`/users`) supports the widest filter set of any list endpoint here — locale, contact
info presence, LTV/balance/usage ranges, purchase recency, and free-text `userIdLike`. `queueKey` on the
value-users queues uses the same six-value enum as
[campaign audience filters](/docs/email/campaigns#audiencefilter): `new_paid_activation`,
`key_created_low_usage`, `high_balance_inactive`, `high_value`, `low_balance_renewal`, `churn_risk_paid`.
Saved audiences created here are reusable by `savedAudienceId` in both campaign `audienceFilter` and trigger
`match.userFilter`.

## Metric definitions

```
GET /api/projects/{projectId}/metric-definitions
GET /api/projects/{projectId}/metric-definitions/{metricId}
PUT /api/projects/{projectId}/metric-definitions/{metricId}
POST /api/projects/{projectId}/metric-definitions/{metricId}/publish
POST /api/projects/{projectId}/metric-definitions/{metricId}/rollback   { "targetVersionId": "..." }
GET /api/projects/{projectId}/metric-definitions/{metricId}/query?from=&to=
```

Metric definitions are versioned: `PUT /api/projects/{projectId}/metric-definitions/{metricId}` only mutates a **draft**; `publish`
cuts an immutable version (optionally kicking off a backfill job); `rollback` reactivates a prior version.
`query` reads the active version's value over a window — `from`/`to` are **Unix milliseconds**, both
required, and the query runs against the underlying transactions data source. Trying to mutate a
published/archived metric with `PUT /api/projects/{projectId}/metric-definitions/{metricId}` returns `409`.

<Note title="This is the one query endpoint a project API key can reach">
  `metric-definitions` (list/get/query = read; create/update/publish/rollback = write) accepts a scoped API
  key with `metrics:read` or `metrics:write` in addition to the session cookie — call it out if you're
  building an automated dashboard that shouldn't hold a browser session.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Projects" href="/docs/console/projects">
    Project details and API keys.
  </Card>
  <Card title="Members" href="/docs/console/members">
    Roles and access.
  </Card>
</CardGroup>
