BIO.RE
Creator Analytics

Creator Daily Trend

Per-day views and clicks for the creator's bio page. Bucketed via Postgres date_trunc('day', enteredAt). Days with no traffic are absent — fill gaps client-side.

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

Daily time series of pageviews and link clicks for the creator's bio page. The bucketing is done server-side with Postgres date_trunc('day', "enteredAt"), so each row is a single calendar day in the database server's time zone.

Days with zero traffic are MISSING from the array. The SQL GROUP BY date_trunc('day', enteredAt) only emits rows that actually have pageviews. To plot a continuous line/bar chart, fill missing dates client-side with views: 0, clicks: 0. Don't assume daily.length === days.

day is bucketed in the database server's time zone. The grouping key is date_trunc('day', "enteredAt") without a timezone argument, so it uses the Postgres server's TZ. Two visits at 2026-04-30T23:30Z and 2026-05-01T00:30Z may or may not fall in the same day depending on server TZ. Render day as a UTC date in your client and accept that day boundaries follow the DB, not the user.

clicks is filtered, not separately counted. The SQL uses COUNT(*) FILTER (WHERE "linkClicked" IS NOT NULL) — same scan, same window, just a conditional aggregate. So clicks ≤ views always holds for the same row.

Request

Query parameters

ParamTypeDefaultNotes
daysinteger30Lookback window. Capped at 365.

Headers

HeaderRequiredNotes
Authorization: Bearer <accessToken>JwtAuthGuard.

Response

200 OKApiResponseOf<CreatorTrendDto>

{
  "success": true,
  "data": {
    "days": 30,
    "daily": [
      { "day": "2026-04-01T00:00:00.000Z", "views": 145, "clicks": 12 },
      { "day": "2026-04-02T00:00:00.000Z", "views": 132, "clicks": 8 }
    ]
  }
}

Top-level fields

FieldTypeNotes
daysnumberEcho of the effective window after defaulting + capping.
dailyarrayOne row per calendar day that had at least one pageview.

Item fields (daily[])

FieldTypeNotes
daystring (ISO 8601)Postgres date_trunc('day', enteredAt) serialized — midnight of the day in DB server TZ.
viewsnumberPageview count for that day.
clicksnumberSubset of views where linkClicked IS NOT NULL.

Errors

Same as overview.

Side effects

  1. JwtAuthGuard + ThrottleGuard.
  2. getBioPageId(userId).
  3. analyticsDb.$queryRaw:
    SELECT
      date_trunc('day', "enteredAt") as day,
      COUNT(*)::int as views,
      COUNT(*) FILTER (WHERE "linkClicked" IS NOT NULL)::int as clicks
    FROM "AnalyticsPageView"
    WHERE "bioPageId" = $bioPageId::uuid
      AND "enteredAt" >= $since
    GROUP BY date_trunc('day', "enteredAt")
    ORDER BY day ASC
  4. Return { days, daily }. No client-side gap-filling — empty days are omitted.

Code samples

curl 'https://api.bio.re/api/v1/creators/analytics/trend?days=30' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type DailyPoint = { day: string; views: number; clicks: number };

async function getTrend(
  accessToken: string,
  days = 30,
): Promise<{ days: number; daily: DailyPoint[] }> {
  const res = await fetch(
    `https://api.bio.re/api/v1/creators/analytics/trend?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;
}

// Server omits zero days — fill the gaps client-side for chart rendering.
function fillGaps(daily: DailyPoint[], windowDays: number): DailyPoint[] {
  const map = new Map(daily.map(d => [d.day.slice(0, 10), d]));
  const out: DailyPoint[] = [];
  for (let i = windowDays - 1; i >= 0; i--) {
    const d = new Date(Date.now() - i * 86_400_000);
    const key = d.toISOString().slice(0, 10);
    out.push(map.get(key) ?? { day: `${key}T00:00:00.000Z`, views: 0, clicks: 0 });
  }
  return out;
}

Try it

GET
/api/v1/creators/analytics/trend
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/trend"
{
  "success": true,
  "data": {
    "days": 30,
    "daily": [
      {
        "day": "2019-08-24T14:15:22Z",
        "views": 145,
        "clicks": 12
      }
    ]
  }
}
{
  "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.ts118–139 (trend)
DTO (response)apps/api-core/src/modules/analytics/dto/analytics-client-response.dto.ts149–155 (CreatorTrendDto), 136–145 (item shape)
Prisma modelpackages/prisma-analytics/prisma/schema.prismaAnalyticsPageView.enteredAt / linkClicked (110–111), index @@index([bioPageId, enteredAt]) (117)

On this page