BIO.RE
Creator Analytics

Creator Link Performance

Per-link click counts and CTR for the creator's bio page. Cross-DB join — clicks come from Analytics DB, link metadata (title + URL) is fetched from main DB by id.

GET /api/v1/creators/analytics/links — 🔑 Bearer · Rate limit: 60 req / minute

Returns every bio link that received at least one click in the window, with click count and click-through rate. This endpoint joins across both databases — the click counts come from the Analytics DB (AnalyticsPageView WHERE linkClicked IS NOT NULL), and the link titles/URLs come from the main DB (prisma.bioLink.findMany).

Cross-DB join — done in application code, not SQL. Step 1: aggregate clicks per linkClicked UUID in Analytics DB. Step 2: pull link details for the resulting ids from the main DB's BioLink table. Step 3: stitch them together in JS. There's no foreign-key constraint between the two databases.

Deleted/renamed links may appear as "Unknown". When the main-DB lookup misses (link was deleted after the click was recorded), the row still appears in the response with title: 'Unknown' and url: ''. Don't filter these out server-side — historical click data has no other surface.

ctr is per-link / per-bio-page-views. (link.clicks / totalViews) * 100, 2-decimal. The denominator is total bio-page pageviews in the window (the same number overview.totalViews returns), not "views of the link." Two links can have CTRs that don't sum to the bio page's overall click-through rate from overview.

Request

Query parameters

ParamTypeDefaultNotes
daysinteger30Lookback window. Capped at 365.

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>JwtAuthGuard.

Response

200 OKApiResponseOf<CreatorLinksDto>

{
  "success": true,
  "data": {
    "links": [
      {
        "id": "link-uuid-1",
        "title": "My Portfolio",
        "url": "https://example.com",
        "clicks": 120,
        "ctr": 4.8
      },
      {
        "id": "link-uuid-deleted",
        "title": "Unknown",
        "url": "",
        "clicks": 6,
        "ctr": 0.24
      }
    ],
    "totalViews": 2500
  }
}

Top-level fields

FieldTypeNotes
linksarraySorted by clicks DESC. Empty when no link clicks happened in the window.
totalViewsnumberTotal pageviews of the bio page in the window. CTR denominator.
FieldTypeNotes
idstring (UUID)BioLink.id from main DB. May reference a now-deleted link (see warn).
titlestringBioLink.title or "Unknown" when the link no longer exists.
urlstringBioLink.url or "" (empty string) when the link no longer exists.
clicksnumberPageviews where linkClicked = link.id.
ctrnumberMath.round((clicks / totalViews) * 10000) / 100 — 2-decimal percentage. 0 when totalViews === 0.

Errors

Same as overview.

Side effects

  1. JwtAuthGuard + ThrottleGuard.
  2. getBioPageId(userId).
  3. Step 1 (Analytics DB) — click counts grouped by link id:
    SELECT "linkClicked" as link_id, COUNT(*)::int as clicks
    FROM "AnalyticsPageView"
    WHERE "bioPageId" = $bioPageId::uuid
      AND "linkClicked" IS NOT NULL
      AND "enteredAt" >= $since
    GROUP BY "linkClicked"
    ORDER BY clicks DESC
  4. Step 2 (main DB) — fetch link metadata:
    • prisma.bioLink.findMany({ where: { id: { in: linkIds } }, select: { id, title, url } }).
    • Only runs if Step 1 returned at least one row.
  5. Step 3 (Analytics DB) — total views denominator:
    • analyticsDb.analyticsPageView.count({ where: { bioPageId, enteredAt: { gte: since } } }).
  6. Step 4 (memory) — stitch the two lists by id. Missing main-DB rows fall back to { title: 'Unknown', url: '' }.
  7. Return { links, totalViews }.

Code samples

curl 'https://api.bio.re/api/v1/creators/analytics/links?days=30' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type LinkPerf = { id: string; title: string; url: string; clicks: number; ctr: number };

async function getLinks(
  accessToken: string,
  days = 30,
): Promise<{ links: LinkPerf[]; totalViews: number }> {
  const res = await fetch(
    `https://api.bio.re/api/v1/creators/analytics/links?days=${days}`,
    { headers: { Authorization: `Bearer ${accessToken}` } },
  );
  const json = await res.json();
  if (!res.ok || !json.success) throw new Error(json?.error?.message ?? 'Failed');
  return json.data;
}

Try it

GET
/api/v1/creators/analytics/links
AuthorizationBearer <token>

In: header

Query Parameters

days?string

Response Body

application/json

application/json

application/json

curl -X GET "https://loading/api/v1/creators/analytics/links"
{
  "success": true,
  "data": {
    "links": [
      {
        "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
        "title": "My Portfolio",
        "url": "https://example.com",
        "clicks": 120,
        "ctr": 4.8
      }
    ],
    "totalViews": 2500
  }
}
{
  "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/analytics/creator-analytics.controller.ts165–204 (links — cross-DB join in JS)
DTO (response)apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts194–200 (CreatorLinksDto), 175–190 (item shape)
Prisma model (main)packages/prisma/prisma/schema.prismaBioLink.id / title / url (lines 455–474)
Prisma model (analytics)packages/prisma-analytics/prisma/schema.prismaAnalyticsPageView.linkClicked / bioPageId / enteredAt (lines 109–111)

On this page