Get Public Bio Page (Fan-side)
SSR-friendly public bio page render by username. CDN + in-memory cache, status filter (suspended/banned/deactivated → 404), only published creators with active scheduled links.
GET /api/v1/bio/:username — 🌐 Public · Rate limit: 120 req / minute
The canonical fan-side bio page render — by username, no auth. Returns the visible payload only: profile basics, the published BioPage with scheduling-filtered active links, the active client theme preset, public DM packages, verified social accounts, and (when enabled) the "Powered by BIO.RE" referral badge. Cached in-memory and at the CDN edge (s-maxage=60, stale-while-revalidate=300).
Status filter — these all return 404, not 403:
- User not
ACTIVE(SUSPENDED/BANNED/DELETED/DEACTIVATED) - Creator not
ACTIVE(creator-sideSUSPENDED/BANNED/DEACTIVATED) BioPage.published === false
The 404 is intentional — refusing to confirm an account exists when it shouldn't be visible. Don't try to disambiguate "doesn't exist" vs "hidden" client-side.
Link filter is server-side: only active = true links with scheduledStart <= now <= scheduledEnd (or null bounds) are returned. The creator-side editor (GET /creators/:creatorId/bio) returns ALL links so the editor can show inactive / future ones.
Request
Path parameters
| Param | Type | Notes |
|---|---|---|
username | string | Lowercased server-side; case-insensitive lookup |
No body, no headers required.
Response headers
| Header | Value |
|---|---|
Cache-Control | public, s-maxage=60, stale-while-revalidate=300 |
Response
200 OK — ApiResponseOf<PublicBioPageResponseDto>
{
"success": true,
"data": {
"userId": "u1a2b3c4-d5e6-7890-abcd-ef1234567890",
"username": "johndoe",
"displayName": "John Doe",
"bio": "Designer & creator",
"avatarUrl": "https://cdn.bio.re/avatars/abc.webp",
"level": "BRONZE",
"dmType": "SINGLE_PAY",
"dmPrice": "5.00",
"dmActive": true,
"vacationMode": false,
"avgRating": "4.5",
"ratingCount": 42,
"userStatus": "ACTIVE",
"bioPage": {
"id": "b1a2b3c4-d5e6-7890-abcd-ef1234567890",
"bio": "Designer & creator",
"templateId": "t1a2b3c4-d5e6-7890-abcd-ef1234567890",
"themeOverride": null,
"customCss": null,
"embedEnabled": false,
"published": true,
"emailCollectionEnabled": true,
"links": [
{ "id": "l1...", "title": "My Site", "url": "https://example.com", "icon": "globe", "isSocial": false, "platform": null, "embedType": null, "embedMeta": null }
],
"template": { "name": "Minimal Dark", "tokens": { /* design tokens */ } }
},
"socialAccounts": [
{ "platform": "INSTAGRAM", "platformUsername": "johndoe" }
],
"dmPackages": [
{ "id": "d1...", "dmType": "SINGLE_PAY", "price": "5.00" }
],
"themePreset": {
"slug": "midnight",
"name": "Midnight",
"lightTokens": {},
"darkTokens": {},
"typography": {},
"layout": {}
},
"referralBadge": {
"enabled": true,
"referralCode": "ABC123",
"url": "https://bio.re/r/ABC123"
}
}
}Top-level fields
| Field | Type | Notes |
|---|---|---|
userId | string (UUID) | The creator's user id |
username | string | Lowercased echo of the path param |
displayName / bio / avatarUrl | string | null | Profile basics from User |
level | enum | BRONZE / SILVER / GOLD / PLATINUM |
dmType / dmPrice | enum | null / string | null | DM pricing snapshot |
dmActive | boolean | Computed — creatorProfile.dmActive && !vacationMode (vacation forces DMs off in the public view even if creator left them on) |
vacationMode | boolean | CreatorProfile.vacationMode |
avgRating / ratingCount | string | null / number | Aggregate rating |
userStatus | enum | Always ACTIVE here — non-active users 404 |
bioPage | object | null | The published page (links scheduling-filtered server-side) |
socialAccounts | array | Verified social accounts (verified = true), max 50, sorted by oldest connection |
dmPackages | array | Active DMPackage rows for this creator (max 10) |
themePreset | object | null | Currently-active CLIENT theme preset (status PUBLISHED, isActive: true) — admin-managed |
referralBadge | object | undefined | Present only when the referral service successfully resolved a link — failures fall back to undefined (the badge is hidden gracefully) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
404 | creator.bio.not_found | User unknown / not ACTIVE / creator status blocked / BioPage.published === false |
429 | (throttle) | Rate limit exceeded (120 req/min) |
Side effects
- In-memory cache hit path — if the bio page for this username is in the in-memory cache and not expired, return the cached payload directly (LRU touch on access). TTL =
analytics.bio_cache_ttl_minutes(admin-managed, default 5). - Cache miss —
prisma.user.findUnique({ where: { username: lowercased } })selecting (id,displayName,bio,avatarUrl,status). If missing ORstatus !== ACTIVE→ return null → 404. - Read
CreatorProfilejoined withBioPage(with scheduling-filtered links + template). - Status filter — if
creator.creatorStatus IN (SUSPENDED, BANNED, DEACTIVATED)→ 404. - Published filter — if
bioPage.published !== true→ 404. - In parallel: read verified
SocialAccountrows, the active CLIENTThemePreset, activeDMPackagerows, and the referral link (failure-safe — bad referral lookups don't break the page). - Compute
dmActive = creator.dmActive && !vacationMode. - Cache the assembled payload (LRU eviction at 10,000 entries) and return it.
Code samples
curl https://api.bio.re/api/v1/bio/johndoeasync function getPublicBioPage(username: string): Promise<unknown> {
const res = await fetch(`https://api.bio.re/api/v1/bio/${encodeURIComponent(username)}`);
if (res.status === 404) return null; // Treat hidden / unknown identically per security note
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Bio page fetch failed'), {
code: json?.error?.code,
});
}
return json.data; // PublicBioPageResponseDto
}import { useQuery } from '@tanstack/react-query';
export const bioKeys = {
public: (username: string) => ['bio', 'public', username.toLowerCase()] as const,
};
export function usePublicBioPage(username: string) {
return useQuery({
queryKey: bioKeys.public(username),
queryFn: async () => {
const res = await fetch(`/api/v1/bio/${encodeURIComponent(username)}`);
if (res.status === 404) return null;
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Bio page fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data;
},
enabled: Boolean(username),
// Match the CDN cache window
staleTime: 60_000,
});
}Try it
curl -X GET "https://loading/api/v1/bio/string"{
"success": true,
"data": {
"userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b",
"username": "johndoe",
"displayName": "John Doe",
"bio": "string",
"avatarUrl": "https://cdn.bio.re/avatars/abc.jpg",
"level": "BRONZE",
"dmType": "FREE",
"dmPrice": "5.00",
"dmActive": true,
"vacationMode": false,
"avgRating": "4.5",
"ratingCount": 42,
"userStatus": "ACTIVE",
"bioPage": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"bio": "string",
"templateId": "196100ac-4eec-4fb6-a7f7-86c8b584771d",
"themeOverride": {},
"customCss": "string",
"embedEnabled": false,
"published": true,
"emailCollectionEnabled": false,
"links": [
{
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"title": "My Website",
"url": "https://example.com",
"icon": "string",
"isSocial": false,
"platform": "YOUTUBE",
"embedType": "VIDEO",
"embedMeta": {}
}
],
"template": {
"name": "Minimal Dark",
"tokens": {}
}
},
"socialAccounts": [
{
"platform": "INSTAGRAM",
"platformUsername": "johndoe"
}
],
"dmPackages": [
{
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"dmType": "FREE",
"price": "5.00"
}
],
"themePreset": {
"slug": "midnight",
"name": "Midnight",
"lightTokens": {},
"darkTokens": {},
"typography": {},
"layout": {}
},
"referralBadge": {
"enabled": true,
"referralCode": "ABC123",
"url": "https://bio.re/r/ABC123"
}
}
}{
"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/creator/bio-analytics.controller.ts | 57–70 (getPublicBioPage) |
| DTO (response) | apps/api-core/src/modules/creator/dto/bio-analytics-response.dto.ts | 152–202 (PublicBioPageResponseDto), nested DTOs 30–144 |
| Service | apps/api-core/src/modules/creator/bio-analytics.service.ts | 52–158 (getCachedBioPage), 163–167 (invalidateCache) |
| Cache config | apps/api-core/src/modules/config/config.service.ts | analytics.bio_cache_ttl_minutes (admin-managed) |
| Prisma models | packages/prisma/prisma/schema.prisma | User, CreatorProfile, BioPage, BioLink (scheduling), BioTemplate, SocialAccount, DMPackage, ThemePreset (CLIENT, PUBLISHED, isActive) |
List Bio Templates
Public catalog of active bio page templates. CDN-cached (5min). Used in the creator editor to render the template picker.
Add Bio Link
Append a link to the bio page. Validates URL + scheduling, runs title through moderation, blocks manual social links when DM is active, auto-detects embed type, and respects an admin-managed max-links cap.