Skip to main content

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: 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"
  }
}

Admin: Manage Onboarding Pages

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


12. Get All Pages (Admin)

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

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

Access Level: 🔒 Protected (Admin/Moderator)

Authentication: Bearer Token (ROLE_ADMIN, ROLE_SUPER_ADMIN, ROLE_MODERATOR)

Success Response JSON Sample:

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

13. Get Page by ID (Admin)

Purpose: Get single page details for editing.

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

Access Level: 🔒 Protected (Admin/Moderator)

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

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


14. Create Page

Purpose: Create a new onboarding preference page.

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

Access Level: 🔒 Protected (Admin/Moderator)

Authentication: Bearer Token

Request JSON Sample:

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

Request Body Parameters:

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

Success Response JSON Sample:

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

Error Responses:

Duplicate Category Key (400):

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

15. Update Page

Purpose: Update an existing onboarding page.

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

Access Level: 🔒 Protected (Admin/Moderator)

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

Request JSON Sample: Same as Create Page

Success Response JSON Sample:

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

16. Delete Page

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

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

Access Level: 🔒 Protected (Admin/Moderator)

Path Parameters:

Parameter Type Required Description
pageId UUID Yes Page identifier

Success Response JSON Sample:

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

17. Activate Page

Purpose: Activate a deactivated page.

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

Access Level: 🔒 Protected (Admin/Moderator)

Success Response JSON Sample:

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

18. Deactivate Page

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

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

Access Level: 🔒 Protected (Admin/Moderator)

Success Response JSON Sample:

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

19. Reorder Pages

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

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

Access Level: 🔒 Protected (Admin/Moderator)

Request JSON Sample:

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

Request Body Parameters:

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

Success Response JSON Sample:

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

Admin Panel Implementation Guide

Page List View

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

Create/Edit Page Form

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

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

Quick Actions

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

Frontend Implementation Guide

Phone Verification Screen

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

Preference Pages Loop

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

Progress Indicator

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