List Subscribers (Creator)
Owner-only paginated list of confirmed, active mailing-list subscribers. Filter is confirmed=true AND unsubscribedAt=null.
GET /api/v1/creators/subscribers — 🔑 Bearer
Lists the calling creator's mailing-list subscribers — only those who confirmed their subscription AND have not unsubscribed. Owner-only: the creator's bio page is resolved server-side from the bearer's user id; you don't pass a bioPageId. Pagination via ?page / ?limit, server-clamped to [1, 100] per page.
Sensitive field exposure (known issue). Each item currently includes confirmToken — a server-side comment in the DTO marks this as a TODO to omit/mask in a future iteration. Until then, do not log the response in any client-side observability tool, and treat the field as if it weren't there. It will likely become null on confirmed subscribers anyway (the confirm endpoint sets confirmToken: null), but unconfirmed-then-unsubscribed edge cases may surface real tokens.
Request
Query parameters
| Param | Type | Default | Validation | Notes |
|---|---|---|---|---|
page | number | 1 | ParseIntPipe, server-clamped to >= 1 | Page index (1-based) |
limit | number | 50 | ParseIntPipe, server-clamped to [1, 100] | Items per page |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<SubscribersResponseDto>
{
"success": true,
"data": {
"items": [
{
"id": "s1u2b3c4-d5e6-7890-abcd-ef1234567890",
"bioPageId": "b1a2b3c4-d5e6-7890-abcd-ef1234567890",
"email": "[email protected]",
"name": "Fan Doe",
"subscribedAt": "2026-04-29T20:00:00.000Z",
"unsubscribedAt": null,
"confirmed": true,
"confirmToken": null,
"source": "bio_page",
"createdAt": "2026-04-29T20:00:00.000Z"
}
],
"total": 42
}
}Item fields
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | BioEmailSubscriber.id — pass to the unsubscribe link |
bioPageId | string (UUID) | The bio page this subscription belongs to |
email | string | Lowercased email |
name | string | null | HTML-stripped at write time |
subscribedAt | string (ISO 8601) | When the row was created (subscription start) |
unsubscribedAt | string (ISO 8601) | null | Always null here (filter excludes non-null) |
confirmed | boolean | Always true here (filter requires confirmation) |
confirmToken | string | null | Sensitive — see callout above. Typically null for confirmed subscribers. |
source | string | null | Acquisition source tag (e.g. bio_page) |
createdAt | string (ISO 8601) | DB row creation timestamp |
Top-level fields
| Field | Type | Notes |
|---|---|---|
items | array | Up to limit items, ordered by subscribedAt DESC |
total | number | Total count of confirmed + active subscribers (matching the same filter) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (validation) | page / limit not parseable as int |
401 | (guard) | Missing / invalid bearer token |
404 | creator.bio.not_found | No BioPage for this creator |
Side effects
- Resolve
creatorIdfrom the bearer'suserIdviaprisma.creatorProfile.findUnique({ where: { userId }, select: { id: true } }). - Lookup
BioPagebycreatorId; thrownot_foundif missing. - Clamp pagination:
safePage = max(page, 1),safeLimit = min(max(limit, 1), 100). - Two queries in parallel:
findMany({ where: { bioPageId, confirmed: true, unsubscribedAt: null }, skip, take, orderBy: { subscribedAt: 'desc' } }).count({ where: <same filter> }).
- Return
{ items, total }. No mutations.
Code samples
curl 'https://api.bio.re/api/v1/creators/subscribers?page=1&limit=50' \
-H "Authorization: Bearer $ACCESS_TOKEN"type SubscriberItem = {
id: string;
bioPageId: string;
email: string;
name: string | null;
subscribedAt: string;
unsubscribedAt: string | null;
confirmed: boolean;
confirmToken: string | null;
source: string | null;
createdAt: string;
};
type SubscribersResponse = {
items: SubscriberItem[];
total: number;
};
async function getSubscribers(accessToken: string, page = 1, limit = 50): Promise<SubscribersResponse> {
const url = new URL('https://api.bio.re/api/v1/creators/subscribers');
url.searchParams.set('page', String(page));
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 ?? 'Subscribers fetch failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery } from '@tanstack/react-query';
export const creatorKeys = {
subscribers: (page: number, limit: number) => ['creators', 'subscribers', page, limit] as const,
};
export function useSubscribers(page = 1, limit = 50) {
return useQuery({
queryKey: creatorKeys.subscribers(page, limit),
queryFn: async () => {
const url = new URL('/api/v1/creators/subscribers', window.location.origin);
url.searchParams.set('page', String(page));
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 ?? 'Subscribers fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as SubscribersResponse;
},
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/subscribers?page=0&limit=0"{
"success": true,
"data": {
"items": [
{
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"bioPageId": "19ae7b68-8677-4862-a3d1-274c4a95a121",
"email": "[email protected]",
"name": "string",
"subscribedAt": "2019-08-24T14:15:22Z",
"unsubscribedAt": "2019-08-24T14:15:22Z",
"confirmed": true,
"confirmToken": "string",
"source": "bio_page",
"createdAt": "2019-08-24T14:15:22Z"
}
],
"total": 42
}
}{
"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 | 351–361 (getSubscribers) |
| DTO (response) | apps/api-core/src/modules/creator/dto/creator-client-response.dto.ts | 595–601 (SubscribersResponseDto), 562–593 (SubscriberItemDto) |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 731–744 (getSubscribers) |
| Prisma model | packages/prisma/prisma/schema.prisma | BioEmailSubscriber (filtered confirmed = true AND unsubscribedAt = null) |
Unsubscribe
Public soft-delete endpoint. Sets unsubscribedAt = now() — the row stays for audit but is excluded from active subscriber lists and future newsletter dispatches.
Creator Analytics Overview
Top-line counters for the authenticated creator's bio page — total views, unique visitors (by hashed visitorId), total link clicks, CTR. Scoped to the caller's own bio page; foreign access is impossible.