Register New Account
Create a new BIO.RE account — email + password + terms acceptance, with optional username, OAuth attribution, and locale.
POST /api/v1/auth/register — 🌐 Public · Rate limit: 10 req / hour · Captcha: required
Creates a new user account. Sends a verification email. Returns the new userId so the client can navigate to the email verification flow. Username is optional at registration; users can claim a username later.
This endpoint enforces the platform-wide kill switch platform.registration_enabled (admin-managed). When the switch is off, registration returns 403 Forbidden with i18nKey auth.register.closed.
Request
Headers
| Header | Value | Notes |
|---|---|---|
Content-Type | application/json | Required |
User-Agent | (auto) | Captured into User.registrationDevice |
Accept-Language | (auto) | Used as fallback when body.locale is empty |
cf-connecting-ip | (auto, Cloudflare) | Source for GeoIP User.registrationCountry |
Body — RegisterDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
email | string (RFC 5322) | ✓ | Trimmed + lowercased; disposable domain blocked; MX record checked | Persisted to User.email (unique) |
password | string | ✓ | 8–128 chars; must include upper + lower + digit | bcrypt hashed (salt rounds from config auth.salt_rounds, min 10) |
acceptedTerms | boolean | ✓ | Must equal true | GDPR consent record written |
acceptedPrivacy | boolean | ✓ | Must equal true | GDPR consent record written |
username | string | — | 1–100 chars, ^[a-z0-9._-]+$ | Unique across User + ReservedUsername |
displayName | string | — | max 100 chars | UI display (separate from username) |
intent | 'creator' | 'fan' | — | max 20 chars | Sets User.intent; null = undecided |
captchaToken | string | — | Validated by CaptchaGuard against the active captcha provider (admin-managed via external.captcha.active_provider) | Required when captcha is enabled |
referralCode | string | — | Resolves against User.referralCode first, then ReferralLink.code | Sets User.referredBy |
locale | string | — | Must be in platform.supported_locales | Falls back to Accept-Language then platform default |
utmSource utmMedium utmCampaign utmTerm utmContent | string × 5 | — | max 100 chars each | Permanent attribution |
firstReferrerUrl | string | — | max 2048 chars | Permanent attribution |
firstLandingPage | string | — | max 2048 chars | Permanent attribution |
Field turnstileToken is deprecated — accepted as a backwards-compat alias for captchaToken for one sprint. Do not use in new code; the active provider is admin-selected via external.captcha.active_provider.
Response
201 Created — success
{
"success": true,
"data": {
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"message": "Registration successful. Please check your email to verify your account."
}
}Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | auth.register.invalid_email | Disposable domain, invalid MX, or otherwise rejected by EmailValidatorService |
400 | (DTO validation array) | class-validator failure (password policy, terms not accepted, etc.) |
403 | auth.register.closed | Platform kill switch platform.registration_enabled = false |
409 | auth.register.email_exists | Email already registered |
409 | auth.register.account_previously_deleted | Email matches a GDPR-deleted tombstone (SHA-256 hash) |
409 | auth.register.username_unavailable | Username taken in User or ReservedUsername |
409 | auth.register.referral_code_collision | Generated referral code collided 3 times against ReferralLink.code (rare; client can retry) |
429 | (throttle) | Rate limit exceeded (10 req/hour) |
All non-validation errors follow the platform error envelope:
{ "success": false, "error": { "code": "auth.register.email_exists", "message": "Email already registered", "i18nKey": "auth.register.email_exists", "correlationId": "..." } }Side effects
- Kill-switch check —
platform.registration_enabled(ConfigService). - Email validation — disposable domain + MX record check (
EmailValidatorService). - Email uniqueness — Prisma
User.emailunique. - GDPR tombstone check — SHA-256 of email matched against
email = 'deleted_<hash>@deleted.bio.re'andstatus = DELETED. - Username uniqueness — both
User.usernameandReservedUsername.username. - Referral resolution —
User.referralCodefirst, fallback toReferralLink.code. - Password hashing — bcrypt, configurable salt rounds (min 10).
- Atomic transaction —
Userinsert +EmailVerificationinsert +ConsentRecord(terms, privacy) insert. - Cross-table referral code uniqueness — generated code retried up to 3 times against
ReferralLink.code. - GeoIP lookup — non-blocking; populates
User.registrationCountry. - Welcome / verification email — queued to worker-service, dispatched via the active email provider (admin-managed via
external.email.active_provider; failover handled server-side).
Code samples
curl -X POST https://api.bio.re/api/v1/auth/register \
-H 'Content-Type: application/json' \
-H 'Accept-Language: en' \
-d '{
"email": "[email protected]",
"username": "creator",
"password": "SecureP@ss123",
"acceptedTerms": true,
"acceptedPrivacy": true,
"displayName": "Awesome Creator",
"intent": "creator",
"captchaToken": "<turnstile_token>",
"locale": "en"
}'type RegisterRequest = {
email: string;
password: string;
acceptedTerms: true;
acceptedPrivacy: true;
username?: string;
displayName?: string;
intent?: 'creator' | 'fan';
captchaToken?: string;
referralCode?: string;
locale?: string;
};
type RegisterResponse = { userId: string; message: string };
async function register(input: RegisterRequest): Promise<RegisterResponse> {
const res = await fetch('https://api.bio.re/api/v1/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
const code = json?.error?.code ?? `http_${res.status}`;
throw Object.assign(new Error(json?.error?.message ?? 'Register failed'), { code });
}
return json.data as RegisterResponse;
}import { useMutation } from '@tanstack/react-query';
export function useRegister() {
return useMutation({
mutationFn: async (input: RegisterRequest) => {
const res = await fetch('/api/v1/auth/register', {
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 ?? 'Register failed'), {
code: json?.error?.code ?? `http_${res.status}`,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as RegisterResponse;
},
onSuccess: ({ userId }) => {
// Navigate to email verification page
router.push(`/auth/verify-email?userId=${userId}`);
},
});
}Try it
Staging only. Production API requires admin-managed captcha tokens and is rate-limited. Set NEXT_PUBLIC_OPENAPI_PROXY_URL env var to a staging proxy if you want browser-side requests; otherwise use the playground for request shape inspection only.
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 POST "https://loading/api/v1/auth/register" \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "password": "SecureP@ss123", "acceptedTerms": true, "acceptedPrivacy": true }'{
"success": true,
"data": {
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"message": "Registration successful. Please check your email to verify your account."
}
}{
"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/auth/auth.controller.ts | 60–96 |
| DTO (request) | apps/api-core/src/modules/auth/dto/index.ts | 5–65 (RegisterDto) |
| DTO (response) | apps/api-core/src/modules/auth/dto/response.dto.ts | 6–12 (RegisterResponseDto) |
| Service | apps/api-core/src/modules/auth/auth.service.ts | 126 (register(), ~120 lines) |
| Prisma model | packages/prisma/prisma/schema.prisma | 24 (User) |