Connect Social Account
Verify a creator's ownership of a social account via OAuth callback. Stores the link plus access/refresh tokens for later platform calls. Recomputes totalFollowers.
POST /api/v1/creators/social/connect — 🔑 Bearer · Rate limit: 30 req / hour
Verifies the creator actually owns the social account they're connecting by exchanging the OAuth code (and PKCE codeVerifier for X/Twitter) for the platform's identity. On success, persists the link in SocialAccount (with stored OAuth tokens for future platform calls), creates an initial SocialMetrics row, and recomputes CreatorProfile.totalFollowers across all linked accounts.
The code and redirectUri come from the platform's OAuth callback — your frontend / mobile redirects the user to the platform's auth screen, the platform redirects back with ?code=..., and you pass that here. codeVerifier is required for X/Twitter (PKCE flow) — generate it client-side, store in session storage, send the code_challenge derived from it on the initial auth request, and submit the verifier here.
One social account per (platform, platformUserId) pair. If the same social account is already linked to a different bio.re creator, the call fails with 409 account_linked_elsewhere — not silently overwritten.
Request
Body — ConnectSocialDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
platform | string | ✓ | IsString() | Platform identifier (e.g. instagram, x, youtube, tiktok) — server uses this to pick the OAuth handler |
code | string | ✓ | IsString() | OAuth authorization code from the platform's callback |
redirectUri | string | ✓ | IsString() | The same redirect URI used in the auth request — platforms re-validate it |
codeVerifier | string | optional | IsString() | Required for X/Twitter (PKCE). Other platforms ignore it. |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
201 Created — SuccessOnlyResponseDto
{
"success": true
}| Field | Type | Notes |
|---|---|---|
success | boolean | Always true on 201. Read the connected list back via GET /creators/social to render the updated UI. |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | creator.social.verification_failed | OAuth verification failed (bad code, expired, redirect mismatch, missing PKCE verifier for X) |
400 | (DTO validation) | Missing required fields |
401 | (guard) | Missing / invalid bearer token |
409 | creator.social.already_connected | The same (platform, platformUserId) is already linked to this creator |
409 | creator.social.account_linked_elsewhere | The same (platform, platformUserId) is linked to a different creator |
429 | (throttle) | Rate limit exceeded (30 req/hour) |
Side effects
- Call the social verification handler for
platformwith(code, redirectUri, codeVerifier). This is a server-to-platform OAuth token exchange + identity probe. - If
verified === falseor noplatformUserIdreturned →verification_failed. - Lookup
SocialAccountby(platform, platformUserId). If found and same user →already_connected. If found and different user →account_linked_elsewhere. - Inside one transaction:
SocialAccount.create({ id, userId, platform, platformUserId, platformUsername, verified: true, connectedAt: now(), accessToken?, refreshToken?, tokenExpiresAt? })— OAuth tokens stored when the platform returned them.SocialMetrics.create({ id, socialAccountId, followerCount, lastSyncedAt: now() })— initial follower count from the verification probe.- Sum every
SocialMetrics.followerCountrow for this user →CreatorProfile.totalFollowers.
- Audit log:
[social] Connected {platform} for user {userId} (followers: <count>).
Code samples
curl -X POST https://api.bio.re/api/v1/creators/social/connect \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"platform": "instagram",
"code": "AQBx8...",
"redirectUri": "https://bio.re/auth/callback/instagram"
}'type ConnectSocialInput = {
platform: string;
code: string;
redirectUri: string;
codeVerifier?: string; // required for X/Twitter (PKCE)
};
async function connectSocial(accessToken: string, input: ConnectSocialInput): Promise<void> {
const res = await fetch('https://api.bio.re/api/v1/creators/social/connect', {
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 ?? 'Social connect failed'), {
code: json?.error?.code,
});
}
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useConnectSocial() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: ConnectSocialInput) => {
const res = await fetch('/api/v1/creators/social/connect', {
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 ?? 'Social connect failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['creators', 'social'] });
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
curl -X POST "https://loading/api/v1/creators/social/connect" \ -H "Content-Type: application/json" \ -d '{ "platform": "instagram", "code": "AQBx8...", "redirectUri": "https://bio.re/callback" }'{
"success": 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"
}
}{
"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 | 62–71 (connectSocial) |
| DTO (request) | apps/api-core/src/modules/creator/dto/creator-social.dto.ts | 4–16 (ConnectSocialDto) |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Service | apps/api-core/src/modules/creator/creator.service.ts | 203–263 (connectSocialVerified + connectSocial) |
| OAuth verifier | apps/api-core/src/modules/creator/social/ | socialVerification.verify() (per-platform OAuth token exchange + identity probe) |
| Prisma models | packages/prisma/prisma/schema.prisma | SocialAccount, SocialMetrics, CreatorProfile.totalFollowers |
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.
Disconnect Social Account
Remove a connected social account. Cascade-deletes the SocialMetrics row and recomputes totalFollowers across the creator's remaining linked accounts.