Auth, Files, Profile & Onboarding


Authentication

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-01-05
Version: v1.0

Base URL: https://api.fursahub.com/api/v1

Short Description: Authentication endpoints for Fursa Hub platform. Handles user registration/login via Firebase, token management, and session control. All new users start the onboarding flow after successful authentication.

Hints:


Authentication Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                        AUTHENTICATION FLOW                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  1. USER OPENS APP                                                       │
│     └── App shows language selector (calls GET /languages)              │
│     └── User picks language (e.g., "sw" for Swahili)                    │
│     └── App stores language + theme preference locally                   │
│                                                                          │
│  2. USER SIGNS IN VIA FIREBASE                                          │
│     └── Google Sign-In / Apple Sign-In / Email+Password                 │
│     └── Firebase returns ID Token                                        │
│                                                                          │
│  3. APP SENDS TO FURSA HUB BACKEND                                      │
│     └── POST /auth/firebase/authenticate                                 │
│     └── Include: firebaseToken, preferredLanguage, theme                │
│                                                                          │
│  4. BACKEND RESPONSE                                                     │
│     ├── NEW USER: Creates account, returns tokens + onboarding status   │
│     └── EXISTING USER: Returns tokens + current onboarding status       │
│                                                                          │
│  5. CHECK ONBOARDING STATUS                                              │
│     └── onboarding.isComplete = false → Navigate to onboarding flow     │
│     └── onboarding.isComplete = true → Navigate to home screen          │
│                                                                          │
│  6. TOKEN MANAGEMENT                                                     │
│     └── Store accessToken (for API calls)                               │
│     └── Store refreshToken (for renewing accessToken)                   │
│     └── When accessToken expires → POST /auth/refresh                   │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Standard Response Format

Success Response Structure

{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-01-05T10:30:45",
  "data": { }
}

Error Response Structure

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-01-05T10:30:45",
  "data": "Error description"
}

Endpoints


1. Authenticate with Firebase

Purpose: Exchange Firebase ID token for Fursa Hub access tokens. Creates new user if first time.

Endpoint: POST {base_url}/auth/firebase/authenticate

Access Level: 🌐 Public

Authentication: None (Firebase token in body)

Request Headers:

Header Type Required Description
Content-Type string Yes application/json

Request JSON Sample:

{
  "firebaseToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "preferredLanguage": "sw",
  "theme": "DARK",
  "deviceInfo": "Android 14, Samsung Galaxy S24"
}

Request Body Parameters:

Parameter Type Required Description Validation
firebaseToken string Yes Firebase ID token from client SDK Must be valid Firebase token
preferredLanguage string No User's language preference 2-5 chars (e.g., "en", "sw", "fr")
theme string No UI theme preference enum: LIGHT, DARK, SYSTEM
deviceInfo string No Device information for session tracking Max 255 chars

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Authentication successful",
  "action_time": "2025-01-05T10:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "email": "user@example.com",
      "username": "johndoe",
      "phoneNumber": null,
      "fullName": "John Doe",
      "profilePhotoUrl": "https://lh3.googleusercontent.com/...",
      "isPhoneVerified": false,
      "isEmailVerified": true,
      "preferredLanguage": "sw",
      "theme": "DARK",
      "authProvider": "GOOGLE",
      "role": "ROLE_USER",
      "createdAt": "2025-01-05T10:30:45"
    },
    "onboarding": {
      "isComplete": false,
      "currentStep": "PENDING_PHONE_VERIFICATION"
    }
  }
}

Success Response Fields:

Field Description
accessToken JWT token for API requests (expires in 1 hour)
refreshToken Token to get new accessToken (expires in 30 days)
tokenType Always "Bearer"
expiresIn Access token lifetime in seconds
user User profile information
user.theme User's theme preference: LIGHT, DARK, or SYSTEM
onboarding.isComplete false = must complete onboarding, true = can access app
onboarding.currentStep Current onboarding step (see Onboarding docs)

2. Refresh Access Token

Purpose: Get a new access token using refresh token when current one expires.

Endpoint: POST {base_url}/auth/refresh

Access Level: 🌐 Public

Authentication: None (refresh token in body)

Request JSON Sample:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Request Body Parameters:

Parameter Type Required Description Validation
refreshToken string Yes Refresh token from authentication Must be valid, non-expired

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Token refreshed successfully",
  "action_time": "2025-01-05T11:30:45",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": { ... },
    "onboarding": { ... }
  }
}

Error Responses:

Invalid Refresh Token (401):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Invalid refresh token",
  "action_time": "2025-01-05T11:30:45",
  "data": "Invalid refresh token"
}

Expired Refresh Token (401):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Refresh token expired",
  "action_time": "2025-01-05T11:30:45",
  "data": "Refresh token expired"
}

3. Logout

Purpose: Invalidate all refresh tokens for the user, ending all sessions.

Endpoint: POST {base_url}/auth/logout

Access Level: 🔒 Protected

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {accessToken}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Logged out successfully",
  "action_time": "2025-01-05T12:00:00",
  "data": null
}

4. Get Supported Languages

Purpose: Get list of supported languages for language selector screen.

Endpoint: GET {base_url}/languages

Access Level: 🌐 Public

Authentication: None

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Languages retrieved successfully",
  "action_time": "2025-01-05T10:00:00",
  "data": [
    {
      "code": "en",
      "name": "English",
      "nativeName": "English"
    },
    {
      "code": "sw",
      "name": "Swahili",
      "nativeName": "Kiswahili"
    },
    {
      "code": "fr",
      "name": "French",
      "nativeName": "Français"
    },
    {
      "code": "zh",
      "name": "Chinese",
      "nativeName": "中文"
    }
  ]
}

Frontend Implementation Guide

Step 1: First App Launch

Show language selector screen
├── Call GET /languages to get options
├── User selects language
├── Store locally: selectedLanguage, theme (default: SYSTEM)
└── Navigate to sign-in screen

Step 2: Sign In

Firebase Sign-In
├── Use Firebase SDK (Google/Apple/Email)
├── On success, get Firebase ID token
└── Call POST /auth/firebase/authenticate with:
    - firebaseToken
    - preferredLanguage (from step 1)
    - theme (from step 1)
    - deviceInfo (optional)

Step 3: Handle Response

Check response.data.onboarding.isComplete
├── false → Navigate to onboarding flow
│   └── Start at response.data.onboarding.currentStep
└── true → Navigate to home screen

Step 4: Store Tokens

Save securely:
├── accessToken → For Authorization header
├── refreshToken → For token renewal
└── user data → For UI display

Step 5: API Calls

All protected endpoints:
├── Add header: Authorization: Bearer {accessToken}
├── On 401 error → Try refresh token
│   ├── Success → Retry original request
│   └── Fail → Force re-login
└── Continue normal flow

Files Management

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-01-05
Version: v1.0

Base URL: https://api.fursahub.com/api/v1

Short Description: File upload and management endpoints for Fursa Hub. Handles image uploads for profiles, posts, events, and other content. Uses MinIO for storage with automatic BlurHash generation for images.

Hints:


File Storage Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                        FILE STORAGE ARCHITECTURE                         │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  UPLOAD FLOW:                                                           │
│  ┌────────────────────────────────────────────────────────────────┐     │
│  │ 1. Client uploads file to: POST /files/upload-single           │     │
│  │ 2. Backend:                                                     │     │
│  │    ├── Validates file (size, type)                             │     │
│  │    ├── Creates user bucket if needed (fursa-{userId})          │     │
│  │    ├── Generates unique filename (UUID)                        │     │
│  │    ├── Uploads to MinIO storage                                │     │
│  │    ├── If image: generates BlurHash + dimensions               │     │
│  │    └── Returns file metadata with permanentUrl                 │     │
│  └────────────────────────────────────────────────────────────────┘     │
│                                                                          │
│  STORAGE STRUCTURE:                                                      │
│  MinIO Server                                                            │
│  └── fursa-{userId}/                    ← User's bucket                 │
│      ├── profile/                       ← Profile photos                │
│      │   └── abc123-uuid.jpg                                            │
│      ├── social_post/                   ← Post media                    │
│      │   ├── def456-uuid.jpg                                            │
│      │   └── ghi789-uuid.mp4                                            │
│      ├── events/                        ← Event images                  │
│      ├── opportunities/                 ← Opportunity attachments       │
│      ├── calls/                         ← Call for proposals            │
│      ├── funds/                         ← Fund documents                │
│      ├── innovation/                    ← Innovation hub files          │
│      └── skill_center/                  ← Course materials              │
│                                                                          │
│  ACCESS:                                                                 │
│  └── Files accessible at: {filesServerUrl}/{bucketName}/{objectKey}    │
│  └── Example: https://files.fursahub.com/fursa-uuid123/profile/pic.jpg │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

File Directories

Directory Purpose Typical Use
PROFILE Profile photos User avatar, cover images
SOCIAL_POST Social media posts Post images, videos
CALLS Call for proposals Proposal documents
FUNDS Funding/grants Grant application files
EVENTS Events Event banners, tickets
OPPORTUNITIES Job/opportunity Job post attachments
INNOVATION Innovation hub Project files
SKILL_CENTER Skills/courses Course materials

Endpoints


1. Upload Single File

Purpose: Upload a single file to the specified directory.

Endpoint: POST {base_url}/files/upload-single

Access Level: 🔒 Protected

Authentication: Bearer Token

Content-Type: multipart/form-data

Request Parameters:

Parameter Type Required Description Validation
file file Yes File to upload Max 25MB
directory string Yes Target directory enum: PROFILE, SOCIAL_POST, CALLS, FUNDS, EVENTS, OPPORTUNITIES, INNOVATION, SKILL_CENTER

Example Request (using curl):

curl -X POST \
  -H "Authorization: Bearer {accessToken}" \
  -F "file=@/path/to/image.jpg" \
  -F "directory=PROFILE" \
  {base_url}/files/upload-single

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "File uploaded successfully",
  "action_time": "2025-01-05T10:30:45",
  "data": {
    "fileName": "550e8400-e29b-41d4-a716-446655440000.jpg",
    "originalFileName": "my-photo.jpg",
    "objectKey": "profile/550e8400-e29b-41d4-a716-446655440000.jpg",
    "directory": "PROFILE",
    "contentType": "image/jpeg",
    "fileSize": 245678,
    "fileSizeFormatted": "239.9 KB",
    "permanentUrl": "https://files.fursahub.com/fursa-userid/profile/550e8400.jpg",
    "thumbnailUrl": "https://files.fursahub.com/fursa-userid/profile/550e8400.jpg",
    "blurHash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
    "fileExtension": ".jpg",
    "fileType": "IMAGE",
    "isImage": true,
    "isVideo": false,
    "isDocument": false,
    "width": 1920,
    "height": 1080,
    "dimensions": "1920x1080",
    "checksum": "d41d8cd98f00b204e9800998ecf8427e",
    "uploadedAt": "2025-01-05T10:30:45",
    "uploadedBy": "550e8400-e29b-41d4-a716-446655440000",
    "isPublic": true
  }
}

Success Response Fields:

Field Description
fileName Generated unique filename
originalFileName Original uploaded filename
objectKey Full path in storage (directory/filename)
directory Storage directory
permanentUrl Public URL to access the file
thumbnailUrl Thumbnail URL (same as permanentUrl for images)
blurHash BlurHash string for image placeholders (null for non-images)
fileType IMAGE, VIDEO, DOCUMENT, or OTHER
width, height Image dimensions (null for non-images)
checksum MD5 checksum for integrity verification

Error Responses:

File Too Large (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "File size exceeds maximum limit of 25MB",
  "action_time": "2025-01-05T10:30:45",
  "data": "File size exceeds maximum limit of 25MB"
}

Empty File (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "File is empty",
  "action_time": "2025-01-05T10:30:45",
  "data": "File is empty"
}

2. Upload Multiple Files

Purpose: Upload multiple files at once to the same directory.

Endpoint: POST {base_url}/files/upload

Access Level: 🔒 Protected

Authentication: Bearer Token

Content-Type: multipart/form-data

Request Parameters:

Parameter Type Required Description Validation
files file[] Yes Array of files Max 25MB each
directory string Yes Target directory enum values

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Files uploaded successfully",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "uploadedFiles": [
      {
        "fileName": "uuid1.jpg",
        "originalFileName": "photo1.jpg",
        "permanentUrl": "https://files.fursahub.com/bucket/path/uuid1.jpg",
        "blurHash": "LKO2?U%2Tw=w]...",
        "isImage": true
      },
      {
        "fileName": "uuid2.jpg",
        "originalFileName": "photo2.jpg",
        "permanentUrl": "https://files.fursahub.com/bucket/path/uuid2.jpg",
        "blurHash": "LAB2?Q%1Tu=x]...",
        "isImage": true
      }
    ],
    "totalFiles": 2,
    "successfulUploads": 2,
    "failedUploads": 0,
    "totalSize": 512000,
    "totalSizeFormatted": "500.0 KB",
    "uploadedAt": "2025-01-05T10:35:00",
    "message": "2 files uploaded successfully",
    "errors": null
  }
}

Partial Success Response (some files failed):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Files uploaded successfully",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "uploadedFiles": [...],
    "totalFiles": 3,
    "successfulUploads": 2,
    "failedUploads": 1,
    "message": "2 files uploaded successfully, 1 failed",
    "errors": [
      "Failed to upload large-file.zip: File size exceeds maximum limit of 25MB"
    ]
  }
}

BlurHash Usage

BlurHash is a compact representation of an image placeholder. Use it to show a blurred preview while the actual image loads.

Example BlurHash: LKO2?U%2Tw=w]~RBVZRi};RPxuwH

Frontend Implementation:

// React example with blurhash library
import { Blurhash } from "react-blurhash";

function ImageWithPlaceholder({ imageUrl, blurHash, width, height }) {
  const [loaded, setLoaded] = useState(false);
  
  return (
    <div style={{ position: 'relative' }}>
      {!loaded && blurHash && (
        <Blurhash
          hash={blurHash}
          width={width}
          height={height}
          resolutionX={32}
          resolutionY={32}
        />
      )}
      <img 
        src={imageUrl}
        onLoad={() => setLoaded(true)}
        style={{ display: loaded ? 'block' : 'none' }}
      />
    </div>
  );
}

Frontend Implementation Guide

Profile Photo Upload

1. User selects photo
2. POST /files/upload-single
   ├── file: selected image
   └── directory: "PROFILE"
3. Get permanentUrl from response
4. POST /profile/photo with photoUrl
5. Display image with blurHash placeholder

Social Post with Images

1. User creates post, selects images
2. For each image: POST /files/upload-single
   ├── file: image
   └── directory: "SOCIAL_POST"
3. Collect all permanentUrls
4. Create post with image URLs array
5. Store blurHash for each image for feed display

File Upload Component

function uploadFile(file, directory) {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('directory', directory);
  
  return fetch('{base_url}/files/upload-single', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`
    },
    body: formData
  });
}

Validation Before Upload

const MAX_SIZE = 25 * 1024 * 1024; // 25MB
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];

function validateFile(file) {
  if (file.size > MAX_SIZE) {
    throw new Error('File too large (max 25MB)');
  }
  if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
    throw new Error('Invalid file type');
  }
  return true;
}

Profile

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-01-05
Version: v1.0

Base URL: https://api.fursahub.com/api/v1

Short Description: Profile management endpoints for Fursa Hub. Allows users to view and update their profile information, manage photos, change theme and language preferences. Profile completion is the final step of onboarding.

Hints:


Profile in Onboarding Context

┌─────────────────────────────────────────────────────────────────────────┐
│                     PROFILE & ONBOARDING RELATIONSHIP                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  Profile data comes from multiple sources:                               │
│                                                                          │
│  1. FIREBASE (auto-filled at registration)                              │
│     └── fullName (from Google/Apple account)                            │
│     └── profilePhotoUrl (from social account)                           │
│     └── email (from auth provider)                                      │
│                                                                          │
│  2. REGISTRATION (user choice)                                          │
│     └── preferredLanguage                                               │
│     └── theme                                                           │
│                                                                          │
│  3. ONBOARDING (user completes)                                         │
│     └── phoneNumber (verified via OTP)                                  │
│     └── preferences (from onboarding pages)                             │
│                                                                          │
│  4. PROFILE COMPLETION (final step)                                     │
│     └── username (required, unique)                                     │
│     └── bio (required)                                                  │
│     └── fullName (can edit Firebase default)                            │
│     └── gender, link (optional)                                         │
│                                                                          │
│  When user completes: fullName + username + bio                         │
│  └── Onboarding auto-completes if at PENDING_PROFILE_COMPLETION         │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Endpoints


1. Get Profile

Purpose: Retrieve current user's complete profile information.

Endpoint: GET {base_url}/profile

Access Level: 🔒 Protected

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {accessToken}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Profile retrieved",
  "action_time": "2025-01-05T10:30:45",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "username": "johndoe",
    "phoneNumber": "+255712345678",
    "fullName": "John Doe",
    "bio": "Software developer passionate about tech",
    "gender": "MALE",
    "link": "https://johndoe.com",
    "profilePhotoUrls": [
      "https://files.fursahub.com/bucket/profile/photo1.jpg",
      "https://files.fursahub.com/bucket/profile/photo2.jpg"
    ],
    "primaryPhotoUrl": "https://files.fursahub.com/bucket/profile/photo1.jpg",
    "isPhoneVerified": true,
    "isEmailVerified": true,
    "preferredLanguage": "sw",
    "theme": "DARK",
    "authProvider": "GOOGLE",
    "role": "ROLE_USER",
    "onboardingStatus": "COMPLETED",
    "isOnboardingComplete": true,
    "createdAt": "2025-01-01T08:00:00",
    "updatedAt": "2025-01-05T10:30:00"
  }
}

Success Response Fields:

Field Description
id Unique user identifier (UUID)
email User's email address
username Unique username (lowercase)
phoneNumber Verified phone number in E.164 format
fullName Display name
bio User biography/description
gender MALE or FEMALE
link Personal website or social link
profilePhotoUrls Array of all profile photo URLs
primaryPhotoUrl First photo URL (main profile picture)
isPhoneVerified Phone verification status
isEmailVerified Email verification status
preferredLanguage Language code (en, sw, fr, zh)
theme LIGHT, DARK, or SYSTEM
authProvider GOOGLE, APPLE, or EMAIL
role User role (ROLE_USER, ROLE_ADMIN, etc.)
onboardingStatus Current onboarding step
isOnboardingComplete true if onboarding finished

2. Update Profile

Purpose: Update user profile information. Only provided fields are updated.

Endpoint: PUT {base_url}/profile

Access Level: 🔒 Protected

Authentication: Bearer Token

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {accessToken}
Content-Type string Yes application/json

Request JSON Sample:

{
  "fullName": "John Doe Updated",
  "username": "johndoe_new",
  "bio": "Building the future of opportunity in East Africa",
  "gender": "MALE",
  "link": "https://linkedin.com/in/johndoe",
  "profilePhotoUrls": [
    "https://files.fursahub.com/bucket/profile/new-photo.jpg"
  ],
  "theme": "LIGHT",
  "preferredLanguage": "en"
}

Request Body Parameters:

Parameter Type Required Description Validation
fullName string No Display name Min: 2, Max: 100 chars
username string No Unique username Min: 3, Max: 30 chars, alphanumeric + underscore only
bio string No User biography Max: 500 chars
gender string No User gender enum: MALE, FEMALE
link string No Personal/social link Must be valid URL (https://...)
profilePhotoUrls array No List of photo URLs Array of strings
theme string No UI theme preference enum: LIGHT, DARK, SYSTEM
preferredLanguage string No Language preference Must be active language code

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Profile updated",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "username": "johndoe_new",
    "fullName": "John Doe Updated",
    "bio": "Building the future of opportunity in East Africa",
    "theme": "LIGHT",
    "preferredLanguage": "en",
    "onboardingStatus": "COMPLETED",
    "isOnboardingComplete": true
  }
}

Error Responses:

Username Already Taken (409):

{
  "success": false,
  "httpStatus": "CONFLICT",
  "message": "Username already taken",
  "action_time": "2025-01-05T10:35:00",
  "data": "Username already taken"
}

Invalid Language Code (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Invalid or inactive language code: xx",
  "action_time": "2025-01-05T10:35:00",
  "data": "Invalid or inactive language code: xx"
}

Validation Error (422):

{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "username": "Username can only contain letters, numbers, and underscores",
    "fullName": "Name must be 2-100 characters"
  }
}

3. Update Theme (Quick Toggle)

Purpose: Quick endpoint to change theme without full profile update.

Endpoint: PATCH {base_url}/profile/theme

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "theme": "DARK"
}

Request Body Parameters:

Parameter Type Required Description Validation
theme string Yes Theme preference enum: LIGHT, DARK, SYSTEM

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Theme updated",
  "action_time": "2025-01-05T10:40:00",
  "data": {
    "theme": "DARK"
  }
}

4. Check Username Availability

Purpose: Check if a username is available before updating profile.

Endpoint: GET {base_url}/profile/username/check

Access Level: 🔒 Protected

Authentication: Bearer Token

Query Parameters:

Parameter Type Required Description Validation
username string Yes Username to check Min: 3 chars

Success Response JSON Sample (Available):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username available",
  "action_time": "2025-01-05T10:45:00",
  "data": {
    "username": "newusername",
    "available": true
  }
}

Success Response JSON Sample (Taken):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username taken",
  "action_time": "2025-01-05T10:45:00",
  "data": {
    "username": "existinguser",
    "available": false
  }
}

5. Add Profile Photo

Purpose: Add a new photo to user's profile photos array.

Endpoint: POST {base_url}/profile/photo

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "photoUrl": "https://files.fursahub.com/bucket/profile/new-photo.jpg"
}

Request Body Parameters:

Parameter Type Required Description Validation
photoUrl string Yes URL of uploaded photo Must be valid URL

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Photo added",
  "action_time": "2025-01-05T10:50:00",
  "data": {
    "profilePhotoUrls": [
      "https://files.fursahub.com/bucket/profile/photo1.jpg",
      "https://files.fursahub.com/bucket/profile/new-photo.jpg"
    ],
    "primaryPhotoUrl": "https://files.fursahub.com/bucket/profile/photo1.jpg"
  }
}

6. Remove Profile Photo

Purpose: Remove a photo from user's profile photos array.

Endpoint: DELETE {base_url}/profile/photo

Access Level: 🔒 Protected

Authentication: Bearer Token

Query Parameters:

Parameter Type Required Description
photoUrl string Yes URL of photo to remove

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Photo removed",
  "action_time": "2025-01-05T10:55:00",
  "data": {
    "profilePhotoUrls": [
      "https://files.fursahub.com/bucket/profile/photo1.jpg"
    ],
    "primaryPhotoUrl": "https://files.fursahub.com/bucket/profile/photo1.jpg"
  }
}

Frontend Implementation Guide

Profile Completion Screen

When onboardingStatus = "PENDING_PROFILE_COMPLETION":

1. GET /profile to fetch current data
2. Show form with:
   ├── fullName (pre-filled from Firebase)
   ├── username (auto-generated, editable)
   │   └── On change: GET /profile/username/check
   ├── bio (required)
   ├── gender (optional)
   └── link (optional)
3. PUT /profile with form data
4. If isOnboardingComplete = true → Navigate to home

Settings Screen

Theme Toggle:
└── PATCH /profile/theme with selected theme
└── Apply theme immediately on client

Language Change:
└── PUT /profile with preferredLanguage
└── Reload UI with new language strings

Profile Edit:
└── PUT /profile with changed fields only

Photo Management

Adding Photo:
1. Upload via Files API (POST /files/upload-single)
2. Get permanentUrl from response
3. POST /profile/photo with photoUrl

Removing Photo:
1. DELETE /profile/photo?photoUrl=...
2. Optionally delete from Files API

Onboarding EndUser

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-01-05
Version: v1.0

Base URL: https://api.fursahub.com/api/v1

Short Description: Onboarding flow endpoints for Fursa Hub. Guides new users through phone verification, preference selection, and profile completion. All content is translated based on user's preferredLanguage setting.

Hints:


Complete Onboarding Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                        COMPLETE ONBOARDING FLOW                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  After POST /auth/firebase/authenticate:                                │
│  └── Check response.data.onboarding.currentStep                         │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 1: PENDING_EMAIL_VERIFICATION (Optional/Skippable)         │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • Firebase handles actual email verification                     │    │
│  │ • Check: GET /onboarding/email-verification/status              │    │
│  │ • Skip: POST /onboarding/email-verification/skip                │    │
│  │ • Or wait for user to verify email in Firebase                  │    │
│  │ • Auto-transitions when Firebase reports email verified          │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 2: PENDING_PHONE_VERIFICATION (Required)                   │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • POST /onboarding/auth-phone/request-otp                       │    │
│  │   └── User enters phone number (+255...)                        │    │
│  │   └── Backend sends OTP via SMS                                 │    │
│  │   └── Returns token for verification                            │    │
│  │ • POST /onboarding/auth-phone/verify                            │    │
│  │   └── User enters 6-digit OTP                                   │    │
│  │   └── On success: phone saved, transitions to next step         │    │
│  │ • POST /onboarding/auth-phone/resend-otp (if needed)            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 3: PENDING_PREFERENCES (Required)                          │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • GET /onboarding/pages?current=true                            │    │
│  │   └── Get current preference page                               │    │
│  │ • Loop through pages:                                           │    │
│  │   └── Display options (translated to user's language)           │    │
│  │   └── User selects options                                      │    │
│  │   └── POST /onboarding/pages/{pageId}/response                  │    │
│  │   └── Or POST /onboarding/pages/{pageId}/skip (if skippable)    │    │
│  │   └── Move to next page until all complete                      │    │
│  │ • Auto-transitions when all pages completed                     │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 4: PENDING_PROFILE_COMPLETION (Required)                   │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • GET /profile                                                  │    │
│  │   └── Get current profile (some data from Firebase)             │    │
│  │ • PUT /profile                                                  │    │
│  │   └── fullName (required)                                       │    │
│  │   └── username (required, unique)                               │    │
│  │   └── bio (required)                                            │    │
│  │   └── gender, link (optional)                                   │    │
│  │ • On save: auto-completes onboarding                            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 5: COMPLETED ✓                                             │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • Navigate to home screen                                       │    │
│  │ • User can now access all app features                          │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Phone Verification Endpoints


1. Request OTP

Purpose: Send OTP code to user's phone number for verification.

Endpoint: POST {base_url}/onboarding/auth-phone/request-otp

Access Level: 🔒 Protected

Authentication: Bearer Token

Prerequisite: User must be at PENDING_PHONE_VERIFICATION step

Request JSON Sample:

{
  "phoneNumber": "+255712345678"
}

Request Body Parameters:

Parameter Type Required Description Validation
phoneNumber string Yes Phone number with country code E.164 format: +255XXXXXXXXX

Supported Country Codes:

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent successfully",
  "action_time": "2025-01-05T10:30:45",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "phoneNumber": "+255****678",
    "expiresInSeconds": 600,
    "resendAvailableIn": 120
  }
}

Success Response Fields:

Field Description
token Temporary token for verify/resend endpoints
phoneNumber Masked phone number for display
expiresInSeconds OTP validity period (10 minutes)
resendAvailableIn Seconds until resend allowed (2 minutes)

Error Responses:

Wrong Onboarding Step (412):

{
  "success": false,
  "httpStatus": "PRECONDITION_FAILED",
  "message": "Onboarding step required",
  "action_time": "2025-01-05T10:30:45",
  "data": {
    "message": "Complete email verification first",
    "currentStep": "PENDING_EMAIL_VERIFICATION",
    "requiredStep": "PENDING_EMAIL_VERIFICATION"
  }
}

Rate Limit Exceeded (429):

{
  "success": false,
  "httpStatus": "TOO_MANY_REQUESTS",
  "message": "Too many OTP requests. Try again in 10 minutes.",
  "action_time": "2025-01-05T10:30:45",
  "data": "Too many OTP requests. Try again in 10 minutes."
}

Phone Already Registered (409):

{
  "success": false,
  "httpStatus": "CONFLICT",
  "message": "Phone number already registered",
  "action_time": "2025-01-05T10:30:45",
  "data": "Phone number already registered"
}

2. Verify OTP

Purpose: Verify OTP code and complete phone verification.

Endpoint: POST {base_url}/onboarding/auth-phone/verify

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "otp": "123456"
}

Request Body Parameters:

Parameter Type Required Description Validation
token string Yes Token from request-otp response Must be valid, non-expired
otp string Yes 6-digit OTP code Exactly 6 digits

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Phone verified successfully",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "verified": true,
    "phoneNumber": "+255****678",
    "onboardingStatus": "PENDING_PREFERENCES",
    "nextStep": "/api/v1/onboarding/pages"
  }
}

Error Responses:

Invalid OTP (403):

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Invalid OTP. 2 attempt(s) remaining.",
  "action_time": "2025-01-05T10:35:00",
  "data": "Invalid OTP. 2 attempt(s) remaining."
}

Expired OTP (403):

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "OTP has expired. Please request a new one.",
  "action_time": "2025-01-05T10:35:00",
  "data": "OTP has expired. Please request a new one."
}

Max Attempts Reached (403):

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Maximum attempts reached. Please request a new OTP.",
  "action_time": "2025-01-05T10:35:00",
  "data": "Maximum attempts reached. Please request a new OTP."
}

3. Resend OTP

Purpose: Request a new OTP code using the existing token.

Endpoint: POST {base_url}/onboarding/auth-phone/resend-otp

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success Response: Same as Request OTP endpoint


Email Verification Endpoints


4. Get Email Verification Status

Purpose: Check email verification status and options.

Endpoint: GET {base_url}/onboarding/email-verification/status

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email verification status",
  "action_time": "2025-01-05T10:00:00",
  "data": {
    "verified": false,
    "email": "jo***@example.com",
    "required": false,
    "canSkip": true,
    "currentStep": "PENDING_EMAIL_VERIFICATION"
  }
}

5. Skip Email Verification

Purpose: Skip email verification step (if allowed by config).

Endpoint: POST {base_url}/onboarding/email-verification/skip

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email verification skipped",
  "action_time": "2025-01-05T10:05:00",
  "data": {
    "verified": false,
    "skipped": true,
    "nextStep": "PENDING_PHONE_VERIFICATION"
  }
}

Preference Pages Endpoints


6. Get Onboarding Progress

Purpose: Get overall onboarding progress with all steps.

Endpoint: GET {base_url}/onboarding/progress

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Progress retrieved",
  "action_time": "2025-01-05T10:40:00",
  "data": {
    "percentage": 45.0,
    "currentStage": "PENDING_PREFERENCES",
    "currentStageLabel": "Complete your preferences",
    "steps": [
      {
        "key": "registration",
        "label": "Registration",
        "completed": true,
        "weight": 15.0,
        "skippable": false
      },
      {
        "key": "phone_verification",
        "label": "Phone Verification",
        "completed": true,
        "weight": 15.0,
        "skippable": false
      },
      {
        "key": "page_interests",
        "label": "Your Interests",
        "completed": true,
        "weight": 13.33,
        "skippable": false
      },
      {
        "key": "page_goals",
        "label": "Your Goals",
        "completed": false,
        "weight": 13.33,
        "skippable": true
      },
      {
        "key": "page_experience",
        "label": "Your Experience",
        "completed": false,
        "weight": 13.33,
        "skippable": false
      },
      {
        "key": "profile_completion",
        "label": "Complete Profile",
        "completed": false,
        "weight": 15.0,
        "skippable": false
      }
    ],
    "nextStep": {
      "key": "page_goals",
      "label": "Your Goals",
      "endpoint": "/api/v1/onboarding/pages?page=2",
      "skippable": true
    }
  }
}

7. Get All Pages

Purpose: Get all preference pages with completion status.

Endpoint: GET {base_url}/onboarding/pages

Access Level: 🔒 Protected

Authentication: Bearer Token

Query Parameters:

Parameter Type Required Description
all boolean No Return all pages (default behavior)
page integer No Get specific page by order (1, 2, 3...)
category string No Get page by category key
current boolean No Get first incomplete page

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "All pages retrieved",
  "action_time": "2025-01-05T10:45:00",
  "data": {
    "totalPages": 3,
    "completedPages": 1,
    "isOnboardingComplete": false,
    "pages": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440001",
        "pageOrder": 1,
        "categoryKey": "interests",
        "title": "Maslahi Yako",
        "description": "Chagua mambo yanayokuvutia",
        "bannerImages": ["https://cdn.fursahub.com/onboarding/interests.jpg"],
        "isSkippable": false,
        "minSelections": 1,
        "maxSelections": 5,
        "options": [
          { "key": "jobs", "label": "Kazi", "icon": "briefcase" },
          { "key": "funding", "label": "Ufadhili", "icon": "dollar" },
          { "key": "events", "label": "Matukio", "icon": "calendar" },
          { "key": "skills", "label": "Ujuzi", "icon": "book" },
          { "key": "networking", "label": "Mitandao", "icon": "users" }
        ],
        "isCompleted": true
      },
      {
        "id": "550e8400-e29b-41d4-a716-446655440002",
        "pageOrder": 2,
        "categoryKey": "goals",
        "title": "Malengo Yako",
        "description": "Unataka kufikia nini?",
        "isSkippable": true,
        "minSelections": 1,
        "maxSelections": 3,
        "options": [
          { "key": "find_job", "label": "Kupata kazi", "icon": "search" },
          { "key": "start_business", "label": "Kuanzisha biashara", "icon": "store" },
          { "key": "learn_skills", "label": "Kujifunza ujuzi", "icon": "graduation" },
          { "key": "get_funding", "label": "Kupata ufadhili", "icon": "money" }
        ],
        "isCompleted": false
      }
    ]
  }
}

8. Get Current Page

Purpose: Get the first incomplete preference page.

Endpoint: GET {base_url}/onboarding/pages?current=true

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Current page retrieved",
  "action_time": "2025-01-05T10:50:00",
  "data": {
    "page": {
      "id": "550e8400-e29b-41d4-a716-446655440002",
      "pageOrder": 2,
      "categoryKey": "goals",
      "title": "Malengo Yako",
      "description": "Unataka kufikia nini?",
      "isSkippable": true,
      "minSelections": 1,
      "maxSelections": 3,
      "options": [...]
    },
    "progress": {
      "current": 2,
      "total": 3,
      "nextPage": 3,
      "isLast": false,
      "isCompleted": false
    }
  }
}

9. Submit Page Response

Purpose: Save user's selections for a preference page.

Endpoint: POST {base_url}/onboarding/pages/{pageId}/response

Access Level: 🔒 Protected

Authentication: Bearer Token

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

Request JSON Sample:

{
  "selectedOptions": ["find_job", "learn_skills"]
}

Request Body Parameters:

Parameter Type Required Description Validation
selectedOptions array Yes Array of selected option keys Must match page's min/max selections

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Response saved",
  "action_time": "2025-01-05T10:55:00",
  "data": {
    "saved": true,
    "progress": {
      "current": 2,
      "total": 3,
      "nextPage": 3,
      "isLast": false,
      "isCompleted": false
    }
  }
}

Error Responses:

Too Few Selections (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Minimum 1 selection(s) required",
  "action_time": "2025-01-05T10:55:00",
  "data": "Minimum 1 selection(s) required"
}

Invalid Option (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Invalid option: unknown_key",
  "action_time": "2025-01-05T10:55:00",
  "data": "Invalid option: unknown_key"
}

10. Skip Page

Purpose: Skip a preference page (only if page is skippable).

Endpoint: POST {base_url}/onboarding/pages/{pageId}/skip

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Page skipped",
  "action_time": "2025-01-05T11:00:00",
  "data": {
    "saved": true,
    "progress": {
      "current": 2,
      "total": 3,
      "nextPage": 3,
      "isLast": false,
      "isCompleted": false
    }
  }
}

Error Response (Page Not Skippable):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This page cannot be skipped",
  "action_time": "2025-01-05T11:00:00",
  "data": "This page cannot be skipped"
}

Language Preference Endpoint


11. Set Language Preference

Purpose: Update user's language preference (can be called anytime during onboarding).

Endpoint: POST {base_url}/onboarding/language-preference

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "code": "sw"
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Language preference updated",
  "action_time": "2025-01-05T11:05:00",
  "data": {
    "code": "sw",
    "name": "Swahili",
    "nativeName": "Kiswahili"
  }
}

Frontend Implementation Guide

Phone Verification Screen

1. Show phone input with country code selector
2. On submit: POST /onboarding/auth-phone/request-otp
3. Save token from response
4. Navigate to OTP input screen
5. Show countdown for resendAvailableIn
6. On OTP submit: POST /onboarding/auth-phone/verify
7. On success: Navigate based on nextStep

Preference Pages Loop

1. GET /onboarding/pages?current=true
2. If page is null → All done, navigate to profile
3. Display page with:
   ├── Title, description (translated)
   ├── Banner image
   ├── Options as selectable chips/cards
   └── Skip button (if isSkippable)
4. On submit: POST /onboarding/pages/{pageId}/response
5. Check progress.isCompleted
   ├── true → Navigate to profile completion
   └── false → GET /onboarding/pages?current=true (loop)

Progress Indicator

GET /onboarding/progress
├── Use percentage for progress bar
├── Show steps as dots/icons
├── Highlight current step
└── Show completed steps with checkmarks

Onboarding Analytics Admin

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-01-05
Version: v1.0

Base URL: https://api.fursahub.com/api/v1

Short Description: Onboarding flow endpoints for Fursa Hub. Guides new users through phone verification, preference selection, and profile completion. All content is translated based on user's preferredLanguage setting.

Hints:


Complete Onboarding Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                        COMPLETE ONBOARDING FLOW                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  After POST /auth/firebase/authenticate:                                │
│  └── Check response.data.onboarding.currentStep                         │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 1: PENDING_EMAIL_VERIFICATION (Optional/Skippable)         │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • Firebase handles actual email verification                     │    │
│  │ • Check: GET /onboarding/email-verification/status              │    │
│  │ • Skip: POST /onboarding/email-verification/skip                │    │
│  │ • Or wait for user to verify email in Firebase                  │    │
│  │ • Auto-transitions when Firebase reports email verified          │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 2: PENDING_PHONE_VERIFICATION (Required)                   │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • POST /onboarding/auth-phone/request-otp                       │    │
│  │   └── User enters phone number (+255...)                        │    │
│  │   └── Backend sends OTP via SMS                                 │    │
│  │   └── Returns token for verification                            │    │
│  │ • POST /onboarding/auth-phone/verify                            │    │
│  │   └── User enters 6-digit OTP                                   │    │
│  │   └── On success: phone saved, transitions to next step         │    │
│  │ • POST /onboarding/auth-phone/resend-otp (if needed)            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 3: PENDING_PREFERENCES (Required)                          │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • GET /onboarding/pages?current=true                            │    │
│  │   └── Get current preference page                               │    │
│  │ • Loop through pages:                                           │    │
│  │   └── Display options (translated to user's language)           │    │
│  │   └── User selects options                                      │    │
│  │   └── POST /onboarding/pages/{pageId}/response                  │    │
│  │   └── Or POST /onboarding/pages/{pageId}/skip (if skippable)    │    │
│  │   └── Move to next page until all complete                      │    │
│  │ • Auto-transitions when all pages completed                     │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 4: PENDING_PROFILE_COMPLETION (Required)                   │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • GET /profile                                                  │    │
│  │   └── Get current profile (some data from Firebase)             │    │
│  │ • PUT /profile                                                  │    │
│  │   └── fullName (required)                                       │    │
│  │   └── username (required, unique)                               │    │
│  │   └── bio (required)                                            │    │
│  │   └── gender, link (optional)                                   │    │
│  │ • On save: auto-completes onboarding                            │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                              ↓                                           │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │ STEP 5: COMPLETED ✓                                             │    │
│  ├─────────────────────────────────────────────────────────────────┤    │
│  │ • Navigate to home screen                                       │    │
│  │ • User can now access all app features                          │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Phone Verification Endpoints


1. Request OTP

Purpose: Send OTP code to user's phone number for verification.

Endpoint: POST {base_url}/onboarding/auth-phone/request-otp

Access Level: 🔒 Protected

Authentication: Bearer Token

Prerequisite: User must be at PENDING_PHONE_VERIFICATION step

Request JSON Sample:

{
  "phoneNumber": "+255712345678"
}

Request Body Parameters:

Parameter Type Required Description Validation
phoneNumber string Yes Phone number with country code E.164 format: +255XXXXXXXXX

Supported Country Codes:

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent successfully",
  "action_time": "2025-01-05T10:30:45",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "phoneNumber": "+255****678",
    "expiresInSeconds": 600,
    "resendAvailableIn": 120
  }
}

Success Response Fields:

Field Description
token Temporary token for verify/resend endpoints
phoneNumber Masked phone number for display
expiresInSeconds OTP validity period (10 minutes)
resendAvailableIn Seconds until resend allowed (2 minutes)

Error Responses:

Wrong Onboarding Step (412):

{
  "success": false,
  "httpStatus": "PRECONDITION_FAILED",
  "message": "Onboarding step required",
  "action_time": "2025-01-05T10:30:45",
  "data": {
    "message": "Complete email verification first",
    "currentStep": "PENDING_EMAIL_VERIFICATION",
    "requiredStep": "PENDING_EMAIL_VERIFICATION"
  }
}

Rate Limit Exceeded (429):

{
  "success": false,
  "httpStatus": "TOO_MANY_REQUESTS",
  "message": "Too many OTP requests. Try again in 10 minutes.",
  "action_time": "2025-01-05T10:30:45",
  "data": "Too many OTP requests. Try again in 10 minutes."
}

Phone Already Registered (409):

{
  "success": false,
  "httpStatus": "CONFLICT",
  "message": "Phone number already registered",
  "action_time": "2025-01-05T10:30:45",
  "data": "Phone number already registered"
}

2. Verify OTP

Purpose: Verify OTP code and complete phone verification.

Endpoint: POST {base_url}/onboarding/auth-phone/verify

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "otp": "123456"
}

Request Body Parameters:

Parameter Type Required Description Validation
token string Yes Token from request-otp response Must be valid, non-expired
otp string Yes 6-digit OTP code Exactly 6 digits

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Phone verified successfully",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "verified": true,
    "phoneNumber": "+255****678",
    "onboardingStatus": "PENDING_PREFERENCES",
    "nextStep": "/api/v1/onboarding/pages"
  }
}

Error Responses:

Invalid OTP (403):

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Invalid OTP. 2 attempt(s) remaining.",
  "action_time": "2025-01-05T10:35:00",
  "data": "Invalid OTP. 2 attempt(s) remaining."
}

Expired OTP (403):

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "OTP has expired. Please request a new one.",
  "action_time": "2025-01-05T10:35:00",
  "data": "OTP has expired. Please request a new one."
}

Max Attempts Reached (403):

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Maximum attempts reached. Please request a new OTP.",
  "action_time": "2025-01-05T10:35:00",
  "data": "Maximum attempts reached. Please request a new OTP."
}

3. Resend OTP

Purpose: Request a new OTP code using the existing token.

Endpoint: POST {base_url}/onboarding/auth-phone/resend-otp

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Success Response: Same as Request OTP endpoint


Email Verification Endpoints


4. Get Email Verification Status

Purpose: Check email verification status and options.

Endpoint: GET {base_url}/onboarding/email-verification/status

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email verification status",
  "action_time": "2025-01-05T10:00:00",
  "data": {
    "verified": false,
    "email": "jo***@example.com",
    "required": false,
    "canSkip": true,
    "currentStep": "PENDING_EMAIL_VERIFICATION"
  }
}

5. Skip Email Verification

Purpose: Skip email verification step (if allowed by config).

Endpoint: POST {base_url}/onboarding/email-verification/skip

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email verification skipped",
  "action_time": "2025-01-05T10:05:00",
  "data": {
    "verified": false,
    "skipped": true,
    "nextStep": "PENDING_PHONE_VERIFICATION"
  }
}

Preference Pages Endpoints


6. Get Onboarding Progress

Purpose: Get overall onboarding progress with all steps.

Endpoint: GET {base_url}/onboarding/progress

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Progress retrieved",
  "action_time": "2025-01-05T10:40:00",
  "data": {
    "percentage": 45.0,
    "currentStage": "PENDING_PREFERENCES",
    "currentStageLabel": "Complete your preferences",
    "steps": [
      {
        "key": "registration",
        "label": "Registration",
        "completed": true,
        "weight": 15.0,
        "skippable": false
      },
      {
        "key": "phone_verification",
        "label": "Phone Verification",
        "completed": true,
        "weight": 15.0,
        "skippable": false
      },
      {
        "key": "page_interests",
        "label": "Your Interests",
        "completed": true,
        "weight": 13.33,
        "skippable": false
      },
      {
        "key": "page_goals",
        "label": "Your Goals",
        "completed": false,
        "weight": 13.33,
        "skippable": true
      },
      {
        "key": "page_experience",
        "label": "Your Experience",
        "completed": false,
        "weight": 13.33,
        "skippable": false
      },
      {
        "key": "profile_completion",
        "label": "Complete Profile",
        "completed": false,
        "weight": 15.0,
        "skippable": false
      }
    ],
    "nextStep": {
      "key": "page_goals",
      "label": "Your Goals",
      "endpoint": "/api/v1/onboarding/pages?page=2",
      "skippable": true
    }
  }
}

7. Get All Pages

Purpose: Get all preference pages with completion status.

Endpoint: GET {base_url}/onboarding/pages

Access Level: 🔒 Protected

Authentication: Bearer Token

Query Parameters:

Parameter Type Required Description
all boolean No Return all pages (default behavior)
page integer No Get specific page by order (1, 2, 3...)
category string No Get page by category key
current boolean No Get first incomplete page

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "All pages retrieved",
  "action_time": "2025-01-05T10:45:00",
  "data": {
    "totalPages": 3,
    "completedPages": 1,
    "isOnboardingComplete": false,
    "pages": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440001",
        "pageOrder": 1,
        "categoryKey": "interests",
        "title": "Maslahi Yako",
        "description": "Chagua mambo yanayokuvutia",
        "bannerImages": ["https://cdn.fursahub.com/onboarding/interests.jpg"],
        "isSkippable": false,
        "minSelections": 1,
        "maxSelections": 5,
        "options": [
          { "key": "jobs", "label": "Kazi", "icon": "briefcase" },
          { "key": "funding", "label": "Ufadhili", "icon": "dollar" },
          { "key": "events", "label": "Matukio", "icon": "calendar" },
          { "key": "skills", "label": "Ujuzi", "icon": "book" },
          { "key": "networking", "label": "Mitandao", "icon": "users" }
        ],
        "isCompleted": true
      },
      {
        "id": "550e8400-e29b-41d4-a716-446655440002",
        "pageOrder": 2,
        "categoryKey": "goals",
        "title": "Malengo Yako",
        "description": "Unataka kufikia nini?",
        "isSkippable": true,
        "minSelections": 1,
        "maxSelections": 3,
        "options": [
          { "key": "find_job", "label": "Kupata kazi", "icon": "search" },
          { "key": "start_business", "label": "Kuanzisha biashara", "icon": "store" },
          { "key": "learn_skills", "label": "Kujifunza ujuzi", "icon": "graduation" },
          { "key": "get_funding", "label": "Kupata ufadhili", "icon": "money" }
        ],
        "isCompleted": false
      }
    ]
  }
}

8. Get Current Page

Purpose: Get the first incomplete preference page.

Endpoint: GET {base_url}/onboarding/pages?current=true

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Current page retrieved",
  "action_time": "2025-01-05T10:50:00",
  "data": {
    "page": {
      "id": "550e8400-e29b-41d4-a716-446655440002",
      "pageOrder": 2,
      "categoryKey": "goals",
      "title": "Malengo Yako",
      "description": "Unataka kufikia nini?",
      "isSkippable": true,
      "minSelections": 1,
      "maxSelections": 3,
      "options": [...]
    },
    "progress": {
      "current": 2,
      "total": 3,
      "nextPage": 3,
      "isLast": false,
      "isCompleted": false
    }
  }
}

9. Submit Page Response

Purpose: Save user's selections for a preference page.

Endpoint: POST {base_url}/onboarding/pages/{pageId}/response

Access Level: 🔒 Protected

Authentication: Bearer Token

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

Request JSON Sample:

{
  "selectedOptions": ["find_job", "learn_skills"]
}

Request Body Parameters:

Parameter Type Required Description Validation
selectedOptions array Yes Array of selected option keys Must match page's min/max selections

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Response saved",
  "action_time": "2025-01-05T10:55:00",
  "data": {
    "saved": true,
    "progress": {
      "current": 2,
      "total": 3,
      "nextPage": 3,
      "isLast": false,
      "isCompleted": false
    }
  }
}

Error Responses:

Too Few Selections (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Minimum 1 selection(s) required",
  "action_time": "2025-01-05T10:55:00",
  "data": "Minimum 1 selection(s) required"
}

Invalid Option (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Invalid option: unknown_key",
  "action_time": "2025-01-05T10:55:00",
  "data": "Invalid option: unknown_key"
}

10. Skip Page

Purpose: Skip a preference page (only if page is skippable).

Endpoint: POST {base_url}/onboarding/pages/{pageId}/skip

Access Level: 🔒 Protected

Authentication: Bearer Token

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Page skipped",
  "action_time": "2025-01-05T11:00:00",
  "data": {
    "saved": true,
    "progress": {
      "current": 2,
      "total": 3,
      "nextPage": 3,
      "isLast": false,
      "isCompleted": false
    }
  }
}

Error Response (Page Not Skippable):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This page cannot be skipped",
  "action_time": "2025-01-05T11:00:00",
  "data": "This page cannot be skipped"
}

Language Preference Endpoint


11. Set Language Preference

Purpose: Update user's language preference (can be called anytime during onboarding).

Endpoint: POST {base_url}/onboarding/language-preference

Access Level: 🔒 Protected

Authentication: Bearer Token

Request JSON Sample:

{
  "code": "sw"
}

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Language preference updated",
  "action_time": "2025-01-05T11:05:00",
  "data": {
    "code": "sw",
    "name": "Swahili",
    "nativeName": "Kiswahili"
  }
}

Admin: Manage Onboarding Pages

These endpoints allow admins to create, edit, reorder, and manage onboarding preference pages without code changes.


12. Get All Pages (Admin)

Purpose: Get all onboarding pages with full details for admin management.

Endpoint: GET {base_url}/onboarding/pages/manage

Access Level: 🔒 Protected (Admin/Moderator)

Authentication: Bearer Token (ROLE_ADMIN, ROLE_SUPER_ADMIN, ROLE_MODERATOR)

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Pages retrieved",
  "action_time": "2025-01-05T12:00:00",
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "categoryKey": "interests",
      "pageOrder": 1,
      "isActive": true,
      "isSkippable": false,
      "minSelections": 1,
      "maxSelections": 5,
      "bannerImages": ["https://cdn.fursahub.com/onboarding/interests.jpg"],
      "translations": {
        "en": {
          "title": "Your Interests",
          "description": "Select what interests you"
        },
        "sw": {
          "title": "Maslahi Yako",
          "description": "Chagua mambo yanayokuvutia"
        }
      },
      "options": [
        {
          "key": "jobs",
          "icon": "briefcase",
          "translations": {
            "en": "Jobs",
            "sw": "Kazi"
          }
        },
        {
          "key": "funding",
          "icon": "dollar",
          "translations": {
            "en": "Funding",
            "sw": "Ufadhili"
          }
        }
      ],
      "createdAt": "2025-01-01T00:00:00",
      "updatedAt": "2025-01-05T10:00:00"
    }
  ]
}

13. Get Page by ID (Admin)

Purpose: Get single page details for editing.

Endpoint: GET {base_url}/onboarding/pages/manage/{pageId}

Access Level: 🔒 Protected (Admin/Moderator)

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

Success Response: Same structure as single item in Get All Pages


14. Create Page

Purpose: Create a new onboarding preference page.

Endpoint: POST {base_url}/onboarding/pages/manage

Access Level: 🔒 Protected (Admin/Moderator)

Authentication: Bearer Token

Request JSON Sample:

{
  "categoryKey": "location",
  "pageOrder": 4,
  "isActive": true,
  "isSkippable": true,
  "minSelections": 1,
  "maxSelections": 1,
  "bannerImages": ["https://cdn.fursahub.com/onboarding/location.jpg"],
  "translations": {
    "en": {
      "title": "Your Location",
      "description": "Where are you based?"
    },
    "sw": {
      "title": "Mahali Ulipo",
      "description": "Unaishi wapi?"
    }
  },
  "options": [
    {
      "key": "dar_es_salaam",
      "icon": "map-pin",
      "translations": {
        "en": "Dar es Salaam",
        "sw": "Dar es Salaam"
      }
    },
    {
      "key": "arusha",
      "icon": "map-pin",
      "translations": {
        "en": "Arusha",
        "sw": "Arusha"
      }
    },
    {
      "key": "mwanza",
      "icon": "map-pin",
      "translations": {
        "en": "Mwanza",
        "sw": "Mwanza"
      }
    },
    {
      "key": "other",
      "icon": "map",
      "translations": {
        "en": "Other",
        "sw": "Nyingine"
      }
    }
  ]
}

Request Body Parameters:

Parameter Type Required Description Validation
categoryKey string Yes Unique category identifier Lowercase, no spaces
pageOrder integer Yes Display order (1, 2, 3...) Min: 1
isActive boolean No Page is active Default: true
isSkippable boolean No User can skip this page Default: false
minSelections integer No Minimum options to select Default: 1
maxSelections integer No Maximum options to select Default: 10
bannerImages array No Banner image URLs Array of URLs
translations object Yes Title/description per language Must include "en"
translations.{lang}.title string Yes Page title Max 100 chars
translations.{lang}.description string No Page description Max 500 chars
options array Yes Selectable options Min 2 options
options[].key string Yes Unique option key Lowercase, no spaces
options[].icon string No Icon name From icon library
options[].translations object Yes Label per language Must include "en"

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Page created",
  "action_time": "2025-01-05T12:05:00",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440004",
    "categoryKey": "location",
    "pageOrder": 4,
    "isActive": true,
    ...
  }
}

Error Responses:

Duplicate Category Key (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Category key already exists: interests",
  "action_time": "2025-01-05T12:05:00",
  "data": "Category key already exists: interests"
}

15. Update Page

Purpose: Update an existing onboarding page.

Endpoint: PUT {base_url}/onboarding/pages/manage/{pageId}

Access Level: 🔒 Protected (Admin/Moderator)

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

Request JSON Sample: Same as Create Page

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Page updated",
  "action_time": "2025-01-05T12:10:00",
  "data": { ... }
}

16. Delete Page

Purpose: Delete an onboarding page (soft delete recommended - use deactivate instead).

Endpoint: DELETE {base_url}/onboarding/pages/manage/{pageId}

Access Level: 🔒 Protected (Admin/Moderator)

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Page deleted",
  "action_time": "2025-01-05T12:15:00",
  "data": null
}

17. Activate Page

Purpose: Activate a deactivated page.

Endpoint: PATCH {base_url}/onboarding/pages/manage/{pageId}/activate

Access Level: 🔒 Protected (Admin/Moderator)

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Page activated",
  "action_time": "2025-01-05T12:20:00",
  "data": null
}

18. Deactivate Page

Purpose: Deactivate a page (hides from users without deleting).

Endpoint: PATCH {base_url}/onboarding/pages/manage/{pageId}/deactivate

Access Level: 🔒 Protected (Admin/Moderator)

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Page deactivated",
  "action_time": "2025-01-05T12:25:00",
  "data": null
}

19. Reorder Pages

Purpose: Change the display order of all pages at once.

Endpoint: PATCH {base_url}/onboarding/pages/manage/reorder

Access Level: 🔒 Protected (Admin/Moderator)

Request JSON Sample:

{
  "pageIds": [
    "550e8400-e29b-41d4-a716-446655440002",
    "550e8400-e29b-41d4-a716-446655440001",
    "550e8400-e29b-41d4-a716-446655440003",
    "550e8400-e29b-41d4-a716-446655440004"
  ]
}

Request Body Parameters:

Parameter Type Required Description Validation
pageIds array Yes Page IDs in new order Must include all active page IDs

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Pages reordered",
  "action_time": "2025-01-05T12:30:00",
  "data": null
}

Admin Panel Implementation Guide

Page List View

GET /onboarding/pages/manage
├── Display table with: Order, Category, Title (en), Status, Actions
├── Actions: Edit, Activate/Deactivate, Delete
├── Drag-and-drop for reorder → PATCH /onboarding/pages/manage/reorder
└── "Add New Page" button

Create/Edit Page Form

Fields:
├── Category Key (text, required, unique)
├── Page Order (number)
├── Is Skippable (checkbox)
├── Min/Max Selections (numbers)
├── Banner Images (file upload → Files API)
├── Translations (tabs for each language)
│   ├── Title (text)
│   └── Description (textarea)
└── Options (repeater)
    ├── Key (text, unique within page)
    ├── Icon (icon picker)
    └── Translations (text per language)

On Save:
├── New: POST /onboarding/pages/manage
└── Edit: PUT /onboarding/pages/manage/{pageId}

Quick Actions

Activate: PATCH /onboarding/pages/manage/{pageId}/activate
Deactivate: PATCH /onboarding/pages/manage/{pageId}/deactivate
Delete: DELETE /onboarding/pages/manage/{pageId} (with confirmation)

Frontend Implementation Guide

Phone Verification Screen

1. Show phone input with country code selector
2. On submit: POST /onboarding/auth-phone/request-otp
3. Save token from response
4. Navigate to OTP input screen
5. Show countdown for resendAvailableIn
6. On OTP submit: POST /onboarding/auth-phone/verify
7. On success: Navigate based on nextStep

Preference Pages Loop

1. GET /onboarding/pages?current=true
2. If page is null → All done, navigate to profile
3. Display page with:
   ├── Title, description (translated)
   ├── Banner image
   ├── Options as selectable chips/cards
   └── Skip button (if isSkippable)
4. On submit: POST /onboarding/pages/{pageId}/response
5. Check progress.isCompleted
   ├── true → Navigate to profile completion
   └── false → GET /onboarding/pages?current=true (loop)

Progress Indicator

GET /onboarding/progress
├── Use percentage for progress bar
├── Show steps as dots/icons
├── Highlight current step
└── Show completed steps with checkmarks