BIO.RE
Discover

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

ParamTypeNotes
platformstringFree-form platform identifier (instagram, x, youtube, tiktok, etc). Server uppercases before matching SocialAccount.platform. Empty platform set returns empty results.

Query parameters โ€” DiscoverDto

ParamTypeDefaultValidationNotes
categorystringโ€”IsString()Content category (e.g. fitness, music). Lowercased before matching CreatorCategory.category.
sortstringrecentIsString()popular (totalMessages DESC), rating (avgRating DESC), or recent (updatedAt DESC; also default)
dmTypeenumโ€”IsEnum(DmType)FREE / SINGLE_PAY / PER_MESSAGE
cursorstringโ€”IsString()Opaque cursor (last result id)
limitnumberdiscover.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

HTTPcode / i18nKeyReason
400(DTO validation)dmType invalid enum, limit outside [1, 50]
429(throttle)Rate limit exceeded (60 req/min)
503features.discover_disabledAdmin kill switch DISCOVER is active

Side effects

  1. Resolve pageSize = min(limit ?? config.discover.page_size ?? 20, 100).
  2. Platform pre-filter โ€” socialAccount.findMany({ platform: <upper>, verified: true, take: 1000 }) to get the userIds set.
  3. Empty platform set โ†’ return { results: [], nextCursor: null, hasMore: false }.
  4. Build where = { ...activeCreatorFilter, userId IN <set> }.
  5. Optional dmType filter applied to where.dmType.
  6. Optional category filter โ€” creatorCategory.findMany({ category: <lower>, take: 1000 }) โ†’ category creator IDs. Empty โ†’ return empty results. Else add where.id IN <categoryIds>.
  7. Resolve orderBy from sort switch (default updatedAt desc).
  8. creatorProfile.findMany({ where, include: user, take: pageSize+1, orderBy, [optional cursor + skip:1] }).
  9. Compute hasMore = creators.length > pageSize; trim, assemble nextCursor, 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

GET
/api/v1/discover/category/{platform}

Path Parameters

platform*string

Query Parameters

category?string

Filter by category

sort?string

Sort order

dmType?string

Filter by DM type

Value in"FREE" | "SINGLE_PAY" | "PER_MESSAGE"
cursor?string

Cursor for cursor-based pagination

limit?number

Number of results to return

Range1 <= value <= 50

Response 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

SourcePathLines
Controllerapps/api-core/src/modules/discover/discover.controller.ts57โ€“68 (byCategory)
DTO (request)apps/api-core/src/modules/discover/dto/index.ts21โ€“37 (DiscoverDto)
DTO (response)apps/api-core/src/modules/discover/dto/discover-response.dto.ts37โ€“46 (CursorPaginatedCreatorsDto)
Serviceapps/api-core/src/modules/discover/discover.service.ts298โ€“361 (getByCategory)
Configapps/api-core/src/modules/config/config.service.tsdiscover.page_size (admin-managed default 20)
Prisma modelspackages/prisma/prisma/schema.prismaSocialAccount (platform pre-filter), CreatorProfile (active + dmType + sort), CreatorCategory (optional content niche)

On this page