Unsubscribe
Public soft-delete endpoint. Sets unsubscribedAt = now() — the row stays for audit but is excluded from active subscriber lists and future newsletter dispatches.
GET /api/v1/creators/unsubscribe — 🌐 Public · Rate limit: 10 req / minute
Soft-unsubscribes a fan from a bio page mailing list. Soft-delete: sets unsubscribedAt = now() rather than removing the row, so the audit trail (when did this person sign up, when did they leave) stays intact. The row is excluded from GET /creators/subscribers (which filters unsubscribedAt IS NULL) and from future newsletter dispatches.
The unsubscribe link in newsletter emails is built with the subscriber's id (not email) as the query param — anyone with the link can unsubscribe that subscriber. This is standard mailing-list UX and intentional.
Request
Query parameters
| Param | Type | Required | Notes |
|---|---|---|---|
id | string | ✓ | The BioEmailSubscriber.id from the unsubscribe link in the newsletter email |
No body, no headers required.
Response
200 OK — SuccessOnlyResponseDto
{
"success": true
}| Field | Type | Notes |
|---|---|---|
success | boolean | Always true on 200 |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
404 | creator.subscribe.not_found | id missing or no BioEmailSubscriber row matches |
429 | (throttle) | Rate limit exceeded (10 req/min) |
Side effects
- Reject empty
id→not_found. bioEmailSubscriber.findUnique({ where: { id: subscriberId } }). Missing →not_found.bioEmailSubscriber.update({ where: { id }, data: { unsubscribedAt: new Date() } }).- Return
{ success: true }. The row is NOT deleted; it's just timestamped as unsubscribed.
Code samples
curl 'https://api.bio.re/api/v1/creators/unsubscribe?id=s1u2b3c4-d5e6-7890-abcd-ef1234567890'async function unsubscribe(subscriberId: string): Promise<void> {
const url = new URL('https://api.bio.re/api/v1/creators/unsubscribe');
url.searchParams.set('id', subscriberId);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Unsubscribe failed'), {
code: json?.error?.code,
});
}
}import { useMutation } from '@tanstack/react-query';
export function useUnsubscribe() {
return useMutation({
mutationFn: async (subscriberId: string) => {
const url = new URL('/api/v1/creators/unsubscribe', window.location.origin);
url.searchParams.set('id', subscriberId);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Unsubscribe failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
},
});
}Try it
Authorization
bearer In: header
Query Parameters
Response Body
application/json
application/json
curl -X GET "https://loading/api/v1/creators/unsubscribe?id=string"{
"success": true
}{
"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 | 329–337 (unsubscribe) |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 714–729 (unsubscribe) |
| Prisma model | packages/prisma/prisma/schema.prisma | BioEmailSubscriber.unsubscribedAt |
Resend Confirmation Email
Generate a fresh confirmation token and re-dispatch the email via the active email provider. Always returns the same safe message — no enumeration leak.
List Subscribers (Creator)
Owner-only paginated list of confirmed, active mailing-list subscribers. Filter is confirmed=true AND unsubscribedAt=null.