Profile
Base URL: https://api.fursahub.com/api/v1
Short Description: The Profile API manages user profile information for Fursa Hub. It handles profile retrieval, updates, username validation, and profile photo management. The API features automatic onboarding completion detection when required profile fields are populated.
Hints:
- Profile updates automatically complete onboarding when
fullName,username, andbioare all set - Usernames must be unique, 3-30 characters, alphanumeric with underscores only
- Profile photos are stored as a list (JSONB) - users can have multiple photos
- One photo can be set as "primary" for display purposes
- Gender field is optional with values:
MALE,FEMALE,OTHER,PREFER_NOT_TO_SAY - All profile endpoints require authentication
Profile Management Flow
Profile Completion (During Onboarding)
- User reaches profile step after completing phone verification and preferences
- User updates profile via
PUT /api/v1/profilewith required fields - Backend auto-detects completion when
fullName,username, andbioare all set - Onboarding status updated to
COMPLETEDautomatically - User redirected to main app/dashboard
Profile Photo Flow
- Upload photo via File Management API (
POST /api/v1/files/upload-singlewith directoryPROFILE) - Add photo URL to profile via
POST /api/v1/profile/photo - Set primary photo via
PATCH /api/v1/profile/photo/primary(optional) - Remove photo via
DELETE /api/v1/profile/photoif needed
Username Selection Flow
- Check availability via
GET /api/v1/profile/username/check?username=desired_name - If available: Proceed with profile update
- If taken: Prompt user to choose different username
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-01-02T10:30:45",
"data": "Error description"
}
Endpoints
1. Get Current Profile
Purpose: Retrieve the authenticated user's complete profile information.
Endpoint: GET {base_url}/profile
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Profile retrieved successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john.doe@gmail.com",
"username": "johndoe",
"phoneNumber": "+255712345678",
"fullName": "John Doe",
"bio": "Passionate software developer with 5+ years of experience in mobile and web development. Looking for exciting opportunities in East Africa's tech ecosystem.",
"gender": "MALE",
"link": "https://linkedin.com/in/johndoe",
"profilePhotoUrls": [
"http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo1.jpg",
"http://localhost:9000/fursa-550e8400.../profile/b2c3d4e5_photo2.jpg"
],
"primaryPhotoUrl": "http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo1.jpg",
"isPhoneVerified": true,
"isEmailVerified": true,
"preferredLanguage": "en",
"authProvider": "GOOGLE",
"role": "ROLE_USER",
"onboardingStatus": "COMPLETED",
"isOnboardingComplete": true,
"createdAt": "2025-01-01T08:00:00",
"updatedAt": "2025-01-02T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
id |
Unique user identifier (UUID) |
email |
User's email address |
username |
Unique username (3-30 chars, alphanumeric + underscore) |
phoneNumber |
Verified phone number in E.164 format |
fullName |
User's display name |
bio |
User's biography/professional summary (max 500 chars) |
gender |
Gender: MALE, FEMALE, OTHER, PREFER_NOT_TO_SAY, or null |
link |
Portfolio/social media URL |
profilePhotoUrls |
Array of profile photo URLs (JSONB list) |
primaryPhotoUrl |
Currently selected primary photo URL |
isPhoneVerified |
Phone verification status |
isEmailVerified |
Email verification status |
preferredLanguage |
Language preference code |
authProvider |
Original auth provider: GOOGLE, APPLE, EMAIL |
role |
User role in the system |
onboardingStatus |
Current onboarding step |
isOnboardingComplete |
Boolean flag for onboarding completion |
createdAt |
Account creation timestamp |
updatedAt |
Last profile update timestamp |
Error Response JSON Sample:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token is missing or invalid",
"action_time": "2025-01-02T10:30:45",
"data": "Token is missing or invalid"
}
2. Update Profile
Purpose: Update user profile fields. Automatically completes onboarding when required fields (fullName, username, bio) are all set.
Endpoint: PUT {base_url}/profile
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"fullName": "John Doe",
"username": "johndoe",
"bio": "Passionate software developer with 5+ years of experience in mobile and web development.",
"gender": "MALE",
"link": "https://linkedin.com/in/johndoe"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
fullName |
string | No* | User's display name | 2-100 characters |
username |
string | No* | Unique username | 3-30 chars, alphanumeric + underscore, must be unique |
bio |
string | No* | Biography/professional summary | Max 500 characters |
gender |
string | No | User's gender | Enum: MALE, FEMALE, OTHER, PREFER_NOT_TO_SAY |
link |
string | No | Portfolio/social URL | Valid URL format, max 500 characters |
*Required for onboarding completion
Auto-Completion Logic: When the following conditions are met, onboarding automatically completes:
fullNameis not null/emptyusernameis not null/emptybiois not null/empty- Current
onboardingStatusisPENDING_PROFILE_COMPLETION
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Profile updated successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john.doe@gmail.com",
"username": "johndoe",
"phoneNumber": "+255712345678",
"fullName": "John Doe",
"bio": "Passionate software developer with 5+ years of experience in mobile and web development.",
"gender": "MALE",
"link": "https://linkedin.com/in/johndoe",
"profilePhotoUrls": [],
"primaryPhotoUrl": null,
"isPhoneVerified": true,
"isEmailVerified": true,
"preferredLanguage": "en",
"authProvider": "GOOGLE",
"role": "ROLE_USER",
"onboardingStatus": "COMPLETED",
"isOnboardingComplete": true,
"createdAt": "2025-01-01T08:00:00",
"updatedAt": "2025-01-02T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
onboardingStatus |
Will change to COMPLETED if auto-completion triggered |
isOnboardingComplete |
Will be true if auto-completion triggered |
updatedAt |
Timestamp of this update |
| (other fields) | Same as Get Profile response |
Error Response JSON Samples:
Username Already Taken (409):
{
"success": false,
"httpStatus": "CONFLICT",
"message": "Username already taken",
"action_time": "2025-01-02T10:30:45",
"data": "Username 'johndoe' is already in use"
}
Validation Error (422):
{
"success": false,
"httpStatus": "UNPROCESSABLE_ENTITY",
"message": "Validation failed",
"action_time": "2025-01-02T10:30:45",
"data": {
"username": "Username must be 3-30 characters, alphanumeric and underscores only",
"bio": "Bio must not exceed 500 characters",
"link": "Must be a valid URL"
}
}
Invalid Gender Value (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid gender value",
"action_time": "2025-01-02T10:30:45",
"data": "Invalid gender: UNKNOWN. Valid values: MALE, FEMALE, OTHER, PREFER_NOT_TO_SAY"
}
3. Check Username Availability
Purpose: Check if a username is available before updating profile.
Endpoint: GET {base_url}/profile/username/check
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
Query Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
username |
string | Yes | Username to check | 3-30 chars, alphanumeric + underscore |
Success Response JSON Sample (Available):
{
"success": true,
"httpStatus": "OK",
"message": "Username is available",
"action_time": "2025-01-02T10:30:45",
"data": {
"username": "johndoe_new",
"available": true
}
}
Success Response JSON Sample (Not Available):
{
"success": true,
"httpStatus": "OK",
"message": "Username is not available",
"action_time": "2025-01-02T10:30:45",
"data": {
"username": "johndoe",
"available": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
username |
The username that was checked |
available |
true if available, false if taken |
Error Response JSON Sample:
Invalid Username Format (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid username format",
"action_time": "2025-01-02T10:30:45",
"data": "Username must be 3-30 characters, containing only letters, numbers, and underscores"
}
4. Add Profile Photo
Purpose: Add a photo URL to the user's profile photo list. Photo must be uploaded first via File Management API.
Endpoint: POST {base_url}/profile/photo
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"photoUrl": "http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo.jpg"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
photoUrl |
string | Yes | URL of uploaded photo | Valid URL format |
Photo Upload Flow:
- First upload photo via
POST /api/v1/files/upload-singlewithdirectory=PROFILE - Get
permanentUrlfrom upload response - Add URL to profile via this endpoint
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Photo added successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"profilePhotoUrls": [
"http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo.jpg"
],
"primaryPhotoUrl": "http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo.jpg",
"totalPhotos": 1
}
}
Success Response Fields:
| Field | Description |
|---|---|
profilePhotoUrls |
Updated list of all profile photo URLs |
primaryPhotoUrl |
Current primary photo (auto-set to first if none selected) |
totalPhotos |
Total number of photos in profile |
Error Response JSON Samples:
Invalid URL (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid photo URL",
"action_time": "2025-01-02T10:30:45",
"data": "Photo URL must be a valid URL"
}
Photo Already Exists (409):
{
"success": false,
"httpStatus": "CONFLICT",
"message": "Photo already exists",
"action_time": "2025-01-02T10:30:45",
"data": "This photo URL is already in your profile"
}
5. Remove Profile Photo
Purpose: Remove a photo URL from the user's profile photo list.
Endpoint: DELETE {base_url}/profile/photo
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"photoUrl": "http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo.jpg"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
photoUrl |
string | Yes | URL of photo to remove | Must exist in profile |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Photo removed successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"profilePhotoUrls": [
"http://localhost:9000/fursa-550e8400.../profile/b2c3d4e5_photo2.jpg"
],
"primaryPhotoUrl": "http://localhost:9000/fursa-550e8400.../profile/b2c3d4e5_photo2.jpg",
"totalPhotos": 1,
"removedUrl": "http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo.jpg"
}
}
Success Response Fields:
| Field | Description |
|---|---|
profilePhotoUrls |
Updated list after removal |
primaryPhotoUrl |
Updated primary (auto-selects next if removed was primary) |
totalPhotos |
Updated total count |
removedUrl |
The URL that was removed |
Error Response JSON Sample:
Photo Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Photo not found",
"action_time": "2025-01-02T10:30:45",
"data": "This photo URL is not in your profile"
}
6. Set Primary Photo
Purpose: Set a specific photo as the primary/main profile photo.
Endpoint: PATCH {base_url}/profile/photo/primary
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"photoUrl": "http://localhost:9000/fursa-550e8400.../profile/b2c3d4e5_photo2.jpg"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
photoUrl |
string | Yes | URL of photo to set as primary | Must exist in profilePhotoUrls |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Primary photo updated",
"action_time": "2025-01-02T10:30:45",
"data": {
"primaryPhotoUrl": "http://localhost:9000/fursa-550e8400.../profile/b2c3d4e5_photo2.jpg",
"profilePhotoUrls": [
"http://localhost:9000/fursa-550e8400.../profile/a1b2c3d4_photo1.jpg",
"http://localhost:9000/fursa-550e8400.../profile/b2c3d4e5_photo2.jpg"
]
}
}
Success Response Fields:
| Field | Description |
|---|---|
primaryPhotoUrl |
The newly set primary photo URL |
profilePhotoUrls |
Complete list of all profile photos |
Error Response JSON Sample:
Photo Not In Profile (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Photo not in profile",
"action_time": "2025-01-02T10:30:45",
"data": "Cannot set primary: This photo URL is not in your profile photo list"
}
Standard Error Types
Application-Level Exceptions (400-499)
| Status Code | HTTP Status | When It Occurs |
|---|---|---|
| 400 | BAD_REQUEST | Invalid input data, invalid gender/username format |
| 401 | UNAUTHORIZED | Token empty, invalid, or expired |
| 404 | NOT_FOUND | Photo URL not found in profile |
| 409 | CONFLICT | Username already taken, photo already exists |
| 422 | UNPROCESSABLE_ENTITY | Validation errors with field details |
Server-Level Exceptions (500+)
| Status Code | HTTP Status | When It Occurs |
|---|---|---|
| 500 | INTERNAL_SERVER_ERROR | Database errors, unexpected failures |
Quick Reference
Username Rules
| Rule | Requirement |
|---|---|
| Length | 3-30 characters |
| Characters | Letters (a-z, A-Z), numbers (0-9), underscore (_) |
| Uniqueness | Must be unique across all users |
| Case | Case-insensitive for uniqueness check |
Profile Field Limits
| Field | Max Length | Required for Onboarding |
|---|---|---|
fullName |
100 chars | ✅ Yes |
username |
30 chars | ✅ Yes |
bio |
500 chars | ✅ Yes |
gender |
- | ❌ No |
link |
500 chars | ❌ No |
Auto-Completion Trigger
Onboarding completes automatically when:
fullName != null AND
username != null AND
bio != null AND
onboardingStatus == PENDING_PROFILE_COMPLETION