# 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**:

- Firebase handles the actual sign-in (Google, Apple, Email) - your app gets a Firebase ID token
- Send that Firebase token to our backend to get Fursa Hub access tokens
- Access tokens expire in 1 hour, use refresh token to get new ones
- Pass `preferredLanguage` and `theme` during first authentication to set user preferences

---

## 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

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

```

### Error Response Structure

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/firebase/authenticate`

**Access Level**: 🌐 Public

**Authentication**: None (Firebase token in body)

**Request Headers**:

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>`application/json`</td></tr></tbody></table>

**Request JSON Sample**:

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

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>firebaseToken</td><td>string</td><td>Yes</td><td>Firebase ID token from client SDK</td><td>Must be valid Firebase token</td></tr><tr><td>preferredLanguage</td><td>string</td><td>No</td><td>User's language preference</td><td>2-5 chars (e.g., "en", "sw", "fr")</td></tr><tr><td>theme</td><td>string</td><td>No</td><td>UI theme preference</td><td>enum: `LIGHT`, `DARK`, `SYSTEM`</td></tr><tr><td>deviceInfo</td><td>string</td><td>No</td><td>Device information for session tracking</td><td>Max 255 chars</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**:

<table id="bkmrk-field-description-ac"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>accessToken</td><td>JWT token for API requests (expires in 1 hour)</td></tr><tr><td>refreshToken</td><td>Token to get new accessToken (expires in 30 days)</td></tr><tr><td>tokenType</td><td>Always "Bearer"</td></tr><tr><td>expiresIn</td><td>Access token lifetime in seconds</td></tr><tr><td>user</td><td>User profile information</td></tr><tr><td>user.theme</td><td>User's theme preference: LIGHT, DARK, or SYSTEM</td></tr><tr><td>onboarding.isComplete</td><td>`false` = must complete onboarding, `true` = can access app</td></tr><tr><td>onboarding.currentStep</td><td>Current onboarding step (see Onboarding docs)</td></tr></tbody></table>

---

## 2. Refresh Access Token

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

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/refresh`

**Access Level**: 🌐 Public

**Authentication**: None (refresh token in body)

**Request JSON Sample**:

```json
{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>refreshToken</td><td>string</td><td>Yes</td><td>Refresh token from authentication</td><td>Must be valid, non-expired</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):*

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

```

*Expired Refresh Token (401):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/auth/logout`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-1"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>`Bearer {accessToken}`</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/languages`

**Access Level**: 🌐 Public

**Authentication**: None

**Success Response JSON Sample**:

```json
{
  "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**:

- Maximum file size: 25MB per file
- Supported image types: JPEG, PNG, GIF, WebP, BMP
- Supported video types: MP4, AVI, MOV, WebM, MKV
- Supported documents: PDF, Word, Excel, Text files
- Each user gets their own storage bucket
- BlurHash automatically generated for images (use for loading placeholders)
- Files are publicly accessible via permanentUrl

---

## 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

<table id="bkmrk-directory-purpose-ty"><thead><tr><th>Directory</th><th>Purpose</th><th>Typical Use</th></tr></thead><tbody><tr><td>`PROFILE`</td><td>Profile photos</td><td>User avatar, cover images</td></tr><tr><td>`SOCIAL_POST`</td><td>Social media posts</td><td>Post images, videos</td></tr><tr><td>`CALLS`</td><td>Call for proposals</td><td>Proposal documents</td></tr><tr><td>`FUNDS`</td><td>Funding/grants</td><td>Grant application files</td></tr><tr><td>`EVENTS`</td><td>Events</td><td>Event banners, tickets</td></tr><tr><td>`OPPORTUNITIES`</td><td>Job/opportunity</td><td>Job post attachments</td></tr><tr><td>`INNOVATION`</td><td>Innovation hub</td><td>Project files</td></tr><tr><td>`SKILL_CENTER`</td><td>Skills/courses</td><td>Course materials</td></tr></tbody></table>

---

## Endpoints

---

## 1. Upload Single File

**Purpose**: Upload a single file to the specified directory.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/files/upload-single`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Content-Type**: `multipart/form-data`

**Request Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>file</td><td>file</td><td>Yes</td><td>File to upload</td><td>Max 25MB</td></tr><tr><td>directory</td><td>string</td><td>Yes</td><td>Target directory</td><td>enum: PROFILE, SOCIAL\_POST, CALLS, FUNDS, EVENTS, OPPORTUNITIES, INNOVATION, SKILL\_CENTER</td></tr></tbody></table>

**Example Request** (using curl):

```bash
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**:

```json
{
  "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**:

<table id="bkmrk-field-description-fi"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>fileName</td><td>Generated unique filename</td></tr><tr><td>originalFileName</td><td>Original uploaded filename</td></tr><tr><td>objectKey</td><td>Full path in storage (directory/filename)</td></tr><tr><td>directory</td><td>Storage directory</td></tr><tr><td>permanentUrl</td><td>Public URL to access the file</td></tr><tr><td>thumbnailUrl</td><td>Thumbnail URL (same as permanentUrl for images)</td></tr><tr><td>blurHash</td><td>BlurHash string for image placeholders (null for non-images)</td></tr><tr><td>fileType</td><td>`IMAGE`, `VIDEO`, `DOCUMENT`, or `OTHER`</td></tr><tr><td>width, height</td><td>Image dimensions (null for non-images)</td></tr><tr><td>checksum</td><td>MD5 checksum for integrity verification</td></tr></tbody></table>

**Error Responses**:

*File Too Large (400):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/files/upload`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Content-Type**: `multipart/form-data`

**Request Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>files</td><td>file\[\]</td><td>Yes</td><td>Array of files</td><td>Max 25MB each</td></tr><tr><td>directory</td><td>string</td><td>Yes</td><td>Target directory</td><td>enum values</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):

```json
{
  "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**:

```javascript
// 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**:

- All profile endpoints require authentication (Bearer token)
- Username must be unique and can only contain letters, numbers, and underscores
- Profile photos should be uploaded via Files API first, then URLs added here
- Completing profile (fullName + username + bio) auto-completes onboarding if at final step
- Theme changes take effect immediately on client side

---

## 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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/profile`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>`Bearer {accessToken}`</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**:

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

---

## 2. Update Profile

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

**Endpoint**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/profile`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-1"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>`Bearer {accessToken}`</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>`application/json`</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "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**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>fullName</td><td>string</td><td>No</td><td>Display name</td><td>Min: 2, Max: 100 chars</td></tr><tr><td>username</td><td>string</td><td>No</td><td>Unique username</td><td>Min: 3, Max: 30 chars, alphanumeric + underscore only</td></tr><tr><td>bio</td><td>string</td><td>No</td><td>User biography</td><td>Max: 500 chars</td></tr><tr><td>gender</td><td>string</td><td>No</td><td>User gender</td><td>enum: `MALE`, `FEMALE`</td></tr><tr><td>link</td><td>string</td><td>No</td><td>Personal/social link</td><td>Must be valid URL (https://...)</td></tr><tr><td>profilePhotoUrls</td><td>array</td><td>No</td><td>List of photo URLs</td><td>Array of strings</td></tr><tr><td>theme</td><td>string</td><td>No</td><td>UI theme preference</td><td>enum: `LIGHT`, `DARK`, `SYSTEM`</td></tr><tr><td>preferredLanguage</td><td>string</td><td>No</td><td>Language preference</td><td>Must be active language code</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):*

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

```

*Invalid Language Code (400):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `{base_url}/profile/theme`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "theme": "DARK"
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>theme</td><td>string</td><td>Yes</td><td>Theme preference</td><td>enum: `LIGHT`, `DARK`, `SYSTEM`</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/profile/username/check`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>username</td><td>string</td><td>Yes</td><td>Username to check</td><td>Min: 3 chars</td></tr></tbody></table>

**Success Response JSON Sample (Available)**:

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

```

**Success Response JSON Sample (Taken)**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/profile/photo`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

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

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>photoUrl</td><td>string</td><td>Yes</td><td>URL of uploaded photo</td><td>Must be valid URL</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/profile/photo`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>photoUrl</td><td>string</td><td>Yes</td><td>URL of photo to remove</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**:

- Onboarding steps must be completed in order - skipping ahead returns 412 PRECONDITION\_FAILED
- Phone verification uses OTP sent via SMS (supports TZ, KE, UG, RW, BI country codes)
- Preference pages are dynamic - admin can add/remove/reorder without code changes
- User's preferredLanguage determines translation of all onboarding content
- Email verification is optional/skippable by default (Firebase handles actual verification)

---

## 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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{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**:

```json
{
  "phoneNumber": "+255712345678"
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>phoneNumber</td><td>string</td><td>Yes</td><td>Phone number with country code</td><td>E.164 format: +255XXXXXXXXX</td></tr></tbody></table>

**Supported Country Codes**:

- `+255` - Tanzania
- `+254` - Kenya
- `+256` - Uganda
- `+250` - Rwanda
- `+257` - Burundi

**Success Response JSON Sample**:

```json
{
  "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**:

<table id="bkmrk-field-description-to"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>token</td><td>Temporary token for verify/resend endpoints</td></tr><tr><td>phoneNumber</td><td>Masked phone number for display</td></tr><tr><td>expiresInSeconds</td><td>OTP validity period (10 minutes)</td></tr><tr><td>resendAvailableIn</td><td>Seconds until resend allowed (2 minutes)</td></tr></tbody></table>

**Error Responses**:

*Wrong Onboarding Step (412):*

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/auth-phone/verify`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

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

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>token</td><td>string</td><td>Yes</td><td>Token from request-otp response</td><td>Must be valid, non-expired</td></tr><tr><td>otp</td><td>string</td><td>Yes</td><td>6-digit OTP code</td><td>Exactly 6 digits</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/auth-phone/resend-otp`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/email-verification/status`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/email-verification/skip`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/progress`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/pages`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>all</td><td>boolean</td><td>No</td><td>Return all pages (default behavior)</td></tr><tr><td>page</td><td>integer</td><td>No</td><td>Get specific page by order (1, 2, 3...)</td></tr><tr><td>category</td><td>string</td><td>No</td><td>Get page by category key</td></tr><tr><td>current</td><td>boolean</td><td>No</td><td>Get first incomplete page</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/pages?current=true`

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/pages/{pageId}/response`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>pageId</td><td>UUID</td><td>Yes</td><td>Page identifier</td></tr></tbody></table>

**Request JSON Sample**:

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

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>selectedOptions</td><td>array</td><td>Yes</td><td>Array of selected option keys</td><td>Must match page's min/max selections</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/pages/{pageId}/skip`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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)**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/language-preference`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "code": "sw"
}

```

**Success Response JSON Sample**:

```json
{
  "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**:

- Onboarding steps must be completed in order - skipping ahead returns 412 PRECONDITION\_FAILED
- Phone verification uses OTP sent via SMS (supports TZ, KE, UG, RW, BI country codes)
- Preference pages are dynamic - admin can add/remove/reorder without code changes
- User's preferredLanguage determines translation of all onboarding content
- Email verification is optional/skippable by default (Firebase handles actual verification)

---

## 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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{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**:

```json
{
  "phoneNumber": "+255712345678"
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>phoneNumber</td><td>string</td><td>Yes</td><td>Phone number with country code</td><td>E.164 format: +255XXXXXXXXX</td></tr></tbody></table>

**Supported Country Codes**:

- `+255` - Tanzania
- `+254` - Kenya
- `+256` - Uganda
- `+250` - Rwanda
- `+257` - Burundi

**Success Response JSON Sample**:

```json
{
  "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**:

<table id="bkmrk-field-description-to"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>token</td><td>Temporary token for verify/resend endpoints</td></tr><tr><td>phoneNumber</td><td>Masked phone number for display</td></tr><tr><td>expiresInSeconds</td><td>OTP validity period (10 minutes)</td></tr><tr><td>resendAvailableIn</td><td>Seconds until resend allowed (2 minutes)</td></tr></tbody></table>

**Error Responses**:

*Wrong Onboarding Step (412):*

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/auth-phone/verify`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

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

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>token</td><td>string</td><td>Yes</td><td>Token from request-otp response</td><td>Must be valid, non-expired</td></tr><tr><td>otp</td><td>string</td><td>Yes</td><td>6-digit OTP code</td><td>Exactly 6 digits</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/auth-phone/resend-otp`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/email-verification/status`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/email-verification/skip`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/progress`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/pages`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>all</td><td>boolean</td><td>No</td><td>Return all pages (default behavior)</td></tr><tr><td>page</td><td>integer</td><td>No</td><td>Get specific page by order (1, 2, 3...)</td></tr><tr><td>category</td><td>string</td><td>No</td><td>Get page by category key</td></tr><tr><td>current</td><td>boolean</td><td>No</td><td>Get first incomplete page</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/pages?current=true`

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/pages/{pageId}/response`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>pageId</td><td>UUID</td><td>Yes</td><td>Page identifier</td></tr></tbody></table>

**Request JSON Sample**:

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

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>selectedOptions</td><td>array</td><td>Yes</td><td>Array of selected option keys</td><td>Must match page's min/max selections</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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):*

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/pages/{pageId}/skip`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Success Response JSON Sample**:

```json
{
  "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)**:

```json
{
  "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**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/language-preference`

**Access Level**: 🔒 Protected

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "code": "sw"
}

```

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/pages/manage`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Authentication**: Bearer Token (ROLE\_ADMIN, ROLE\_SUPER\_ADMIN, ROLE\_MODERATOR)

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/onboarding/pages/manage/{pageId}`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-5"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>pageId</td><td>UUID</td><td>Yes</td><td>Page identifier</td></tr></tbody></table>

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

---

## 14. Create Page

**Purpose**: Create a new onboarding preference page.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/onboarding/pages/manage`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Authentication**: Bearer Token

**Request JSON Sample**:

```json
{
  "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**:

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

**Success Response JSON Sample**:

```json
{
  "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):*

```json
{
  "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**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/onboarding/pages/manage/{pageId}`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-7"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>pageId</td><td>UUID</td><td>Yes</td><td>Page identifier</td></tr></tbody></table>

**Request JSON Sample**: Same as Create Page

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/onboarding/pages/manage/{pageId}`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-8"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>pageId</td><td>UUID</td><td>Yes</td><td>Page identifier</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `{base_url}/onboarding/pages/manage/{pageId}/activate`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `{base_url}/onboarding/pages/manage/{pageId}/deactivate`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Success Response JSON Sample**:

```json
{
  "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**: <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `{base_url}/onboarding/pages/manage/reorder`

**Access Level**: 🔒 Protected (Admin/Moderator)

**Request JSON Sample**:

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

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-9"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>pageIds</td><td>array</td><td>Yes</td><td>Page IDs in new order</td><td>Must include all active page IDs</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "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

```