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
| Param | Type | Default | Notes |
|---|---|---|---|
days | integer | 30 | Lookback window. Capped at 365. |
Headers
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JwtAuthGuard. |
Response
200 OK — ApiResponseOf<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
| Field | Type | Notes |
|---|---|---|
links | array | Sorted by clicks DESC. Empty when no link clicks happened in the window. |
totalViews | number | Total pageviews of the bio page in the window. CTR denominator. |
Item fields (links[])
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | BioLink.id from main DB. May reference a now-deleted link (see warn). |
title | string | BioLink.title or "Unknown" when the link no longer exists. |
url | string | BioLink.url or "" (empty string) when the link no longer exists. |
clicks | number | Pageviews where linkClicked = link.id. |
ctr | number | Math.round((clicks / totalViews) * 10000) / 100 — 2-decimal percentage. 0 when totalViews === 0. |
Errors
Same as overview.
Side effects
JwtAuthGuard+ThrottleGuard.getBioPageId(userId).- 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 - 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.
- Step 3 (Analytics DB) — total views denominator:
analyticsDb.analyticsPageView.count({ where: { bioPageId, enteredAt: { gte: since } } }).
- Step 4 (memory) — stitch the two lists by
id. Missing main-DB rows fall back to{ title: 'Unknown', url: '' }. - 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
Authorization
bearer In: header
Query Parameters
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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/analytics/creator-analytics.controller.ts | 165–204 (links — cross-DB join in JS) |
| DTO (response) | apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts | 194–200 (CreatorLinksDto), 175–190 (item shape) |
| Prisma model (main) | packages/prisma/prisma/schema.prisma | BioLink.id / title / url (lines 455–474) |
| Prisma model (analytics) | packages/prisma-analytics/prisma/schema.prisma | AnalyticsPageView.linkClicked / bioPageId / enteredAt (lines 109–111) |
Creator Session Metrics
Aggregate session metrics for the authenticated creator's bio page — total sessions, bounce rate, average session duration, average pages per session.
Export Creator Analytics (CSV)
Download a CSV of the overview, traffic, or links endpoint. CSV-injection-safe via formula-character escaping. Returns text/csv with attachment Content-Disposition.