Upgrade to Creator
Convert an authenticated user into a creator. Atomic — creates CreatorProfile + BioPage + flips User.intent to CREATOR. Gated by an admin kill switch.
POST /api/v1/creators/upgrade — 🔑 Bearer · Rate limit: 30 req / hour
Converts a fan account into a creator. Atomic: in one transaction, creates a CreatorProfile (level BRONZE, kycStatus = NOT_STARTED), creates a BioPage (published), and updates User.intent = CREATOR (also sets displayName / username if supplied). Returns the new creatorId and the public bioPageUrl.
Username is mandatory. If the user already has a username on their User row, you can omit it from the body — it will be reused. If the user has never set a username, the body must include one (lowercase alphanumeric + ._-, max 100 chars). Reserved usernames (e.g. system-protected) are rejected with 409.
Admin kill switch. When the CREATOR_UPGRADE kill switch is active (admin-managed), every call returns 503 features.creator_upgrade_disabled regardless of body. Surface a "creator signups are paused" UI when you see that response.
Request
Body — OnboardingDto
All fields optional except username is conditionally required (see callout).
| Field | Type | Validation | Notes |
|---|---|---|---|
displayName | string | MaxLength(100) | Optional — overrides User.displayName if present |
bio | string | MaxLength(500) | Initial bio for the new BioPage. Falls back to User.displayName (or empty) if absent. |
themeId | string | MaxLength(50) | Optional — sets BioPage.templateId to this id |
username | string | regex ^[a-z0-9._-]+$, MaxLength(100) | Required if User.username is null. Lowercased + trimmed server-side. |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
201 Created — ApiResponseOf<UpgradeToCreatorResponseDto>
{
"success": true,
"data": {
"creatorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"bioPageUrl": "bio.re/johndoe"
}
}| Field | Type | Notes |
|---|---|---|
creatorId | string (UUID) | CreatorProfile.id — pass to all /creators/:creatorId/* endpoints |
bioPageUrl | string | bio.re/<resolved-username> — render as the public bio link |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | creator.upgrade.username_required | Body has no username AND User.username is null |
400 | (DTO validation) | username regex / length fail; displayName > 100; bio > 500 |
401 | (guard) | Missing / invalid bearer token |
404 | creator.upgrade.user_not_found | Token decoded but user row missing |
409 | creator.upgrade.already_creator | A CreatorProfile already exists for this user (also raised on Prisma P2002 race) |
409 | creator.upgrade.username_unavailable | Username matches a row in ReservedUsername |
503 | features.creator_upgrade_disabled | Admin kill switch CREATOR_UPGRADE is active |
Side effects
- Reject if
CreatorProfilealready exists for this user (already_creator). - Resolve
username: from body (lowercased + trimmed) or fall back toUser.username. If both are null →username_required. - Reject if username matches a
ReservedUsernamerow. - Inside one transaction (atomic):
CreatorProfile.create({ id: randomUUID(), userId, level: BRONZE, kycStatus: NOT_STARTED }).BioPage.create({ id: randomUUID(), creatorId, bio: input.bio ?? user.displayName ?? '', templateId: input.themeId ?? null, published: true }).User.update({ intent: CREATOR, [+ displayName if supplied, + username if changed] }).- On Prisma
P2002(concurrent upgrade race), throwalready_creator.
- Audit log:
[creator] Upgraded user {userId} → creator {creatorId}.
Code samples
curl -X POST https://api.bio.re/api/v1/creators/upgrade \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"displayName": "Jane Doe",
"bio": "Designer & creator",
"username": "janedoe"
}'type UpgradeInput = {
displayName?: string;
bio?: string;
themeId?: string;
username?: string; // required if User.username is null
};
type UpgradeResponse = {
creatorId: string;
bioPageUrl: string;
};
async function upgradeToCreator(accessToken: string, input: UpgradeInput): Promise<UpgradeResponse> {
const res = await fetch('https://api.bio.re/api/v1/creators/upgrade', {
method: 'POST',
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 ?? 'Upgrade failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useUpgradeToCreator() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: UpgradeInput) => {
const res = await fetch('/api/v1/creators/upgrade', {
method: 'POST',
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 ?? 'Upgrade failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as UpgradeResponse;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['users', 'profile'] });
qc.invalidateQueries({ queryKey: ['auth', 'me'] });
qc.invalidateQueries({ queryKey: ['creators', 'profile'] });
},
});
}Try it
Authorization
bearer In: header
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 POST "https://loading/api/v1/creators/upgrade" \ -H "Content-Type: application/json" \ -d '{}'{
"success": true,
"data": {
"creatorId": "688ebf54-d343-4104-8711-82c2feac534a",
"bioPageUrl": "https://bio.re/johndoe"
}
}{
"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 | 39–50 (upgradeToCreator) |
| DTO (request) | apps/api-core/src/modules/creator/dto/creator.dto.ts | 5–19 (OnboardingDto) |
| DTO (response) | apps/api-core/src/modules/creator/dto/creator-client-response.dto.ts | 7–13 (UpgradeToCreatorResponseDto) |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 120–183 (upgradeToCreator) |
| Kill switch decorator | apps/api-core/src/common/guards/kill-switch.guard.ts | RequireKillSwitch('CREATOR_UPGRADE') |
| Prisma models | packages/prisma/prisma/schema.prisma | CreatorProfile, BioPage, User.intent (UserIntent.CREATOR), ReservedUsername, enum CreatorLevel.BRONZE, enum KycStatus.NOT_STARTED |
Save Cookie Consent
Public endpoint to save user's consent choices. Works for anonymous (userId null) and authenticated. Stores under documentType COOKIE_CONSENT with current privacy version. IP captured ONLY when analytics consented (GDPR).
Get Creator Profile
Read the calling creator's full profile — level, KYC, DM pricing, vacation, earnings, Stripe Connect status, bank details, plus the joined BioPage (with links + template), categories, and active DM packages.