Update Bio Page
Sparse update of the BioPage. Bio strips HTML tags, customCss strips XSS vectors, bio body runs through trust-safety content moderation. Cache invalidated on success.
PATCH /api/v1/creators/:creatorId/bio — 🔑 Bearer · Rate limit: 30 req / hour
Sparse update — only fields present in the body are written. bio is HTML-stripped, customCss is XSS-sanitized (expression(...), javascript:, non-https url(...) removed), and bio content runs through the trust-safety moderation gate before persistence. On success, the cached public bio page for this creator is invalidated.
XSS sanitization is destructive. Submitted <script> / <img onerror=...> etc. will be silently stripped — the response shape ({ success: true }) doesn't tell you what was removed. Validate client-side too if you want to surface "we removed N tags" UI.
Content moderation gate (no NSFW). When bio is supplied, it is run through trustSafetyService.checkContent(). If the moderation flags it, the entire request fails with 400 creator.bio.content_flagged and no fields are written (the moderation check runs after sparse-data assembly but before the DB write).
Request
Path parameters
| Param | Type | Validation | Notes |
|---|---|---|---|
creatorId | string (UUID) | ParseUUIDPipe | Must match the bearer's CreatorProfile.id (otherwise 403) |
Body — UpdateBioPageDto
All fields optional. Send only what you want to change.
| Field | Type | Validation | Notes |
|---|---|---|---|
bio | string | MaxLength(5000) | HTML-stripped server-side AND moderation-checked |
templateId | string (UUID) | null | IsUUID('4') (when not null) | Set to null to clear |
themeOverride | object | — | Free-form JSON; not sanitized — keep design tokens, don't smuggle scripts |
customCss | string | MaxLength(10000) | XSS-sanitized (see above) |
embedEnabled | boolean | — | Gates GET /bio/embed/:username |
published | boolean | — | When false, public render returns 404 |
emailCollectionEnabled | boolean | — | Gates POST /creators/:bioPageId/subscribe |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — SuccessOnlyResponseDto
{
"success": true
}| Field | Type | Notes |
|---|---|---|
success | boolean | Always true on 200. Re-fetch via GET /creators/:creatorId/bio to see the post-write state. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | bio > 5000, customCss > 10000, templateId not a UUID |
400 | creator.bio.content_flagged | Moderation flagged the submitted bio |
401 | (guard) | Missing / invalid bearer token |
403 | (verifyCreatorOwnership) | creatorId does not belong to the bearer's user |
404 | creator.bio.not_found | BioPage row missing for this creator |
429 | (throttle) | Rate limit exceeded (30 req/hour) |
Side effects
- Ownership check —
verifyCreatorOwnership(creatorId, userId)→ 403 on mismatch. - Build sparse
dataobject from defined keys only. - Sanitize:
bio→stripHtmlTags()(regex/<[^>]*>/g).customCss→sanitizeCss()(blocksexpression(...),javascript:, non-https/dataurl(...)rewritten tourl(about:blank, plus tag strip).
- Moderation gate (only when
biois supplied):trustSafetyService.checkContent(input.bio). Ifflagged, throwcontent_flagged— no DB write occurs. prisma.bioPage.update({ where: { creatorId }, data }).- Audit log:
[bio] Updated for creator {creatorId}. - Cache invalidation —
invalidateBioCache(creatorId)purges the public-render cache for this creator's username so the nextGET /bio/:usernamere-builds from DB.
Code samples
curl -X PATCH https://api.bio.re/api/v1/creators/c1a2b3c4-d5e6-7890-abcd-ef1234567890/bio \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"bio": "Designer & creator",
"published": true,
"emailCollectionEnabled": true
}'type UpdateBioPageInput = {
bio?: string;
templateId?: string | null;
themeOverride?: Record<string, unknown>;
customCss?: string;
embedEnabled?: boolean;
published?: boolean;
emailCollectionEnabled?: boolean;
};
async function updateBioPage(accessToken: string, creatorId: string, input: UpdateBioPageInput): Promise<void> {
const res = await fetch(`https://api.bio.re/api/v1/creators/${creatorId}/bio`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Bio page update failed'), {
code: json?.error?.code,
});
}
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useUpdateBioPage(creatorId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: UpdateBioPageInput) => {
const res = await fetch(`/api/v1/creators/${creatorId}/bio`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Bio page update failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['creators', creatorId, 'bio'] });
qc.invalidateQueries({ queryKey: ['creators', 'profile'] });
},
});
}Try it
Authorization
bearer In: header
Path Parameters
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X PATCH "https://loading/api/v1/creators/string/bio" \ -H "Content-Type: application/json" \ -d '{}'{
"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"
}
}{
"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"
}
}{
"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 | 136–148 (updateBioPage) |
| DTO (request) | apps/api-core/src/modules/creator/dto/creator.dto.ts | 21–45 (UpdateBioPageDto) |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 306–327 (updateBioPage), 30–32 (stripHtmlTags), 34–40 (sanitizeCss) |
| Moderation gate | apps/api-core/src/modules/trust-safety/trust-safety.service.ts | checkContent() |
| Cache | apps/api-core/src/modules/creator/bio-analytics.service.ts | invalidateCache() (in-memory bio page cache) |
| Prisma model | packages/prisma/prisma/schema.prisma | BioPage (bio / templateId / themeOverride / customCss / embedEnabled / published / emailCollectionEnabled) |
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.
List Bio Templates
Public catalog of active bio page templates. CDN-cached (5min). Used in the creator editor to render the template picker.