Get Referral Link
Get-or-create the calling user's personal referral link. First call generates a code from their username (or random UUID slice on collision); subsequent calls return the existing one.
GET /api/v1/referral/link โ ๐ Bearer ยท Kill-switched
Returns the calling user's personal referral link. Get-or-create: first read generates a ReferralLink row (code derived from User.username, falling back to an 8-char UUID slice on collision); every subsequent read returns the existing row. There is at most one active ReferralLink per user (uniqueness enforced on ReferralLink.userId).
Cross-table uniqueness. The ReferralLink.code must NOT collide with any User.referralCode (a separate column on the User table โ also a referral identifier) or any ReferralLinkAlias.code (preserved old codes from previous renames). The server retries up to 3 times with random UUID slices on collision; if all 3 retry, the request fails with 400 code_collision.
Username drives the default code AND tracks renames. If the user's username is alice123, their referral link will be bio.re/ref/alice123. When they later rename to alice456, the live ReferralLink.code is repointed to alice456 and the previous code is preserved as a ReferralLinkAlias โ both bio.re/ref/alice123 and bio.re/ref/alice456 keep resolving to the same ReferralLink. See Change Username ยท Referral code side-effect for the rules and the collision skip behavior.
Request
No body, no params.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | โ | JWT from POST /auth/login |
Response
200 OK โ ApiResponseOf<ReferralLinkCodeResponseDto>
{
"success": true,
"data": {
"code": "alice123",
"link": "bio.re/ref/alice123"
}
}| Field | Type | Notes |
|---|---|---|
code | string | The referral identifier โ pass to POST /referral/click/:code for tracking |
link | string | The full shareable URL โ bio.re/ref/<code> |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | referral.link.code_collision | Failed to generate a unique code after 3 retries (rare โ would require username-vs-other-user collision plus 2 UUID slice collisions in a row) |
401 | (guard) | Missing / invalid bearer token |
503 | features.referral_disabled | Admin kill switch REFERRAL is active |
Side effects
referralLink.findUnique({ where: { userId } }). Existing row โ return{ code, link }directly.- No row โ load
User.usernamefor the candidate code; fallback torandomUUID().slice(0, 8)if null. - Cross-table collision check (up to 3 attempts), run as one
Promise.all:user.findUnique({ where: { referralCode: code } })โ clash with someone'sUser.referralCode.referralLinkAlias.findUnique({ where: { code } })โ clash with a preserved old vanity code from a previous rename.- On hit, regenerate with
randomUUID().slice(0, 8)and retry. - 3rd failure โ throw
code_collision. - Note: collision against another
ReferralLink.codeis enforced by the unique constraint atreferralLink.create()time and surfaces as PrismaP2002; explicit pre-checks cover the cross-table cases the unique index can't.
referralLink.create({ id, userId, code }).- Return
{ code, link: 'bio.re/ref/<code>' }.
Alias resolution
Any code passed to a referral lookup (e.g. POST /referral/click/:code, plus all auth/oauth signup callers that accept a referrer code) is resolved through the canonical alias-aware helper:
referralLink.findUnique({ where: { code } })โ direct hit on the live code.- If no direct hit:
referralLinkAlias.findUnique({ where: { code }, include: { referralLink: true } })โ fall back to a preserved old vanity code. - Returns the underlying
ReferralLinkrow in either case (ornullif neither matches).
This means:
- A user's link has at most one live code (
ReferralLink.code) but may have multiple aliases (one per past vanity rename โ see Change Username ยท Referral code side-effect). - Old shared URLs never break and keep crediting the original referrer.
- All counters (
clicks,signups,conversions) and reward rows live onReferralLink. Aliases carry no counters of their own โ they only mapcode โ referralLinkIdโ so click-traffic onbio.re/ref/<oldcode>andbio.re/ref/<newcode>aggregates to the sameReferralLink, with no double-counting. - A code currently held by any of
ReferralLink.code,ReferralLinkAlias.code, orUser.referralCodecannot be claimed as a new link or rename target.
Code samples
curl https://api.bio.re/api/v1/referral/link \
-H "Authorization: Bearer $ACCESS_TOKEN"type ReferralLink = {
code: string;
link: string;
};
async function getReferralLink(accessToken: string): Promise<ReferralLink> {
const res = await fetch('https://api.bio.re/api/v1/referral/link', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Referral link fetch failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery } from '@tanstack/react-query';
export const referralKeys = {
link: () => ['referral', 'link'] as const,
};
export function useReferralLink() {
return useQuery({
queryKey: referralKeys.link(),
queryFn: async () => {
const res = await fetch('/api/v1/referral/link');
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Referral link fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as ReferralLink;
},
// Code is stable per session but may change after a username rename
// (vanity sync โ see /docs/user/username#referral-code-side-effect).
// Invalidate ['referral', 'link'] from the username-change mutation's onSuccess.
staleTime: 60 * 60_000,
gcTime: 60 * 60_000,
});
}Try it
Authorization
bearer In: header
Response Body
application/json
application/json
curl -X GET "https://loading/api/v1/referral/link"{
"success": true,
"data": {
"code": "alice123",
"link": "bio.re/ref/alice123"
}
}{
"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/referral/referral.controller.ts | 42โ47 (getLink) |
| DTO (response) | apps/api-core/src/modules/referral/dto/referral-response.dto.ts | 298โ304 (ReferralLinkCodeResponseDto) |
| Service (get-or-create) | apps/api-core/src/modules/referral/referral.service.ts | 34โ61 (getOrCreateLink) |
| Service (alias resolver) | apps/api-core/src/modules/referral/referral.service.ts | 23โ32 (resolveLinkByCode) โ used by trackClick, recordSignup, and auth/oauth signup callers (apps/api-core/src/modules/auth/auth.service.ts:174,191, apps/api-core/src/modules/auth/oauth.service.ts:273) |
| Service (rename sync) | apps/api-core/src/modules/user/user.service.ts | 209โ230 (referral block inside changeUsername transaction) |
| Kill switch | apps/api-core/src/common/guards/kill-switch.guard.ts | RequireKillSwitch('REFERRAL') (class-level on ReferralController, line 35) |
| Prisma models | packages/prisma/prisma/schema.prisma | ReferralLink 2103โ2119 (unique userId, unique code, has-many aliases), ReferralLinkAlias 2121โ2130 (unique code, FK referralLinkId cascade), User.referralCode (cross-table uniqueness check) |
| Live response | MISSING โ to be captured by Lead before publish | โ |
Get Last-Seen + Online Status
REST read of socket-written presence state. Returns isOnline (live socket connection) and lastSeen (ISO-8601 of final disconnect, ~30-day TTL). Soft-degrades to "offline + null" when the session Redis is unreachable.
Track Referral Click
Public endpoint. Increments the click counter on a ReferralLink. Silent on missing/inactive codes โ the request never fails.