Reorder Bio Links
Atomic reordering of bio page links. Submit the full ordered array; sortOrder is set to the array index. Server verifies every linkId belongs to the creator before writing.
POST /api/v1/creators/:creatorId/links/reorder — 🔑 Bearer · Rate limit: 30 req / hour
Atomic reorder. Submit the full ordered list of linkIds; the server sets sortOrder = index for each, all inside a single Prisma transaction. The server verifies every submitted id belongs to the creator's bio page before writing — partial / cross-creator id sets are rejected wholesale.
Submit the full list — not a delta. If you omit a link's id from the array, its sortOrder is not changed. There is no implicit "leave others alone" — your client must always send the complete current ordering as you'd like it persisted.
Cross-creator id rejection. The service runs findMany({ where: { bioPageId, id: { in: linkIds } } }) and compares lengths — any id that doesn't belong to this creator's bio page causes the whole call to fail with 400 not_owned. No partial writes leak.
Request
Path parameters
| Param | Type | Validation | Notes |
|---|---|---|---|
creatorId | string (UUID) | ParseUUIDPipe | Must match the bearer's CreatorProfile.id (otherwise 403) |
Body — ReorderLinksDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
linkIds | string[] | ✓ | IsArray(), ArrayUnique() | The full ordered set of link ids. Position in the array becomes sortOrder. |
| 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 read the post-write order. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | linkIds not unique / not an array |
400 | creator.links.not_owned | At least one submitted linkId doesn't belong to this creator's bio page |
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. - Lookup
BioPagebycreatorId; thrownot_foundif missing. - Cross-creator id verification —
bioLink.findMany({ where: { bioPageId, id: { in: linkIds } } })(max 100). Ifresult.length !== linkIds.length→ thrownot_owned. No writes occur. - Atomic write — build N
bioLink.update({ where: { id }, data: { sortOrder: index } })operations and run them inside a singleprisma.$transaction(updates)so all positions land or none do. - Cache invalidation —
invalidateBioCache(creatorId).
Code samples
curl -X POST https://api.bio.re/api/v1/creators/c1a2b3c4-d5e6-7890-abcd-ef1234567890/links/reorder \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"linkIds": [
"l3a2b3c4-d5e6-7890-abcd-ef1234567890",
"l1a2b3c4-d5e6-7890-abcd-ef1234567890",
"l2a2b3c4-d5e6-7890-abcd-ef1234567890"
]
}'async function reorderBioLinks(accessToken: string, creatorId: string, linkIds: string[]): Promise<void> {
const res = await fetch(`https://api.bio.re/api/v1/creators/${creatorId}/links/reorder`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ linkIds }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Reorder failed'), {
code: json?.error?.code,
});
}
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useReorderBioLinks(creatorId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (linkIds: string[]) => {
const res = await fetch(`/api/v1/creators/${creatorId}/links/reorder`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ linkIds }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Reorder failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
},
// Optimistic UI: locally re-sort, then call mutation; on error revert
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
curl -X POST "https://loading/api/v1/creators/string/links/reorder" \ -H "Content-Type: application/json" \ -d '{ "linkIds": [ "link_1", "link_2", "link_3" ] }'{
"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"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/creator/creator.controller.ts | 189–200 (reorderLinks) |
| DTO (request) | apps/api-core/src/modules/creator/dto/creator-social.dto.ts | 35–38 (ReorderLinksDto) |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 464–483 (reorderLinks) |
| Prisma models | packages/prisma/prisma/schema.prisma | BioPage, BioLink.sortOrder |
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.
Get Embeddable Bio Page
Iframe-friendly variant of the public bio render — same payload, but only succeeds when embedEnabled is true. Sets X-Frame-Options ALLOWALL so partner sites can host the iframe.