Behaviour & performance
Cohort retention, navigation paths and Core Web Vitals.
4 endpoints, all relative to https://analytics.appfor.you. Shared rules live in Conventions, Rate limits and Error codes.
Cohort retention
/api/v2/sites/{siteId}/retentionscope: readComputed from session summaries with a 5-minute in-process cache. Retention values are percentages rounded to 1 decimal; index 0 is the cohort period itself and is always 100.
- This endpoint does NOT accept the usual `range` presets — it is parameterised by `granularity` and `periods` instead.
Path parameters
| siteId | string | The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere. |
Query parameters
| Name | Type | Description |
|---|---|---|
| granularity | 'day' | 'week' | 'month' | Cohort bucket size. Default `week`. |
| periods | integer | Number of periods to project, 2–24. Default 8. Alias: `weeks`. |
| startDate | string | ISO-8601 or YYYY-MM-DD. Alias: `from`. |
| endDate | string | ISO-8601 or YYYY-MM-DD. Alias: `to`. |
Example request
curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \ "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/retention?granularity=week&periods=8"
Example response
{
"granularity": "week",
"periods": 8,
"averageRetention": [100, 41.2, 28.7, 22.1, 18.9, 16.4, 15.0, 13.8],
"curves": [
{
"cohort": "2025-01-06",
"totalUsers": 512,
"retention": [100, 44.1, 30.2, 24.0, 20.1, 17.2, 15.4, 14.1]
}
],
"cohorts": [
{
"cohort": "2025-01-06",
"totalUsers": 512,
"periods": [100, 44.1, 30.2, 24.0, 20.1, 17.2, 15.4, 14.1]
}
]
}User journeys
/api/v2/sites/{siteId}/journeysscope: read`topJourneys` groups sessions with more than one pageview by their first 5 pathnames in timestamp order. `entryPages` / `exitPages` are the first and last pathname of each session. `avgPagesPerSession` is the mean pageview count per session, rounded to 1 decimal.
- `limit` and `offset` apply to `topJourneys`, `entryPages` and `exitPages` alike.
Path parameters
| siteId | string | The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere. |
Query parameters
| Name | Type | Description |
|---|---|---|
| range | string | Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied. |
| from | string (YYYY-MM-DD) | Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400. |
| to | string (YYYY-MM-DD) | Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400. |
| limit | integer | Rows to return. Default 50, minimum 1, maximum 500. |
| offset | integer | Rows to skip, for paging. Default 0. |
Example request
curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \ "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/journeys?range=30d&limit=10"
Example response
{
"range": {
"since": "2025-02-19T00:00:00.000Z",
"until": "2025-03-21T00:00:00.000Z"
},
"topJourneys": [
{ "path": ["/", "/pricing", "/signup"], "count": 184 }
],
"entryPages": [
{ "page": "/", "count": 2410 }
],
"exitPages": [
{ "page": "/pricing", "count": 890 }
],
"avgPagesPerSession": 2.4,
"totalSessions": 5934,
"limit": 10,
"offset": 0
}Core Web Vitals
/api/v2/sites/{siteId}/vitalsscope: read`summary` carries p50/p75/p99 for each metric (null when there are no samples) plus good / needsImprovement / poor percentages for the four core metrics. Thresholds: LCP 2500/4000 ms, FID 100/300 ms, CLS 0.1/0.25, INP 200/500 ms. `trend` is a daily p75 series for LCP, FID and CLS; `byPage` and `byDevice` give p75 for LCP, CLS and INP with sample counts.
- `limit` / `offset` paginate `byPage` only (sorted by sample count desc). `byDevice` and `trend` are always returned in full.
Path parameters
| siteId | string | The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere. |
Query parameters
| Name | Type | Description |
|---|---|---|
| range | string | Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied. |
| from | string (YYYY-MM-DD) | Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400. |
| to | string (YYYY-MM-DD) | Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400. |
| limit | integer | Rows to return. Default 50, minimum 1, maximum 500. |
| offset | integer | Rows to skip, for paging. Default 0. |
Example request
curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \ "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/vitals?range=30d"
Example response
{
"range": {
"since": "2025-02-19T00:00:00.000Z",
"until": "2025-03-21T00:00:00.000Z"
},
"summary": {
"lcp": { "p50": 1820, "p75": 2410, "p99": 5120, "good": 78.2, "needsImprovement": 16.4, "poor": 5.4 },
"cls": { "p50": 0.02, "p75": 0.08, "p99": 0.31, "good": 88.1, "needsImprovement": 8.6, "poor": 3.3 },
"ttfb": { "p50": 210, "p75": 380, "p99": 1290 }
},
"trend": [
{ "date": "2025-03-21", "lcp_p75": 2380, "fid_p75": 12, "cls_p75": 0.07 }
],
"byPage": [
{ "pathname": "/", "count": 1820, "lcp_p75": 2210, "cls_p75": 0.05, "inp_p75": 148 }
],
"byDevice": [
{ "device": "desktop", "count": 3120, "lcp_p75": 1980, "cls_p75": 0.04, "inp_p75": 122 }
],
"limit": 50,
"offset": 0
}Refused traffic
/api/v2/sites/{siteId}/bot-eventsscope: readEvery beacon this product judges a bot is discarded before storage and answered with the same 204 an accepted beacon gets, so this endpoint is the only way an integrator can see what left the dataset. `byReason` carries the machine identifiers (`ua_pattern`, `referrer_spam`, `datacenter_ip`, `js_challenge`, `custom_rule`) each with a `costOfBeingWrong` line naming who a false positive on it actually is -- that is the difference between "we blocked 43 bots" and "we deleted 12 of your customers". `byPath` says which of the three ingestion routes refused it (`collect`, `vitals`, `recordings`); rows written before the path was recorded report `unknown` rather than being attributed to a guess. `sample` is the most recent rows with the hashed address, so the counts can be checked against real user agents rather than trusted. The rule itself is documented in `docs/bot-rule.md`.
- `sessions` is the number of DISTINCT VISITS behind the refusals, counted once across every reason. `byReason[].sessions` is a per-reason set and the sets overlap, so adding them up over-counts -- use the top-level figure.
- `datacenter_ip` is off by default. A site only produces it when its owner sets `treatDatacenterAsBot` to `"corroborated"` (refuse when the client also fails to identify a browser engine) or `true` (refuse on the address alone).
- `ip` is a truncated SHA-256 of the client address, never the address. It is stable within a site, so repeat offenders group, and it does not reverse to an IP.
- `retention.actual` is `indefinite` and says so deliberately: `BotEvent` declares a 30-day TTL index that has never been built, and `Site.dataRetentionDays` is validated, plan-capped, displayed and enforced by nothing. Do not build a compliance report on the declared figure.
- `limit` / `offset` paginate `sample` only. `byReason`, `byPath` and `byDay` are always returned in full.
Path parameters
| siteId | string | The Mongo _id returned as `siteId` by GET /api/v2/sites, or the public `trackingId` embedded in the tracking snippet. Both are accepted everywhere. |
Query parameters
| Name | Type | Description |
|---|---|---|
| range | string | Relative window: `today`, `7d`, `30d`, `90d`, `6m` or `12m`. Default `7d`. Ignored when both `from` and `to` are supplied. |
| from | string (YYYY-MM-DD) | Explicit window start (inclusive), from `T00:00:00.000Z` on that day. Supply together with `to` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400. |
| to | string (YYYY-MM-DD) | Explicit window end (inclusive) — the WHOLE day, through `T23:59:59.999Z`, clamped to now when it is in the future. `from=X&to=X` is therefore a full 24-hour day. Supply together with `from` to override `range`. An unparseable value, a window that ends before it starts, or a window that starts in the future returns 400. |
| limit | integer | Rows to return. Default 50, minimum 1, maximum 500. |
| offset | integer | Rows to skip, for paging. Default 0. |
Example request
curl -H "Authorization: Bearer $ANALYTICS_API_KEY" \ "https://analytics.appfor.you/api/v2/sites/6507f1f77bcf86cd799439011/bot-events?range=30d&limit=5"
Example response
{
"range": {
"since": "2025-02-19T00:00:00.000Z",
"until": "2025-03-21T00:00:00.000Z"
},
"total": 2351,
"sessions": 1804,
"addresses": 972,
"byReason": [
{
"reason": "ua_pattern",
"count": 2351,
"sessions": 1804,
"addresses": 972,
"costOfBeingWrong": "a browser whose user agent matched a bot pattern. Cheap to get wrong: the patterns match self-identifying crawlers and headless automation, which no ordinary visitor sends."
}
],
"byPath": [
{ "path": "collect", "count": 2301 },
{ "path": "vitals", "count": 44 },
{ "path": "unknown", "count": 6 }
],
"byDay": [
{ "date": "2025-03-21", "count": 84 }
],
"sample": [
{
"reason": "ua_pattern",
"path": "collect",
"sessionId": "s_8f2a1c",
"ip": "9d2f1a4b8c3e5077",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/146.0.0.0 Safari/537.36",
"referrer": null,
"timestamp": "2025-03-21T09:14:02.118Z"
}
],
"retention": {
"declared": "30 days, by the TTL index on BotEvent.timestamp",
"actual": "indefinite",
"note": "The TTL index is declared and has never been built."
},
"limit": 5,
"offset": 0
}