Onboarding EndUser
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: POST {base_url}/onboarding/auth-phone/request-otp
Access Level: 🔒 Protected
Authentication: Bearer Token
Prerequisite: User must be at PENDING_PHONE_VERIFICATION step
Request JSON Sample:
{
"phoneNumber": "+255712345678"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| phoneNumber | string | Yes | Phone number with country code | E.164 format: +255XXXXXXXXX |
Supported Country Codes:
+255- Tanzania+254- Kenya+256- Uganda+250- Rwanda+257- Burundi
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent successfully",
"action_time": "2025-01-05T10:30:45",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"phoneNumber": "+255****678",
"expiresInSeconds": 600,
"resendAvailableIn": 120
}
}
Success Response Fields:
| Field | Description |
|---|---|
| token | Temporary token for verify/resend endpoints |
| phoneNumber | Masked phone number for display |
| expiresInSeconds | OTP validity period (10 minutes) |
| resendAvailableIn | Seconds until resend allowed (2 minutes) |
Error Responses:
Wrong Onboarding Step (412):
{
"success": false,
"httpStatus": "PRECONDITION_FAILED",
"message": "Onboarding step required",
"action_time": "2025-01-05T10:30:45",
"data": {
"message": "Complete email verification first",
"currentStep": "PENDING_EMAIL_VERIFICATION",
"requiredStep": "PENDING_EMAIL_VERIFICATION"
}
}
Rate Limit Exceeded (429):
{
"success": false,
"httpStatus": "TOO_MANY_REQUESTS",
"message": "Too many OTP requests. Try again in 10 minutes.",
"action_time": "2025-01-05T10:30:45",
"data": "Too many OTP requests. Try again in 10 minutes."
}
Phone Already Registered (409):
{
"success": false,
"httpStatus": "CONFLICT",
"message": "Phone number already registered",
"action_time": "2025-01-05T10:30:45",
"data": "Phone number already registered"
}
2. Verify OTP
Purpose: Verify OTP code and complete phone verification.
Endpoint: POST {base_url}/onboarding/auth-phone/verify
Access Level: 🔒 Protected
Authentication: Bearer Token
Request JSON Sample:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"otp": "123456"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| token | string | Yes | Token from request-otp response | Must be valid, non-expired |
| otp | string | Yes | 6-digit OTP code | Exactly 6 digits |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Phone verified successfully",
"action_time": "2025-01-05T10:35:00",
"data": {
"verified": true,
"phoneNumber": "+255****678",
"onboardingStatus": "PENDING_PREFERENCES",
"nextStep": "/api/v1/onboarding/pages"
}
}
Error Responses:
Invalid OTP (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Invalid OTP. 2 attempt(s) remaining.",
"action_time": "2025-01-05T10:35:00",
"data": "Invalid OTP. 2 attempt(s) remaining."
}
Expired OTP (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "OTP has expired. Please request a new one.",
"action_time": "2025-01-05T10:35:00",
"data": "OTP has expired. Please request a new one."
}
Max Attempts Reached (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Maximum attempts reached. Please request a new OTP.",
"action_time": "2025-01-05T10:35:00",
"data": "Maximum attempts reached. Please request a new OTP."
}
3. Resend OTP
Purpose: Request a new OTP code using the existing token.
Endpoint: POST {base_url}/onboarding/auth-phone/resend-otp
Access Level: 🔒 Protected
Authentication: Bearer Token
Request JSON Sample:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Success Response: Same as Request OTP endpoint
Email Verification Endpoints
4. Get Email Verification Status
Purpose: Check email verification status and options.
Endpoint: GET {base_url}/onboarding/email-verification/status
Access Level: 🔒 Protected
Authentication: Bearer Token
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Email verification status",
"action_time": "2025-01-05T10:00:00",
"data": {
"verified": false,
"email": "jo***@example.com",
"required": false,
"canSkip": true,
"currentStep": "PENDING_EMAIL_VERIFICATION"
}
}
5. Skip Email Verification
Purpose: Skip email verification step (if allowed by config).
Endpoint: POST {base_url}/onboarding/email-verification/skip
Access Level: 🔒 Protected
Authentication: Bearer Token
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Email verification skipped",
"action_time": "2025-01-05T10:05:00",
"data": {
"verified": false,
"skipped": true,
"nextStep": "PENDING_PHONE_VERIFICATION"
}
}
Preference Pages Endpoints
6. Get Onboarding Progress
Purpose: Get overall onboarding progress with all steps.
Endpoint: GET {base_url}/onboarding/progress
Access Level: 🔒 Protected
Authentication: Bearer Token
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Progress retrieved",
"action_time": "2025-01-05T10:40:00",
"data": {
"percentage": 45.0,
"currentStage": "PENDING_PREFERENCES",
"currentStageLabel": "Complete your preferences",
"steps": [
{
"key": "registration",
"label": "Registration",
"completed": true,
"weight": 15.0,
"skippable": false
},
{
"key": "phone_verification",
"label": "Phone Verification",
"completed": true,
"weight": 15.0,
"skippable": false
},
{
"key": "page_interests",
"label": "Your Interests",
"completed": true,
"weight": 13.33,
"skippable": false
},
{
"key": "page_goals",
"label": "Your Goals",
"completed": false,
"weight": 13.33,
"skippable": true
},
{
"key": "page_experience",
"label": "Your Experience",
"completed": false,
"weight": 13.33,
"skippable": false
},
{
"key": "profile_completion",
"label": "Complete Profile",
"completed": false,
"weight": 15.0,
"skippable": false
}
],
"nextStep": {
"key": "page_goals",
"label": "Your Goals",
"endpoint": "/api/v1/onboarding/pages?page=2",
"skippable": true
}
}
}
7. Get All Pages
Purpose: Get all preference pages with completion status.
Endpoint: GET {base_url}/onboarding/pages
Access Level: 🔒 Protected
Authentication: Bearer Token
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| all | boolean | No | Return all pages (default behavior) |
| page | integer | No | Get specific page by order (1, 2, 3...) |
| category | string | No | Get page by category key |
| current | boolean | No | Get first incomplete page |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "All pages retrieved",
"action_time": "2025-01-05T10:45:00",
"data": {
"totalPages": 3,
"completedPages": 1,
"isOnboardingComplete": false,
"pages": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"pageOrder": 1,
"categoryKey": "interests",
"title": "Maslahi Yako",
"description": "Chagua mambo yanayokuvutia",
"bannerImages": ["https://cdn.fursahub.com/onboarding/interests.jpg"],
"isSkippable": false,
"minSelections": 1,
"maxSelections": 5,
"options": [
{ "key": "jobs", "label": "Kazi", "icon": "briefcase" },
{ "key": "funding", "label": "Ufadhili", "icon": "dollar" },
{ "key": "events", "label": "Matukio", "icon": "calendar" },
{ "key": "skills", "label": "Ujuzi", "icon": "book" },
{ "key": "networking", "label": "Mitandao", "icon": "users" }
],
"isCompleted": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"pageOrder": 2,
"categoryKey": "goals",
"title": "Malengo Yako",
"description": "Unataka kufikia nini?",
"isSkippable": true,
"minSelections": 1,
"maxSelections": 3,
"options": [
{ "key": "find_job", "label": "Kupata kazi", "icon": "search" },
{ "key": "start_business", "label": "Kuanzisha biashara", "icon": "store" },
{ "key": "learn_skills", "label": "Kujifunza ujuzi", "icon": "graduation" },
{ "key": "get_funding", "label": "Kupata ufadhili", "icon": "money" }
],
"isCompleted": false
}
]
}
}
8. Get Current Page
Purpose: Get the first incomplete preference page.
Endpoint: GET {base_url}/onboarding/pages?current=true
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Current page retrieved",
"action_time": "2025-01-05T10:50:00",
"data": {
"page": {
"id": "550e8400-e29b-41d4-a716-446655440002",
"pageOrder": 2,
"categoryKey": "goals",
"title": "Malengo Yako",
"description": "Unataka kufikia nini?",
"isSkippable": true,
"minSelections": 1,
"maxSelections": 3,
"options": [...]
},
"progress": {
"current": 2,
"total": 3,
"nextPage": 3,
"isLast": false,
"isCompleted": false
}
}
}
9. Submit Page Response
Purpose: Save user's selections for a preference page.
Endpoint: POST {base_url}/onboarding/pages/{pageId}/response
Access Level: 🔒 Protected
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| pageId | UUID | Yes | Page identifier |
Request JSON Sample:
{
"selectedOptions": ["find_job", "learn_skills"]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| selectedOptions | array | Yes | Array of selected option keys | Must match page's min/max selections |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Response saved",
"action_time": "2025-01-05T10:55:00",
"data": {
"saved": true,
"progress": {
"current": 2,
"total": 3,
"nextPage": 3,
"isLast": false,
"isCompleted": false
}
}
}
Error Responses:
Too Few Selections (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Minimum 1 selection(s) required",
"action_time": "2025-01-05T10:55:00",
"data": "Minimum 1 selection(s) required"
}
Invalid Option (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid option: unknown_key",
"action_time": "2025-01-05T10:55:00",
"data": "Invalid option: unknown_key"
}
10. Skip Page
Purpose: Skip a preference page (only if page is skippable).
Endpoint: POST {base_url}/onboarding/pages/{pageId}/skip
Access Level: 🔒 Protected
Authentication: Bearer Token
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page skipped",
"action_time": "2025-01-05T11:00:00",
"data": {
"saved": true,
"progress": {
"current": 2,
"total": 3,
"nextPage": 3,
"isLast": false,
"isCompleted": false
}
}
}
Error Response (Page Not Skippable):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "This page cannot be skipped",
"action_time": "2025-01-05T11:00:00",
"data": "This page cannot be skipped"
}
Language Preference Endpoint
11. Set Language Preference
Purpose: Update user's language preference (can be called anytime during onboarding).
Endpoint: POST {base_url}/onboarding/language-preference
Access Level: 🔒 Protected
Authentication: Bearer Token
Request JSON Sample:
{
"code": "sw"
}
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Language preference updated",
"action_time": "2025-01-05T11:05:00",
"data": {
"code": "sw",
"name": "Swahili",
"nativeName": "Kiswahili"
}
}
Frontend Implementation Guide
Phone Verification Screen
1. Show phone input with country code selector
2. On submit: POST /onboarding/auth-phone/request-otp
3. Save token from response
4. Navigate to OTP input screen
5. Show countdown for resendAvailableIn
6. On OTP submit: POST /onboarding/auth-phone/verify
7. On success: Navigate based on nextStep
Preference Pages Loop
1. GET /onboarding/pages?current=true
2. If page is null → All done, navigate to profile
3. Display page with:
├── Title, description (translated)
├── Banner image
├── Options as selectable chips/cards
└── Skip button (if isSkippable)
4. On submit: POST /onboarding/pages/{pageId}/response
5. Check progress.isCompleted
├── true → Navigate to profile completion
└── false → GET /onboarding/pages?current=true (loop)
Progress Indicator
GET /onboarding/progress
├── Use percentage for progress bar
├── Show steps as dots/icons
├── Highlight current step
└── Show completed steps with checkmarks
No comments to display
No comments to display