Track Bio Link Click
Append a click record + increment BioLink.clickCount. Two writes happen in parallel; a missing link silently no-ops the increment but still records the view.
POST /api/v1/bio/:bioPageId/click — 🌐 Public · Rate limit: 60 req / minute
Records a click on a bio link. Two writes run in parallel:
BioLink.clickCount += 1(silently swallows errors — analytics is non-critical)- Append a
BioPageViewrow withlinkClicked = linkId
Both are best-effort fire-and-forget — a non-existent linkId won't fail the request; the view row is still inserted (with linkClicked referencing the bad id) for forensic visibility.
The endpoint records the click on the bio page scope, not the link scope — that's why bioPageId is the path param and linkId is in the body. A single page can hold many links, and the analytics table tracks everything against the page so the breakdowns aggregate cleanly.
Request
Path parameters
| Param | Type | Validation | Notes |
|---|---|---|---|
bioPageId | string (UUID) | ParseUUIDPipe | The page hosting the clicked link |
Body — TrackClickDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
linkId | string (UUID) | ✓ | IsUUID() | The link that was clicked. A bad id silently no-ops the clickCount increment but the view row is still recorded. |
visitorId | string | optional | IsString() | Stable client-generated id (cookie / localStorage UUID) for unique-visitor de-duplication |
The DTO does not carry referrer — the controller passes it as undefined to the service and the row is inserted without it.
No headers required.
Response
200 OK — ApiResponseOf<TrackResponseDto>
{
"success": true,
"data": {
"tracked": true
}
}| Field | Type | Notes |
|---|---|---|
tracked | boolean | Always true on 200 |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | bioPageId not UUID; linkId missing or not UUID |
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
In parallel (Promise.all):
prisma.bioLink.update({ where: { id: linkId }, data: { clickCount: { increment: 1 } } })— wrapped in.catch(() => {})so a non-existent or already-deleted link doesn't fail the request.prisma.bioPageView.create({ id: randomUUID(), bioPageId, visitorId, referrer: undefined, linkClicked: linkId }).
Both writes are independent — failure of one doesn't roll back the other.
Code samples
curl -X POST 'https://api.bio.re/api/v1/bio/b1a2b3c4-d5e6-7890-abcd-ef1234567890/click' \
-H 'Content-Type: application/json' \
-d '{
"linkId": "l1a2b3c4-d5e6-7890-abcd-ef1234567890",
"visitorId": "cookie-uuid-12345"
}'type TrackClickInput = {
linkId: string;
visitorId?: string;
};
async function trackBioClick(bioPageId: string, input: TrackClickInput): Promise<void> {
const res = await fetch(`https://api.bio.re/api/v1/bio/${bioPageId}/click`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!res.ok) {
console.warn('Bio click tracking failed:', res.status);
}
}import { useMutation } from '@tanstack/react-query';
export function useTrackBioClick() {
return useMutation({
mutationFn: async (vars: { bioPageId: string; input: TrackClickInput }) => {
await fetch(`/api/v1/bio/${vars.bioPageId}/click`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(vars.input),
});
// Don't throw — analytics failures must not block the actual link navigation
},
retry: false,
});
}
// Usage: fire the mutation in onClick, then perform the navigation immediately
// (don't await — the click should happen even if tracking is slow)Try it
Path Parameters
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
curl -X POST "https://loading/api/v1/bio/string/click" \ -H "Content-Type: application/json" \ -d '{ "linkId": "009f739c-6620-43b0-978e-b245e723c57a" }'{
"success": true,
"data": {
"tracked": 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"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/creator/bio-analytics.controller.ts | 113–124 (trackClick) |
| DTO (request) | apps/api-core/src/modules/creator/dto/bio-analytics.dto.ts | 14–17 (TrackClickDto) |
| DTO (response) | apps/api-core/src/modules/creator/dto/bio-analytics-response.dto.ts | 214–217 (TrackResponseDto) |
| Service | apps/api-core/src/modules/creator/bio-analytics.service.ts | 207–224 (trackLinkClick) |
| Prisma models | packages/prisma/prisma/schema.prisma | BioLink.clickCount (counter), BioPageView (with linkClicked field) |
Track Bio Page View
Append a view record. Server parses User-Agent and resolves country from CDN headers when the client doesn't supply them. Creator-side analytics aggregate from this stream.
Get Bio Analytics
Owner-only analytics aggregate for a bio page over a configurable time window. Returns total/unique views, top referrers/devices/countries, and per-day view counts.