Browse by Category / Platform
Public creator browse by platform (Instagram / X / etc) AND optional content category. Three sort modes (recent / popular / rating). DM-type filter. Cursor pagination.
GET /api/v1/discover/category/:platform โ ๐ Public ยท Rate limit: 60 req / minute ยท Kill-switched
Browse creators filtered by social platform (path param โ they have a verified linked account on that platform) AND optionally by content category (?category=) AND by DM type (?dmType=). Three sort modes: recent (default), popular (totalMessages), rating (avgRating). Cursor-based pagination โ same shape as /search.
platform is the social network filter, not the content category. The path param picks creators with at least one verified SocialAccount on that platform (e.g. /discover/category/instagram returns creators who linked Instagram via OAuth). The ?category query param is a separate filter on CreatorCategory.category โ that's the content niche (fitness, music, tech, ...).
Server-side platform filter is uppercase. The path param :platform is .toUpperCase()-d before the SocialAccount lookup. Instagram, instagram, INSTAGRAM all hit the same row set. Pass any case the user input.
Request
Path parameters
| Param | Type | Notes |
|---|---|---|
platform | string | Free-form platform identifier (instagram, x, youtube, tiktok, etc). Server uppercases before matching SocialAccount.platform. Empty platform set returns empty results. |
Query parameters โ DiscoverDto
| Param | Type | Default | Validation | Notes |
|---|---|---|---|---|
category | string | โ | IsString() | Content category (e.g. fitness, music). Lowercased before matching CreatorCategory.category. |
sort | string | recent | IsString() | popular (totalMessages DESC), rating (avgRating DESC), or recent (updatedAt DESC; also default) |
dmType | enum | โ | IsEnum(DmType) | FREE / SINGLE_PAY / PER_MESSAGE |
cursor | string | โ | IsString() | Opaque cursor (last result id) |
limit | number | discover.page_size (admin, default 20) | Min(1), Max(50), @Type(Number) | Server-additionally clamps to min(limit, 100) |
No headers required.
Response
200 OK โ ApiResponseOf<CursorPaginatedCreatorsDto>
Same shape as GET /discover/search โ { results: CreatorCardDto[], nextCursor, hasMore }.
{
"success": true,
"data": {
"results": [
{
"id": "c1a2b3c4-d5e6-7890-abcd-ef1234567890",
"userId": "u1a2b3c4-d5e6-7890-abcd-ef1234567890",
"username": "fitness-creator",
"displayName": "Fitness Creator",
"avatarUrl": "https://cdn.bio.re/avatars/abc.webp",
"level": "SILVER",
"dmActive": true,
"totalFollowers": 25000
}
],
"nextCursor": "c1a2b3c4-d5e6-7890-abcd-ef1234567890",
"hasMore": true
}
}Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | dmType invalid enum, limit outside [1, 50] |
429 | (throttle) | Rate limit exceeded (60 req/min) |
503 | features.discover_disabled | Admin kill switch DISCOVER is active |
Side effects
- Resolve
pageSize = min(limit ?? config.discover.page_size ?? 20, 100). - Platform pre-filter โ
socialAccount.findMany({ platform: <upper>, verified: true, take: 1000 })to get theuserIdsset. - Empty platform set โ return
{ results: [], nextCursor: null, hasMore: false }. - Build
where = { ...activeCreatorFilter, userId IN <set> }. - Optional
dmTypefilter applied towhere.dmType. - Optional
categoryfilter โcreatorCategory.findMany({ category: <lower>, take: 1000 })โ category creator IDs. Empty โ return empty results. Else addwhere.id IN <categoryIds>. - Resolve
orderByfromsortswitch (defaultupdatedAt desc). creatorProfile.findMany({ where, include: user, take: pageSize+1, orderBy, [optional cursor + skip:1] }).- Compute
hasMore = creators.length > pageSize; trim, assemblenextCursor, map to cards, return.
Code samples
# All Instagram creators (default recent sort)
curl 'https://api.bio.re/api/v1/discover/category/instagram'
# Popular fitness creators on TikTok with paid DMs
curl 'https://api.bio.re/api/v1/discover/category/tiktok?category=fitness&sort=popular&dmType=SINGLE_PAY&limit=20'type BrowseFilter = {
platform: string;
category?: string;
sort?: 'recent' | 'popular' | 'rating';
dmType?: 'FREE' | 'SINGLE_PAY' | 'PER_MESSAGE';
cursor?: string;
limit?: number;
};
async function browseByCategory(f: BrowseFilter): Promise<CursorPage<CreatorCard>> {
const url = new URL(`https://api.bio.re/api/v1/discover/category/${encodeURIComponent(f.platform)}`);
if (f.category) url.searchParams.set('category', f.category);
if (f.sort) url.searchParams.set('sort', f.sort);
if (f.dmType) url.searchParams.set('dmType', f.dmType);
if (f.cursor) url.searchParams.set('cursor', f.cursor);
if (f.limit) url.searchParams.set('limit', String(f.limit));
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Browse failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useInfiniteQuery } from '@tanstack/react-query';
export const discoverKeys = {
byCategory: (f: Omit<BrowseFilter, 'cursor'>) => ['discover', 'category', f] as const,
};
export function useByCategoryInfinite(f: Omit<BrowseFilter, 'cursor'>) {
return useInfiniteQuery({
queryKey: discoverKeys.byCategory(f),
initialPageParam: undefined as string | undefined,
queryFn: async ({ pageParam }) => {
const url = new URL(`/api/v1/discover/category/${encodeURIComponent(f.platform)}`, window.location.origin);
if (f.category) url.searchParams.set('category', f.category);
if (f.sort) url.searchParams.set('sort', f.sort);
if (f.dmType) url.searchParams.set('dmType', f.dmType);
if (f.limit) url.searchParams.set('limit', String(f.limit));
if (pageParam) url.searchParams.set('cursor', pageParam);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Browse failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as CursorPage<CreatorCard>;
},
getNextPageParam: (last) => (last.hasMore ? last.nextCursor ?? undefined : undefined),
enabled: Boolean(f.platform),
});
}Try it
Path Parameters
Query Parameters
Filter by category
Sort order
Filter by DM type
"FREE" | "SINGLE_PAY" | "PER_MESSAGE"Cursor for cursor-based pagination
Number of results to return
1 <= value <= 50Response Body
application/json
curl -X GET "https://loading/api/v1/discover/category/string"{
"success": true,
"data": {
"results": [
{
"id": "cuid_creator_123",
"userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b",
"username": "johndoe",
"displayName": "John Doe",
"avatarUrl": "https://cdn.bio.re/avatars/abc.jpg",
"level": "BRONZE",
"dmActive": false,
"totalFollowers": 0
}
],
"nextCursor": "string",
"hasMore": false
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/discover/discover.controller.ts | 57โ68 (byCategory) |
| DTO (request) | apps/api-core/src/modules/discover/dto/index.ts | 21โ37 (DiscoverDto) |
| DTO (response) | apps/api-core/src/modules/discover/dto/discover-response.dto.ts | 37โ46 (CursorPaginatedCreatorsDto) |
| Service | apps/api-core/src/modules/discover/discover.service.ts | 298โ361 (getByCategory) |
| Config | apps/api-core/src/modules/config/config.service.ts | discover.page_size (admin-managed default 20) |
| Prisma models | packages/prisma/prisma/schema.prisma | SocialAccount (platform pre-filter), CreatorProfile (active + dmType + sort), CreatorCategory (optional content niche) |
Get Featured Creators
Public admin-curated featured creator list. Filtered by current time window (startsAt/endsAt) + creator active status. Ordered by admin-set position. CDN-cached 5min.
Record Profile View
Authenticated. Upserts a RecentlyViewed row for the (user, creator) pair โ refreshes viewedAt on subsequent visits. Returns whether the row was recorded.