Update Bio Link
Sparse update of a bio link. URL revalidation re-runs embed auto-detection. Title HTML-stripped + moderation-checked. Schedule end must follow start. Cache invalidation is best-effort.
PATCH /api/v1/creators/links/:linkId — 🔑 Bearer · Rate limit: 30 req / hour
Sparse update of a single BioLink. Ownership is checked via the link's bio page → creator → user chain. URL changes re-validate (http(s):// only, javascript: blocked) and re-trigger embed auto-detection. Title changes run through trust-safety moderation. Schedule bounds are validated when both are present.
Embed re-detection on URL change: when you update url without explicitly setting embedType / embedMeta, the server re-runs detectEmbedType() and writes the freshly detected values. To clear an auto-detected embed without changing the URL, set embedType: 'CUSTOM' with embedMeta: {} explicitly.
Cache invalidation is best-effort. After the DB write, the server tries to look up the owning creatorId to purge the public bio page cache. If that lookup fails (e.g. transient DB issue), the cache will expire naturally on its TTL — the update itself is not rolled back.
Request
Path parameters
| Param | Type | Validation | Notes |
|---|---|---|---|
linkId | string (UUID) | ParseUUIDPipe | Must belong to a bio page owned by the bearer's user |
Body — UpdateBioLinkDto
All fields optional. Send only what you want to change.
| Field | Type | Validation | Notes |
|---|---|---|---|
title | string | MaxLength(100) | HTML-stripped + moderation-checked |
url | string | IsUrl() | Re-validated; auto-detected embed gets re-applied unless embedType/embedMeta also supplied |
icon | string | MaxLength(50) | — |
sortOrder | number | IsInt, Min(0), Max(1000) | — |
active | boolean | — | — |
embedType | enum | IsIn(['YOUTUBE','SPOTIFY','TIKTOK','SOUNDCLOUD','TWITCH','APPLE_MUSIC','CUSTOM']) | Wins over auto-detection |
embedMeta | object | IsObject | Wins over auto-detected meta |
scheduledStart | string (ISO 8601) | IsDateString | Pass null semantically by sending undefined (omit) — no explicit clear-to-null in DTO |
scheduledEnd | string (ISO 8601) | IsDateString | Must be after scheduledStart (when both present) |
isSocial | boolean | — | — |
platform | string | MaxLength(30) | Lowercased server-side; null clears |
| 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 for the post-write state. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | creator.links.schedule_invalid | scheduledEnd <= scheduledStart |
400 | creator.links.content_flagged | Moderation flagged the new title |
400 | creator.links.invalid_url | URL not http(s):// OR contains javascript: |
400 | (DTO validation) | Field length / type / enum failures |
401 | (guard) | Missing / invalid bearer token |
403 | creator.links.not_owner | Link's bio page belongs to a different user |
404 | creator.links.not_found | BioLink row missing |
429 | (throttle) | Rate limit exceeded (30 req/hour) |
Side effects
- Ownership check —
verifyLinkOwnership(linkId, userId)traversesBioLink → BioPage → CreatorProfile.userId; mismatch → 403. - Schedule bounds validation when both supplied.
- Build sparse
dataobject:title→stripHtmlTags().url→validateUrl(). If supplied ANDembedType/embedMetanot supplied, re-rundetectEmbedType()and injectembedType+embedMeta.platform→ lowercased (ornullto clear).scheduledStart/scheduledEnd→ ISO string parsed toDate(ornull).
- Moderation gate (when
titlesupplied) — flag → reject. prisma.bioLink.update({ where: { id: linkId }, data }).- Best-effort cache invalidation — try to look up the link's
creatorId; if found,invalidateBioCache(creatorId). Failures are swallowed (cache will expire naturally).
Code samples
curl -X PATCH https://api.bio.re/api/v1/creators/links/l1a2b3c4-d5e6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title",
"active": false
}'type UpdateBioLinkInput = {
title?: string;
url?: string;
icon?: string;
sortOrder?: number;
active?: boolean;
embedType?: EmbedType;
embedMeta?: Record<string, unknown>;
scheduledStart?: string;
scheduledEnd?: string;
isSocial?: boolean;
platform?: string;
};
async function updateBioLink(accessToken: string, linkId: string, input: UpdateBioLinkInput): Promise<void> {
const res = await fetch(`https://api.bio.re/api/v1/creators/links/${linkId}`, {
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 ?? 'Update link failed'), {
code: json?.error?.code,
});
}
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useUpdateBioLink(creatorId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (vars: { linkId: string; input: UpdateBioLinkInput }) => {
const res = await fetch(`/api/v1/creators/links/${vars.linkId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(vars.input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Update link failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['creators', creatorId, 'bio'] });
},
});
}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
curl -X PATCH "https://loading/api/v1/creators/links/string" \ -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"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/creator/creator.controller.ts | 163–174 (updateLink), 107–114 (verifyLinkOwnership) |
| DTO (request) | apps/api-core/src/modules/creator/dto/creator.dto.ts | 82–115 (UpdateBioLinkDto) |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 393–442 (updateLink), 42–50 (validateUrl), 1065–1081 (detectEmbedType) |
| Moderation | apps/api-core/src/modules/trust-safety/trust-safety.service.ts | checkContent() |
| Prisma models | packages/prisma/prisma/schema.prisma | BioLink, BioPage (relation chain for ownership) |
Add Bio Link
Append a link to the bio page. Validates URL + scheduling, runs title through moderation, blocks manual social links when DM is active, auto-detects embed type, and respects an admin-managed max-links cap.
Delete Bio Link
Hard-delete a single bio link by id. Ownership-checked. The owning creator's public bio cache is invalidated; cache lookup failures don't block the delete.