BIO.RE
Creator

Get Bio Analytics

Owner-only analytics aggregate for a bio page over a configurable time window. Returns total/unique views, top referrers/devices/countries, and per-day view counts.

GET /api/v1/bio/:bioPageId/analytics — 🔑 Bearer

Returns aggregated analytics for a BioPage over a configurable window. Ownership-checkedbioPage.creator.userId must match the bearer's subject; otherwise 403. Aggregates run from a 10,000-row sample of recent BioPageView records — sufficient for typical dashboards, with a documented ceiling for very high-volume creators.

Sampling cap. The aggregator pulls at most 10,000 most-recent views within the window for breakdowns (referrer / device / country). totalViews is the true count (separate query); only the breakdown buckets are derived from the sample. For creators with >10k views per window, the breakdown distributions are accurate but absolute counts in each bucket are sample-scoped.

Top-list caps: topReferrers and countryBreakdown are sliced to the top 20 entries (sorted by count DESC). deviceBreakdown is not sliced — the long tail of devices is small.

Request

Path parameters

ParamTypeValidationNotes
bioPageIdstring (UUID)ParseUUIDPipeThe bio page to aggregate

Query parameters

ParamTypeDefaultValidationNotes
daysnumber30parseInt, server-clamped to [1, 365]Window length looking back from now
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKApiResponseOf<CreatorAnalyticsDto>

{
  "success": true,
  "data": {
    "totalViews": 1500,
    "uniqueVisitors": 800,
    "topReferrers": [
      { "referrer": "direct", "count": 612 },
      { "referrer": "https://twitter.com", "count": 304 }
    ],
    "deviceBreakdown": [
      { "device": "mobile", "count": 980 },
      { "device": "desktop", "count": 420 },
      { "device": "tablet", "count": 100 }
    ],
    "countryBreakdown": [
      { "country": "US", "count": 540 },
      { "country": "TR", "count": 380 }
    ],
    "viewsByDay": [
      { "date": "2026-04-15", "views": 42 },
      { "date": "2026-04-16", "views": 51 }
    ]
  }
}

Top-level fields

FieldTypeNotes
totalViewsnumberTrue count from count(BioPageView where bioPageId AND createdAt >= since) (not sample-scoped)
uniqueVisitorsnumberDistinct visitorId values across the sample (rows where visitorId is null are excluded)
topReferrersarray{ referrer, count } — sorted DESC, top 20. Empty / null referrer is bucketed as 'direct'.
deviceBreakdownarray{ device, count } — not sliced. Empty / null device is bucketed as 'unknown'.
countryBreakdownarray{ country, count } — sorted DESC, top 20. Empty / null country is bucketed as 'unknown'.
viewsByDayarray{ date, views }date is YYYY-MM-DD (server-local PostgreSQL date), sorted ASC. Computed from a separate raw SQL query (COUNT(*) GROUP BY DATE(createdAt)).

Errors

HTTPcode / i18nKeyReason
400(validation)bioPageId not UUID
401(guard)Missing / invalid bearer token
403creator.bio.not_ownerbioPage.creator.userId !== bearer.sub
404creator.bio.not_foundBioPage row missing

Side effects

  1. Ownership lookupprisma.bioPage.findUnique({ where: { id }, select: { creator: { select: { userId: true } } } }). Missing → 404. Mismatch → 403.
  2. Clamp daysMath.max(1, Math.min(365, parseInt(days) || 30)). Default 30.
  3. Three queries in parallel:
    • count(BioPageView)totalViews (true count, not sampled).
    • findMany(BioPageView, take: 10000, select: visitorId/referrer/device/country) → sample for breakdown buckets.
    • getViewsByDay() — raw SQL SELECT DATE(createdAt), COUNT(*) GROUP BY DATE ordered ASC.
  4. In-memory aggregation of the sample:
    • uniqueVisitors = Set(views.filter(v => v.visitorId).map(v => v.visitorId)).size.
    • Build referrerMap, deviceMap, countryMap (null/empty bucketed as 'direct' / 'unknown').
    • Sort + slice top 20 for referrers and countries.
  5. Return assembled object. No mutations.

Code samples

curl 'https://api.bio.re/api/v1/bio/b1a2b3c4-d5e6-7890-abcd-ef1234567890/analytics?days=30' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type ReferrerItem = { referrer: string; count: number };
type DeviceItem = { device: string; count: number };
type CountryItem = { country: string; count: number };
type ViewsByDayItem = { date: string; views: number };

type CreatorAnalytics = {
  totalViews: number;
  uniqueVisitors: number;
  topReferrers: ReferrerItem[];
  deviceBreakdown: DeviceItem[];
  countryBreakdown: CountryItem[];
  viewsByDay: ViewsByDayItem[];
};

async function getBioAnalytics(accessToken: string, bioPageId: string, days = 30): Promise<CreatorAnalytics> {
  const url = new URL(`https://api.bio.re/api/v1/bio/${bioPageId}/analytics`);
  url.searchParams.set('days', String(days));
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Analytics fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery } from '@tanstack/react-query';

export const bioKeys = {
  analytics: (bioPageId: string, days: number) =>
    ['bio', bioPageId, 'analytics', days] as const,
};

export function useBioAnalytics(bioPageId: string, days = 30) {
  return useQuery({
    queryKey: bioKeys.analytics(bioPageId, days),
    queryFn: async () => {
      const url = new URL(`/api/v1/bio/${bioPageId}/analytics`, window.location.origin);
      url.searchParams.set('days', String(days));
      const res = await fetch(url);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Analytics fetch failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as CreatorAnalytics;
    },
    enabled: Boolean(bioPageId),
    staleTime: 5 * 60_000, // 5min — analytics is event-sourced and changes incrementally
  });
}

Try it

GET
/api/v1/bio/{bioPageId}/analytics
AuthorizationBearer <token>

In: header

Path Parameters

bioPageId*string

Query Parameters

days?string

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://loading/api/v1/bio/string/analytics"
{
  "success": true,
  "data": {
    "totalViews": 1500,
    "uniqueVisitors": 800,
    "topReferrers": [
      {
        "referrer": "direct",
        "count": 42
      }
    ],
    "deviceBreakdown": [
      {
        "device": "mobile",
        "count": 30
      }
    ],
    "countryBreakdown": [
      {
        "country": "US",
        "count": 25
      }
    ],
    "viewsByDay": [
      {
        "date": "2026-04-15",
        "views": 42
      }
    ]
  }
}
{
  "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"
  }
}
{
  "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

SourcePathLines
Controllerapps/api-core/src/modules/creator/bio-analytics.controller.ts126–142 (getAnalytics)
DTO (response)apps/api-core/src/modules/creator/dto/creator-response.dto.ts385–403 (CreatorAnalyticsDto), 349–379 (nested item DTOs)
Serviceapps/api-core/src/modules/creator/bio-analytics.service.ts247–301 (getAnalytics), 229–242 (getViewsByDay)
Prisma modelpackages/prisma/prisma/schema.prismaBioPageView, BioPage (relation chain for ownership)

On this page