Creator Analytics Overview
Top-line counters for the authenticated creator's bio page — total views, unique visitors (by hashed visitorId), total link clicks, CTR. Scoped to the caller's own bio page; foreign access is impossible.
GET /api/v1/creators/analytics/overview — 🔑 Bearer · Rate limit: 60 req / minute
Four headline metrics for the current creator's bio page over a configurable window. The endpoint resolves the bio page from the JWT subject — there is no creator/userId path or query parameter, so a creator can only ever see their own numbers.
Owner-scoped via JWT, no cross-creator reads. All eight creator-analytics endpoints follow the same pattern: getBioPageId(userId) looks up CreatorProfile then BioPage from the main DB, throws 404 if either is missing. The Analytics DB queries that follow always filter by that resolved bioPageId. There is no way to spoof another creator's analytics through this controller.
uniqueVisitors counts distinct hashed visitor ids. The visitor key was SHA256-hashed at session-create time (see Create Analytics Session) — the count here is groupBy(visitorId) over those hashes, so it's a privacy-preserving distinct-count, not a raw distinct user count.
days is capped at 365. Math.min(parseInt(days ?? '30'), 365) — anything larger is silently clamped. Out-of-range strings (?days=abc) become NaN then default to 30 (since Math.min(NaN, 365) === NaN falls through to default). Prefer to send a clean integer.
Request
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
days | integer | 30 | Lookback window in days. Server caps at 365. |
Headers
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JwtAuthGuard (explicit on this controller, not just global). |
Response
200 OK — ApiResponseOf<CreatorAnalyticsOverviewDto>
{
"success": true,
"data": {
"totalViews": 4200,
"uniqueVisitors": 1800,
"totalClicks": 320,
"ctr": 7.62,
"days": 30
}
}| Field | Type | Notes |
|---|---|---|
totalViews | number | count(AnalyticsPageView WHERE bioPageId = ? AND enteredAt >= since). Counts every pageview, including link-click pageviews. |
uniqueVisitors | number | Distinct hashed visitorId over AnalyticsSession rows in the window. |
totalClicks | number | count(AnalyticsPageView WHERE linkClicked IS NOT NULL ...). A view counts as a "click" when the URL navigation came from a bio link. |
ctr | number | Math.round((totalClicks / totalViews) * 10000) / 100 — 2-decimal percentage. 0 when there are no views. |
days | number | Echo of the effective window (after defaulting + capping). |
Errors
| HTTP | Code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token. |
404 | error.creator.not_found | The authenticated user has no CreatorProfile. |
404 | error.creator.bio_not_found | Creator exists but has no BioPage. |
429 | Throttle | Over 60 req/min from this IP. |
Side effects
JwtAuthGuard.ThrottleGuard(60 req / 60s).getBioPageId(userId)— main DB:prisma.creatorProfile.findFirst({ where: { userId }, select: { id } })→ 404 if missing.prisma.bioPage.findFirst({ where: { creatorId }, select: { id } })→ 404 if missing.
- Compute
since = new Date(Date.now() - d * 86400000). - Three parallel queries against the Analytics DB:
analyticsDb.analyticsPageView.count({ where: { bioPageId, enteredAt: { gte: since } } })analyticsDb.analyticsSession.groupBy({ by: ['visitorId'], where: { bioPageId, startedAt: { gte: since } } }).then(r => r.length)— distinct visitorsanalyticsDb.analyticsPageView.count({ where: { bioPageId, linkClicked: { not: null }, enteredAt: { gte: since } } })
- Compute
ctrand return. No DB writes.
Code samples
curl 'https://api.bio.re/api/v1/creators/analytics/overview?days=30' \
-H "Authorization: Bearer $ACCESS_TOKEN"type CreatorOverview = {
totalViews: number;
uniqueVisitors: number;
totalClicks: number;
ctr: number; // 2-decimal percentage
days: number;
};
async function getOverview(
accessToken: string,
days = 30,
): Promise<CreatorOverview> {
const res = await fetch(
`https://api.bio.re/api/v1/creators/analytics/overview?days=${days}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Overview failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery } from '@tanstack/react-query';
export const creatorAnalyticsKeys = {
overview: (days: number) => ['creator-analytics', 'overview', days] as const,
};
export function useCreatorOverview(days = 30) {
return useQuery({
queryKey: creatorAnalyticsKeys.overview(days),
queryFn: async () => {
const res = await fetch(`/api/v1/creators/analytics/overview?days=${days}`);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Overview failed'), {
code: json?.error?.code,
});
}
return json.data as CreatorOverview;
},
staleTime: 5 * 60_000,
});
}Try it
Authorization
bearer In: header
Query Parameters
Response Body
application/json
application/json
application/json
curl -X GET "https://loading/api/v1/creators/analytics/overview"{
"success": true,
"data": {
"totalViews": 4200,
"uniqueVisitors": 1800,
"totalClicks": 320,
"ctr": 7.62,
"days": 30
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/analytics/creator-analytics.controller.ts | 23–28 (class), 45–62 (overview) |
| Owner-scope helper | apps/api-core/src/modules/analytics/creator-analytics.controller.ts | 30–43 (getBioPageId — main DB lookups) |
| DTO (response) | apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts | 57–72 (CreatorAnalyticsOverviewDto) |
| Prisma model (main) | packages/prisma/prisma/schema.prisma | CreatorProfile.userId (315–317), BioPage.creatorId (426–443) |
| Prisma model (analytics) | packages/prisma-analytics/prisma/schema.prisma | AnalyticsSession.visitorId (line 48), AnalyticsPageView.bioPageId / linkClicked / enteredAt (102–119) |
List Subscribers (Creator)
Owner-only paginated list of confirmed, active mailing-list subscribers. Filter is confirmed=true AND unsubscribedAt=null.
Creator Traffic Sources
Top 15 referrer domains for the authenticated creator's bio page, with visit counts and percentage share. Direct visits show up as "direct".