BIO.RE
Creator

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).

FieldTypeValidationNotes
displayNamestringMaxLength(100)Optional — overrides User.displayName if present
biostringMaxLength(500)Initial bio for the new BioPage. Falls back to User.displayName (or empty) if absent.
themeIdstringMaxLength(50)Optional — sets BioPage.templateId to this id
usernamestringregex ^[a-z0-9._-]+$, MaxLength(100)Required if User.username is null. Lowercased + trimmed server-side.
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

201 CreatedApiResponseOf<UpgradeToCreatorResponseDto>

{
  "success": true,
  "data": {
    "creatorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "bioPageUrl": "bio.re/johndoe"
  }
}
FieldTypeNotes
creatorIdstring (UUID)CreatorProfile.id — pass to all /creators/:creatorId/* endpoints
bioPageUrlstringbio.re/<resolved-username> — render as the public bio link

Errors

HTTPcode / i18nKeyReason
400creator.upgrade.username_requiredBody 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
404creator.upgrade.user_not_foundToken decoded but user row missing
409creator.upgrade.already_creatorA CreatorProfile already exists for this user (also raised on Prisma P2002 race)
409creator.upgrade.username_unavailableUsername matches a row in ReservedUsername
503features.creator_upgrade_disabledAdmin kill switch CREATOR_UPGRADE is active

Side effects

  1. Reject if CreatorProfile already exists for this user (already_creator).
  2. Resolve username: from body (lowercased + trimmed) or fall back to User.username. If both are null → username_required.
  3. Reject if username matches a ReservedUsername row.
  4. 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), throw already_creator.
  5. 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

POST
/api/v1/creators/upgrade
AuthorizationBearer <token>

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

SourcePathLines
Controllerapps/api-core/src/modules/creator/creator.controller.ts39–50 (upgradeToCreator)
DTO (request)apps/api-core/src/modules/creator/dto/creator.dto.ts5–19 (OnboardingDto)
DTO (response)apps/api-core/src/modules/creator/dto/creator-client-response.dto.ts7–13 (UpgradeToCreatorResponseDto)
Serviceapps/api-core/src/modules/creator/creator.service.ts120–183 (upgradeToCreator)
Kill switch decoratorapps/api-core/src/common/guards/kill-switch.guard.tsRequireKillSwitch('CREATOR_UPGRADE')
Prisma modelspackages/prisma/prisma/schema.prismaCreatorProfile, BioPage, User.intent (UserIntent.CREATOR), ReservedUsername, enum CreatorLevel.BRONZE, enum KycStatus.NOT_STARTED

On this page