Onboarding
Base URL: https://api.fursahub.com/api/v1
Short Description: The Onboarding API manages the complete user onboarding flow for Fursa Hub. It handles phone verification via OTP, language preference selection, dynamic preference pages with multilingual support, progress tracking, and administrative page management. The system uses a state machine approach to ensure users complete all required steps before accessing the main platform.
Hints:
- Onboarding follows a strict sequence: Phone Verification → Language Preference → Preference Pages → Profile Completion
- All preference pages are admin-configurable with multilingual support (EN, SW, FR, ZH)
- Phone verification uses internal OTP system via Textify Africa SMS gateway
- Progress endpoint provides weighted percentage completion across all steps
- Skip is only allowed on pages marked as
isSkippable: true - User's
Accept-Languageheader orpreferredLanguagedetermines response language
Onboarding Flow Overview
Complete Onboarding Journey
┌─────────────────────────────────────────────────────────────────────────┐
│ ONBOARDING STATE MACHINE │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ PENDING │ │ PENDING │ │ PENDING │ │ │
│ PHONE │────▶│ PREFERENCES │────▶│ PROFILE │────▶│ COMPLETED │
│ VERIFICATION │ │ │ │ COMPLETION │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
Phone OTP Flow Preference Pages Profile Update Main App
(request/verify) (dynamic pages) (auto-complete) Access
Onboarding Status Values
| Status | Description | User Action Required |
|---|---|---|
PENDING_PHONE_VERIFICATION |
Initial state after Firebase auth | Verify phone number via OTP |
PENDING_PREFERENCES |
Phone verified | Complete all preference pages |
PENDING_PROFILE_COMPLETION |
Preferences done | Fill profile (name, username, bio) |
COMPLETED |
All steps done | Full platform access |
Step Weight Distribution
| Step | Weight | Description |
|---|---|---|
| Registration | 15% | Auto-completed when user exists |
| Email Verification | 15% | From Firebase (may already be done) |
| Phone Verification | 15% | Required - via OTP |
| Preference Pages | 40% | Split equally among active pages |
| Profile Completion | 15% | Final step |
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-01-02T10:30:45",
"data": "Error description"
}
Phone Verification Endpoints
1. Request OTP
Purpose: Request a one-time password to be sent via SMS for phone number verification.
Endpoint: POST {base_url}/onboarding/auth-phone/request-otp
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"phoneNumber": "+255712345678"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
phoneNumber |
string | Yes | Phone number in E.164 format | Pattern: ^\+[0-9]{10,15}$ |
Supported Country Codes:
| Code | Country |
|---|---|
+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-02T10:30:45",
"data": {
"phoneNumber": "+255****678",
"expiresInSeconds": 600,
"resendAvailableIn": 120
}
}
Success Response Fields:
| Field | Description |
|---|---|
phoneNumber |
Masked phone number for security |
expiresInSeconds |
OTP validity period (default: 600 = 10 minutes) |
resendAvailableIn |
Seconds until resend is allowed (default: 120 = 2 minutes) |
Error Response JSON Samples:
Phone Already Registered (409):
{
"success": false,
"httpStatus": "CONFLICT",
"message": "Phone number already registered",
"action_time": "2025-01-02T10:30:45",
"data": "Phone number already registered to another account"
}
Rate Limit Exceeded (429):
{
"success": false,
"httpStatus": "TOO_MANY_REQUESTS",
"message": "Too many OTP requests",
"action_time": "2025-01-02T10:30:45",
"data": "Too many OTP requests. Try again in 10 minutes."
}
Resend Cooldown Active (429):
{
"success": false,
"httpStatus": "TOO_MANY_REQUESTS",
"message": "Please wait before requesting another OTP",
"action_time": "2025-01-02T10:30:45",
"data": "Please wait 85 seconds before requesting another OTP"
}
Unsupported Country (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Unsupported country code",
"action_time": "2025-01-02T10:30:45",
"data": "Supported country codes: +255, +254, +256, +250, +257"
}
2. Verify OTP
Purpose: Verify the OTP code sent to the user's phone number.
Endpoint: POST {base_url}/onboarding/auth-phone/verify
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"phoneNumber": "+255712345678",
"otp": "123456"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
phoneNumber |
string | Yes | Phone number that received OTP | E.164 format |
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-02T10:30:45",
"data": {
"verified": true,
"phoneNumber": "+255712345678",
"onboardingStatus": "PENDING_PREFERENCES",
"nextStep": "/api/v1/onboarding/preferences"
}
}
Success Response Fields:
| Field | Description |
|---|---|
verified |
Always true on success |
phoneNumber |
The verified phone number |
onboardingStatus |
Updated status (moves to PENDING_PREFERENCES) |
nextStep |
API endpoint for next onboarding step |
Error Response JSON Samples:
Invalid OTP (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Invalid OTP",
"action_time": "2025-01-02T10:30:45",
"data": "Invalid OTP. 2 attempt(s) remaining."
}
OTP Expired (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "OTP has expired",
"action_time": "2025-01-02T10:30:45",
"data": "OTP has expired. Please request a new one."
}
Max Attempts Reached (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Maximum attempts reached",
"action_time": "2025-01-02T10:30:45",
"data": "Maximum attempts reached. Please request a new OTP."
}
No Active OTP (403):
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "No active OTP found",
"action_time": "2025-01-02T10:30:45",
"data": "No active OTP found. Please request a new one."
}
3. Resend OTP
Purpose: Resend OTP to the same phone number (subject to cooldown).
Endpoint: POST {base_url}/onboarding/auth-phone/resend-otp
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"phoneNumber": "+255712345678"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
phoneNumber |
string | Yes | Phone number to resend OTP to | E.164 format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "OTP resent successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"phoneNumber": "+255****678",
"expiresInSeconds": 600,
"resendAvailableIn": 120
}
}
Same response format as Request OTP
Language Preference Endpoint
4. Set Language Preference
Purpose: Set user's preferred language for the platform.
Endpoint: POST {base_url}/onboarding/language-preference
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Request JSON Sample:
{
"code": "sw"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
code |
string | Yes | Language code | 2-5 characters, must be active language |
Supported Languages:
| Code | Name | Native Name |
|---|---|---|
en |
English | English |
sw |
Swahili | Kiswahili |
fr |
French | Français |
zh |
Chinese | 中文 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Language preference updated",
"action_time": "2025-01-02T10:30:45",
"data": {
"code": "sw",
"name": "Swahili",
"nativeName": "Kiswahili"
}
}
Success Response Fields:
| Field | Description |
|---|---|
code |
Language code that was set |
name |
English name of the language |
nativeName |
Native name of the language |
Onboarding Progress Endpoint
5. Get Onboarding Progress
Purpose: Get detailed progress information across all onboarding steps with weighted percentages.
Endpoint: GET {base_url}/onboarding/progress
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Accept-Language | string | No | Preferred language (default: en) |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Progress retrieved",
"action_time": "2025-01-02T10:30:45",
"data": {
"percentage": 55.0,
"currentStage": "PENDING_PREFERENCES",
"currentStageLabel": "Complete your preferences",
"steps": [
{
"key": "registration",
"label": "Registration",
"completed": true,
"weight": 15.0
},
{
"key": "email_verification",
"label": "Email Verification",
"completed": true,
"weight": 15.0
},
{
"key": "phone_verification",
"label": "Phone Verification",
"completed": true,
"weight": 15.0
},
{
"key": "page_interests",
"label": "What are your interests?",
"completed": true,
"weight": 13.33
},
{
"key": "page_industries",
"label": "Select your industries",
"completed": false,
"weight": 13.33
},
{
"key": "page_goals",
"label": "What are your goals?",
"completed": false,
"weight": 13.34
},
{
"key": "profile_completion",
"label": "Complete Profile",
"completed": false,
"weight": 15.0
}
],
"nextStep": {
"key": "page_industries",
"label": "Select your industries",
"endpoint": "/api/v1/onboarding/pages?page=2"
}
}
}
Success Response Fields:
| Field | Description |
|---|---|
percentage |
Overall completion percentage (0-100) |
currentStage |
Current onboarding status enum value |
currentStageLabel |
Human-readable description of current stage |
steps |
Array of all onboarding steps with completion status |
steps[].key |
Unique identifier for the step |
steps[].label |
Display label (translated) |
steps[].completed |
Whether step is completed |
steps[].weight |
Percentage weight of this step |
nextStep |
Next incomplete step details |
nextStep.key |
Step identifier |
nextStep.label |
Step display label |
nextStep.endpoint |
API endpoint to complete this step |
Onboarding Pages Endpoints
6. Get All Pages
Purpose: Retrieve all active onboarding preference pages with completion status.
Endpoint: GET {base_url}/onboarding/pages
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Accept-Language | string | No | Preferred language (default: en) |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
all |
boolean | No | If true, returns all pages |
page |
integer | No | Get specific page by order number |
category |
string | No | Get page by category key |
current |
boolean | No | If true, returns current incomplete page |
Success Response JSON Sample (all=true):
{
"success": true,
"httpStatus": "OK",
"message": "All pages retrieved",
"action_time": "2025-01-02T10:30:45",
"data": {
"totalPages": 3,
"completedPages": 1,
"isOnboardingComplete": false,
"pages": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"pageOrder": 1,
"categoryKey": "interests",
"title": "What are your interests?",
"description": "Select the areas you're most interested in exploring on Fursa Hub",
"bannerImages": [
"https://cdn.fursahub.com/onboarding/interests-banner.jpg"
],
"isSkippable": false,
"minSelections": 1,
"maxSelections": 5,
"options": [
{
"key": "technology",
"label": "Technology",
"icon": "💻"
},
{
"key": "agriculture",
"label": "Agriculture",
"icon": "🌾"
},
{
"key": "finance",
"label": "Finance & Banking",
"icon": "💰"
},
{
"key": "health",
"label": "Healthcare",
"icon": "🏥"
},
{
"key": "education",
"label": "Education",
"icon": "📚"
}
],
"isCompleted": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"pageOrder": 2,
"categoryKey": "industries",
"title": "Select your industries",
"description": "Choose the industries you work in or want to explore",
"bannerImages": [],
"isSkippable": true,
"minSelections": 0,
"maxSelections": 10,
"options": [
{
"key": "tech_startup",
"label": "Tech Startups",
"icon": "🚀"
},
{
"key": "manufacturing",
"label": "Manufacturing",
"icon": "🏭"
},
{
"key": "retail",
"label": "Retail & E-commerce",
"icon": "🛒"
}
],
"isCompleted": false
},
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"pageOrder": 3,
"categoryKey": "goals",
"title": "What are your goals?",
"description": "Tell us what you want to achieve on Fursa Hub",
"bannerImages": [],
"isSkippable": false,
"minSelections": 1,
"maxSelections": 3,
"options": [
{
"key": "find_job",
"label": "Find a Job",
"icon": "💼"
},
{
"key": "find_funding",
"label": "Find Funding",
"icon": "💵"
},
{
"key": "network",
"label": "Networking",
"icon": "🤝"
},
{
"key": "learn_skills",
"label": "Learn New Skills",
"icon": "📖"
}
],
"isCompleted": false
}
]
}
}
Success Response Fields:
| Field | Description |
|---|---|
totalPages |
Total number of active onboarding pages |
completedPages |
Number of pages user has completed |
isOnboardingComplete |
Whether all pages are completed |
pages |
Array of page objects |
pages[].id |
Unique page identifier (UUID) |
pages[].pageOrder |
Display order (1, 2, 3...) |
pages[].categoryKey |
Unique category identifier |
pages[].title |
Page title (translated) |
pages[].description |
Page description (translated) |
pages[].bannerImages |
Array of banner image URLs |
pages[].isSkippable |
Whether page can be skipped |
pages[].minSelections |
Minimum options user must select |
pages[].maxSelections |
Maximum options user can select |
pages[].options |
Array of selectable options |
pages[].options[].key |
Option identifier |
pages[].options[].label |
Option label (translated) |
pages[].options[].icon |
Option icon (emoji or URL) |
pages[].isCompleted |
Whether user has completed this page |
7. Get Single Page
Purpose: Retrieve a specific onboarding page with progress context.
Endpoint: GET {base_url}/onboarding/pages?page={pageOrder}
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page |
integer | Yes | Page order number (1, 2, 3...) |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page retrieved",
"action_time": "2025-01-02T10:30:45",
"data": {
"page": {
"id": "550e8400-e29b-41d4-a716-446655440002",
"pageOrder": 2,
"categoryKey": "industries",
"title": "Select your industries",
"description": "Choose the industries you work in or want to explore",
"bannerImages": [],
"isSkippable": true,
"minSelections": 0,
"maxSelections": 10,
"options": [
{
"key": "tech_startup",
"label": "Tech Startups",
"icon": "🚀"
},
{
"key": "manufacturing",
"label": "Manufacturing",
"icon": "🏭"
}
],
"isCompleted": false
},
"progress": {
"current": 2,
"total": 3,
"nextPage": 3,
"isLast": false,
"isCompleted": false
}
}
}
Success Response Fields:
| Field | Description |
|---|---|
page |
Page object (same as in all pages response) |
progress.current |
Current page number |
progress.total |
Total number of pages |
progress.nextPage |
Next page number (null if last) |
progress.isLast |
Whether this is the last page |
progress.isCompleted |
Whether all pages are completed |
8. Get Current Page
Purpose: Get the next incomplete onboarding page.
Endpoint: GET {base_url}/onboarding/pages?current=true
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Success Response JSON Sample (when pages remain):
{
"success": true,
"httpStatus": "OK",
"message": "Current page retrieved",
"action_time": "2025-01-02T10:30:45",
"data": {
"page": {
"id": "550e8400-e29b-41d4-a716-446655440002",
"pageOrder": 2,
"categoryKey": "industries",
"title": "Select your industries",
"description": "Choose the industries you work in",
"bannerImages": [],
"isSkippable": true,
"minSelections": 0,
"maxSelections": 10,
"options": [...],
"isCompleted": false
},
"progress": {
"current": 2,
"total": 3,
"nextPage": 3,
"isLast": false,
"isCompleted": false
}
}
}
Success Response JSON Sample (all pages completed):
{
"success": true,
"httpStatus": "OK",
"message": "Current page retrieved",
"action_time": "2025-01-02T10:30:45",
"data": {
"page": null,
"progress": {
"current": 3,
"total": 3,
"nextPage": null,
"isLast": true,
"isCompleted": true
}
}
}
9. Submit Page Response
Purpose: Submit user's selected options for an onboarding page.
Endpoint: POST {base_url}/onboarding/pages/{pageId}/response
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be application/json |
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
pageId |
UUID | Yes | Page identifier |
Request JSON Sample:
{
"selectedOptions": ["technology", "finance", "health"]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
selectedOptions |
string[] | Yes | Array of selected option keys | Must meet min/max selection rules |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Response saved",
"action_time": "2025-01-02T10:30:45",
"data": {
"saved": true,
"progress": {
"current": 1,
"total": 3,
"nextPage": 2,
"isLast": false,
"isCompleted": false
}
}
}
Success Response Fields:
| Field | Description |
|---|---|
saved |
Always true on success |
progress.current |
Current page number |
progress.total |
Total pages |
progress.nextPage |
Next page to complete (null if done) |
progress.isLast |
Whether current was last page |
progress.isCompleted |
Whether all pages are now completed |
Error Response JSON Samples:
Too Few Selections (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Minimum selections not met",
"action_time": "2025-01-02T10:30:45",
"data": "Minimum 1 selection(s) required"
}
Too Many Selections (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Maximum selections exceeded",
"action_time": "2025-01-02T10:30:45",
"data": "Maximum 5 selection(s) allowed"
}
Invalid Option (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid option",
"action_time": "2025-01-02T10:30:45",
"data": "Invalid option: unknown_key"
}
Page Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Page not found",
"action_time": "2025-01-02T10:30:45",
"data": "Page not found"
}
10. Skip Page
Purpose: Skip an optional onboarding page.
Endpoint: POST {base_url}/onboarding/pages/{pageId}/skip
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
pageId |
UUID | Yes | Page identifier |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page skipped",
"action_time": "2025-01-02T10:30:45",
"data": {
"saved": true,
"progress": {
"current": 2,
"total": 3,
"nextPage": 3,
"isLast": false,
"isCompleted": false
}
}
}
Error Response JSON Sample:
Page Not Skippable (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Page cannot be skipped",
"action_time": "2025-01-02T10:30:45",
"data": "This page cannot be skipped"
}
Admin Endpoints
11. Get All Pages (Admin)
Purpose: Retrieve all onboarding pages including inactive ones for admin management.
Endpoint: GET {base_url}/onboarding/pages/manage
Access Level: 🔒 Protected (Requires ROLE_MODERATOR, ROLE_ADMIN, or ROLE_SUPER_ADMIN)
Authentication: Bearer Token
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Pages retrieved",
"action_time": "2025-01-02T10:30:45",
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"pageOrder": 1,
"categoryKey": "interests",
"title": "What are your interests?",
"description": "Select the areas you're most interested in",
"bannerImages": [],
"isSkippable": false,
"minSelections": 1,
"maxSelections": 5,
"options": [...],
"isCompleted": false
}
]
}
12. Create Page (Admin)
Purpose: Create a new onboarding page with multilingual support.
Endpoint: POST {base_url}/onboarding/pages/manage
Access Level: 🔒 Protected (Requires ROLE_MODERATOR, ROLE_ADMIN, or ROLE_SUPER_ADMIN)
Authentication: Bearer Token
Request JSON Sample:
{
"pageOrder": 4,
"categoryKey": "experience_level",
"isSkippable": true,
"minSelections": 1,
"maxSelections": 1,
"translations": {
"en": {
"title": "What's your experience level?",
"description": "Help us understand your professional background"
},
"sw": {
"title": "Kiwango chako cha uzoefu ni kipi?",
"description": "Tusaidie kuelewa historia yako ya kitaaluma"
}
},
"bannerImages": [
"https://cdn.fursahub.com/onboarding/experience-banner.jpg"
],
"options": [
{
"key": "student",
"icon": "🎓",
"translations": {
"en": "Student",
"sw": "Mwanafunzi"
}
},
{
"key": "entry_level",
"icon": "🌱",
"translations": {
"en": "Entry Level (0-2 years)",
"sw": "Kiwango cha Mwanzo (miaka 0-2)"
}
},
{
"key": "mid_level",
"icon": "📈",
"translations": {
"en": "Mid Level (3-5 years)",
"sw": "Kiwango cha Kati (miaka 3-5)"
}
},
{
"key": "senior",
"icon": "⭐",
"translations": {
"en": "Senior (6+ years)",
"sw": "Mkuu (miaka 6+)"
}
}
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
pageOrder |
integer | Yes | Display order | Min: 1 |
categoryKey |
string | Yes | Unique category identifier | Not blank |
isSkippable |
boolean | No | Whether page can be skipped | Default: false |
minSelections |
integer | No | Minimum required selections | Min: 0 |
maxSelections |
integer | No | Maximum allowed selections | Min: 1 |
translations |
object | Yes | Language translations map | At least en required |
translations.{lang}.title |
string | Yes | Page title in language | Not blank |
translations.{lang}.description |
string | No | Page description | - |
bannerImages |
string[] | No | Array of banner image URLs | - |
options |
array | Yes | Array of option objects | At least 1 option |
options[].key |
string | Yes | Unique option identifier | Not blank |
options[].icon |
string | No | Icon (emoji or URL) | - |
options[].translations |
object | Yes | Option label translations | At least en required |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "CREATED",
"message": "Page created",
"action_time": "2025-01-02T10:30:45",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440004",
"pageOrder": 4,
"categoryKey": "experience_level",
"title": "What's your experience level?",
"description": "Help us understand your professional background",
"bannerImages": ["https://cdn.fursahub.com/onboarding/experience-banner.jpg"],
"isSkippable": true,
"minSelections": 1,
"maxSelections": 1,
"options": [
{"key": "student", "label": "Student", "icon": "🎓"},
{"key": "entry_level", "label": "Entry Level (0-2 years)", "icon": "🌱"},
{"key": "mid_level", "label": "Mid Level (3-5 years)", "icon": "📈"},
{"key": "senior", "label": "Senior (6+ years)", "icon": "⭐"}
],
"isCompleted": false
}
}
13. Update Page (Admin)
Purpose: Update an existing onboarding page.
Endpoint: PUT {base_url}/onboarding/pages/manage/{pageId}
Access Level: 🔒 Protected (Requires ROLE_MODERATOR, ROLE_ADMIN, or ROLE_SUPER_ADMIN)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
pageId |
UUID | Yes | Page identifier |
Request body same as Create Page
14. Delete Page (Admin)
Purpose: Delete an onboarding page.
Endpoint: DELETE {base_url}/onboarding/pages/manage/{pageId}
Access Level: 🔒 Protected (Requires ROLE_MODERATOR, ROLE_ADMIN, or ROLE_SUPER_ADMIN)
Authentication: Bearer Token
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
pageId |
UUID | Yes | Page identifier |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page deleted",
"action_time": "2025-01-02T10:30:45",
"data": null
}
15. Activate/Deactivate Page (Admin)
Purpose: Toggle page active status.
Endpoint (Activate): PATCH {base_url}/onboarding/pages/manage/{pageId}/activate
Endpoint (Deactivate): PATCH {base_url}/onboarding/pages/manage/{pageId}/deactivate
Access Level: 🔒 Protected (Requires ROLE_MODERATOR, ROLE_ADMIN, or ROLE_SUPER_ADMIN)
Authentication: Bearer Token
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Page activated",
"action_time": "2025-01-02T10:30:45",
"data": null
}
16. Reorder Pages (Admin)
Purpose: Change the display order of onboarding pages.
Endpoint: PATCH {base_url}/onboarding/pages/manage/reorder
Access Level: 🔒 Protected (Requires ROLE_MODERATOR, ROLE_ADMIN, or ROLE_SUPER_ADMIN)
Authentication: Bearer Token
Request JSON Sample:
{
"pageIds": [
"550e8400-e29b-41d4-a716-446655440003",
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002"
]
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
pageIds |
UUID[] | Yes | Ordered array of page IDs | Not empty |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Pages reordered",
"action_time": "2025-01-02T10:30:45",
"data": null
}
Standard Error Types
Application-Level Exceptions (400-499)
| Status Code | HTTP Status | When It Occurs |
|---|---|---|
| 400 | BAD_REQUEST | Invalid input, selection rules violated, page not skippable |
| 401 | UNAUTHORIZED | Token empty, invalid, or expired |
| 403 | FORBIDDEN | OTP invalid/expired, insufficient permissions |
| 404 | NOT_FOUND | Page or resource not found |
| 409 | CONFLICT | Phone already registered |
| 422 | UNPROCESSABLE_ENTITY | Validation errors |
| 429 | TOO_MANY_REQUESTS | Rate limit or cooldown active |
Server-Level Exceptions (500+)
| Status Code | HTTP Status | When It Occurs |
|---|---|---|
| 500 | INTERNAL_SERVER_ERROR | Database errors, SMS gateway failures |
Frontend Implementation Guide
Complete Onboarding Flow
const completeOnboarding = async () => {
// Step 1: Check current progress
const progress = await getProgress();
switch (progress.currentStage) {
case 'PENDING_PHONE_VERIFICATION':
navigation.navigate('PhoneVerification');
break;
case 'PENDING_PREFERENCES':
navigation.navigate('OnboardingPages');
break;
case 'PENDING_PROFILE_COMPLETION':
navigation.navigate('ProfileCompletion');
break;
case 'COMPLETED':
navigation.navigate('Home');
break;
}
};
Quick Reference
Onboarding Status Flow
PENDING_PHONE_VERIFICATION → PENDING_PREFERENCES → PENDING_PROFILE_COMPLETION → COMPLETED
OTP Configuration
| Setting | Value |
|---|---|
| OTP Length | 6 digits |
| Expiry | 10 minutes |
| Max Attempts | 3 |
| Resend Cooldown | 2 minutes |
| Rate Limit | 3 requests per 10 minutes |
Supported Languages
| Code | Name | Native Name |
|---|---|---|
en |
English | English |
sw |
Swahili | Kiswahili |
fr |
French | Français |
zh |
Chinese | 中文 |
Admin Roles for Page Management
| Role | Can Manage Pages |
|---|---|
ROLE_USER |
❌ No |
ROLE_MODERATOR |
✅ Yes |
ROLE_ADMIN |
✅ Yes |
ROLE_SUPER_ADMIN |
✅ Yes |