Skip to content

Data queries

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).

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 below, which also accepts a scoped API key with the metrics:read scope.

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.

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.

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

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).

endDatestring

YYYY-MM-DD, inclusive.

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.

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.

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.

POST /api/projects/{projectId}/funnel/calculate
Request body
{ "steps": ["page_view", "click_register", "register_success"], "windowHours": 24 }

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).

POST /api/projects/{projectId}/retention
Request body
{ "startEvent": "sign_up", "returnEvent": "page_view", "granularity": "week", "cohortStart": "2026-06-01", "cohortEnd": "2026-07-01" }

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.

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.

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: 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.

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.

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.