Get Recent Activity
Merged feed of the creator's recent messages, processed payouts, and new bio-page subscribers. Three sources combined, sorted by timestamp DESC, sliced to limit.
GET /api/v1/creators/dashboard/activity — 🔑 Bearer
Returns a merged activity feed of the creator's recent messages received, processed payouts, and new confirmed bio-page email subscribers. Each source pulls up to limit items independently; the union is sorted by timestamp descending and sliced to limit. Default limit is 10, server clamps to [1, 20].
Why bio-email subscribers stand in for "followers". There is no dedicated Follow model today — the closest signal of "someone newly engaged with this creator" is a confirmed BioEmailSubscriber. That choice keeps the activity feed populated for new creators without inventing data. If a Follow model is added later, this endpoint will gain a new activity type rather than replacing this one.
Limit clamps server-side. ?limit=0 becomes 1, ?limit=999 becomes 20. Pagination beyond 20 isn't exposed — design the UI for "recent activity glance" not full history.
Request
Query parameters
| Param | Type | Default | Validation | Notes |
|---|---|---|---|---|
limit | number | 10 | ParseIntPipe, server-clamped to [1, 20] | How many items to return after merge |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<ActivityResponseDto>
{
"success": true,
"data": {
"items": [
{
"type": "message_received",
"title": "New message from @johndoe",
"timestamp": "2026-04-29T20:04:00.000Z",
"meta": { "messageId": "m1a2b3c4-d5e6-7890-abcd-ef1234567890" }
},
{
"type": "payout_processed",
"title": "Payout of 250.00 processed",
"timestamp": "2026-04-29T19:50:00.000Z",
"meta": { "payoutId": "p1a2y3o4-u5t6-7890-abcd-ef1234567890", "amount": "250.00" }
},
{
"type": "follower_new",
"title": "Alice subscribed to your bio",
"timestamp": "2026-04-29T19:30:00.000Z",
"meta": { "subscriberId": "s1u2b3c4-d5e6-7890-abcd-ef1234567890" }
}
]
}
}Activity types
type | Source | title template | meta |
|---|---|---|---|
message_received | Message where receiverId = userId | New message from <senderDisplayName ?? senderUsername ?? 'Someone'> | { messageId } |
payout_processed | PayoutRequest where creatorId AND status = PROCESSED | Payout of <amount> processed | { payoutId, amount } (amount as string for decimal precision) |
follower_new | BioEmailSubscriber where bioPage.creatorId AND confirmed = true | <name ?? 'Someone'> subscribed to your bio | { subscriberId } |
Item fields
| Field | Type | Notes |
|---|---|---|
type | string | One of the three values above. Render-by-type, not text parsing. |
title | string | Server-rendered English. Localize client-side off type + meta if needed. |
timestamp | string (ISO 8601) | Source-specific timestamp: Message.createdAt / PayoutRequest.processedAt ?? createdAt / BioEmailSubscriber.subscribedAt |
meta | object | Type-dependent extras (see table). Optional — absent in the rare case of no extras. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
404 | creator.activity.not_found | No CreatorProfile for this user |
Side effects
- Lookup
CreatorProfile.idbyuserId; thrownot_foundif missing. - Clamp
limitto[1, 20](safeLimit). - In parallel (
Promise.all):Message.findMany({ where: receiverId, orderBy createdAt desc, take safeLimit, select id + createdAt + sender { displayName, username } }).PayoutRequest.findMany({ where creatorId AND status PROCESSED, orderBy processedAt desc, take safeLimit, select id + amount + processedAt + createdAt }).BioEmailSubscriber.findMany({ where bioPage.creatorId AND confirmed, orderBy subscribedAt desc, take safeLimit, select id + name + subscribedAt }).
- Normalize all rows to the common
{ type, title, timestamp, meta? }shape. - Sort by
timestamp DESC; slice tosafeLimit. - Convert
Datetimestamps to ISO 8601 strings; return{ items }. No mutations.
Code samples
curl 'https://api.bio.re/api/v1/creators/dashboard/activity?limit=10' \
-H "Authorization: Bearer $ACCESS_TOKEN"type ActivityType = 'message_received' | 'payout_processed' | 'follower_new';
type ActivityItem = {
type: ActivityType;
title: string;
timestamp: string;
meta?: Record<string, unknown>;
};
async function getActivity(accessToken: string, limit = 10): Promise<ActivityItem[]> {
const url = new URL('https://api.bio.re/api/v1/creators/dashboard/activity');
url.searchParams.set('limit', String(limit));
const res = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Activity fetch failed'), {
code: json?.error?.code,
});
}
return json.data.items;
}import { useQuery } from '@tanstack/react-query';
export const creatorKeys = {
activity: (limit: number) => ['creators', 'dashboard', 'activity', limit] as const,
};
export function useActivity(limit = 10) {
return useQuery({
queryKey: creatorKeys.activity(limit),
queryFn: async () => {
const url = new URL('/api/v1/creators/dashboard/activity', window.location.origin);
url.searchParams.set('limit', String(limit));
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Activity fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data.items as ActivityItem[];
},
staleTime: 30_000,
});
}Try it
Authorization
bearer In: header
Query Parameters
Response Body
application/json
application/json
application/json
curl -X GET "https://loading/api/v1/creators/dashboard/activity?limit=0"{
"success": true,
"data": {
"items": [
{
"type": "message_received",
"title": "New message from @johndoe",
"timestamp": "2019-08-24T14:15:22Z",
"meta": {}
}
]
}
}{
"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/creator.controller.ts | 112–122 (getActivity) |
| DTO (response) | apps/api-core/src/modules/creator/dto/creator-client-response.dto.ts | 437–440 (ActivityResponseDto), 423–435 (nested ActivityItemDto) |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 982–1059 (getActivity) |
| Prisma models | packages/prisma/prisma/schema.prisma | Message (with User join via sender), PayoutRequest (status PROCESSED), BioEmailSubscriber (confirmed = true), BioPage (relation), CreatorProfile |
Get Setup Checklist
Six-item creator onboarding checklist with completion flags. Static set today (avatar, bio, link, DM config, KYC, social) — drives the post-upgrade getting-started UI.
Get Bio Page (Editor)
Read the creator's own BioPage row plus its links (ordered) and template. Ownership-checked. Use this for the editor — public fan-side rendering uses GET /bio/:username instead.