# PONA Auth Secondary Onboarding

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-04-27  
**Version**: v1.0

**Base URL**: `https://api.nexgate.co/api/v1/onboarding/secondary`

**Short Description**: Secondary onboarding completes a user's profile after the primary onboarding step (name, phone, birth date). It is a step-machine — each endpoint returns the next missing step and a refreshed access token. The flow is complete when `stepsRemaining` reaches `0` and `nextMissing` is `null`.

**Hints**:
- All endpoints require a valid `Bearer` access token issued after primary onboarding or login.
- Every response includes a fresh `accessToken` with updated onboarding flags embedded in the JWT claims — replace the stored token after each step.
- Steps can be completed in any order. The `nextMissing` field signals the next recommended step; it does not enforce ordering.
- Email linking has two independent paths: **custom** (user-provided email + OTP) and **Google** (Google ID token). Only one path needs to be completed.
- Profile picture upload expects `multipart/form-data`, not JSON.

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "action": "COLLECT_EMAIL",
  "data": {}
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, BAD_REQUEST, etc.) |
| `message` | string | Human-readable result description |
| `action_time` | string | ISO 8601 timestamp of the response |
| `action` | string | Next frontend action to perform (see action codes below) |
| `data` | object/string | Response payload or error detail |

### Action Codes
| Code | Meaning |
|------|---------|
| `COLLECT_USERNAME` | Username step is next |
| `COLLECT_EMAIL` | Email step is next |
| `COLLECT_PROFILE_PIC` | Profile picture step is next |
| `COLLECT_INTERESTS` | Interests step is next |
| `COLLECT_BIO` | Bio step is next |
| `PROCEED` | All steps complete — onboarding is done |

---

## Standard Secondary Onboarding Response Fields

Most endpoints return a `SecondaryOnboardingResponse`. Its fields are:

| Field | Type | Description |
|-------|------|-------------|
| `accessToken` | string | Fresh JWT — store and use this for all subsequent requests |
| `onboarding` | object | Current completion state of all onboarding steps (see below) |
| `nextMissing` | string | Key name of the next incomplete step, or `null` if all done |
| `stepsRemaining` | integer | Number of steps still pending |

### `onboarding` Object Fields
| Field | Type | Description |
|-------|------|-------------|
| `primaryComplete` | boolean | Primary onboarding (name/phone/DOB) is done |
| `username` | boolean | Username has been set |
| `email` | boolean | Email has been linked and verified |
| `profilePic` | boolean | Profile picture has been uploaded |
| `interests` | boolean | Interests have been selected |
| `bio` | boolean | Bio has been written |

---

## Endpoints

## 1. Get Username Suggestions

**Purpose**: Returns up to 5 AI-generated username suggestions based on the user's first name, last name, and birth date.

**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}/username/suggestions`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Username suggestions",
  "action_time": "2026-04-27T10:30:45",
  "data": {
    "suggestions": [
      "john_sakweli",
      "johnsakweli99",
      "j_sakweli",
      "johnsak2004",
      "jsakweli_"
    ]
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `suggestions` | Array of up to 5 available username strings |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Account not found",
  "action_time": "2026-04-27T10:30:45",
  "data": "Account not found"
}
```

---

## 2. Set Username

**Purpose**: Sets a unique username for the account.

**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}/username`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "username": "john_sakweli"
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `username` | string | Yes | Desired username | Min: 3, Max: 30 characters. Must start with a letter. Only letters, numbers, and underscores allowed. |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Username set successfully",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_EMAIL",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": false,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "nextMissing": "email",
    "stepsRemaining": 4
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `accessToken` | Fresh JWT with updated onboarding claims — replace stored token |
| `onboarding.username` | Now `true` |
| `nextMissing` | Next recommended step key |
| `stepsRemaining` | Steps left to complete |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Username is already taken",
  "action_time": "2026-04-27T10:30:45",
  "data": "Username is already taken"
}
```

**Standard Error Types**:
- `400 BAD_REQUEST`: Username already taken or invalid format
- `401 UNAUTHORIZED`: Missing or invalid token
- `422 UNPROCESSABLE_ENTITY`: Validation failure (length, pattern)

---

## 3. Set Bio

**Purpose**: Saves a short bio to the user's profile.

**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}/bio`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "bio": "Event enthusiast, live music lover, always at the front row."
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `bio` | string | Yes | User's short profile bio | Max: 160 characters. Cannot be blank. |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Bio saved",
  "action_time": "2026-04-27T10:30:45",
  "action": "PROCEED",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": true,
      "interests": true,
      "bio": true
    },
    "nextMissing": null,
    "stepsRemaining": 0
  }
}
```

**Standard Error Types**:
- `400 BAD_REQUEST`: Bio is blank
- `401 UNAUTHORIZED`: Missing or invalid token
- `422 UNPROCESSABLE_ENTITY`: Exceeds 160 characters

---

## 4. Set Interests

**Purpose**: Saves the user's selected interest categories (minimum 3 required).

**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}/interests`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "interestIds": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "7a1234bc-1234-4321-abcd-1234567890ab",
    "9c87654d-4321-1234-dcba-0987654321cd"
  ]
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `interestIds` | array of UUID | Yes | IDs of selected interest categories | Min: 3 items. Must not be empty. Use the interests listing endpoint to get valid IDs. |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Interests saved",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_BIO",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": true,
      "interests": true,
      "bio": false
    },
    "nextMissing": "bio",
    "stepsRemaining": 1
  }
}
```

**Standard Error Types**:
- `400 BAD_REQUEST`: Interest IDs not found
- `401 UNAUTHORIZED`: Missing or invalid token
- `422 UNPROCESSABLE_ENTITY`: Fewer than 3 interests selected

---

## 5. Upload Profile Picture

**Purpose**: Uploads and stores the user's profile picture.

**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-pic`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `multipart/form-data` |

**Query Parameters**:
| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `file` | file (form field) | Yes | Image file to upload | Image formats: JPEG, PNG, WEBP | — |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Profile picture uploaded",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_INTERESTS",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": true,
      "interests": false,
      "bio": false
    },
    "nextMissing": "interests",
    "stepsRemaining": 2
  }
}
```

**Standard Error Types**:
- `400 BAD_REQUEST`: File is missing, empty, or unsupported format
- `401 UNAUTHORIZED`: Missing or invalid token
- `500 INTERNAL_SERVER_ERROR`: Storage failure

---

## 6. Initiate Email Linking (Custom)

**Purpose**: Sends a 6-digit OTP to the provided email address and returns a `tempToken` required for the verify step.

**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}/email/custom/initiate`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "email": "john@example.com"
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `email` | string | Yes | Email address to link | Must be a valid email format. Cannot be blank. |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Verification code sent to your email",
  "action_time": "2026-04-27T10:30:45",
  "data": {
    "tempToken": "eyJhbGciOiJSUzI1NiJ9.temp...",
    "nextAction": "VERIFY_EMAIL"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `tempToken` | Short-lived token required as input to the verify step. Store this until OTP is submitted. |
| `nextAction` | Always `VERIFY_EMAIL` — signals frontend to show OTP input screen |

**Standard Error Types**:
- `400 BAD_REQUEST`: Email already in use by another account
- `401 UNAUTHORIZED`: Missing or invalid token
- `422 UNPROCESSABLE_ENTITY`: Invalid email format

---

## 7. Verify Email (Custom)

**Purpose**: Confirms the OTP sent to the user's email and marks the email step as complete.

**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}/email/custom/verify`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "tempToken": "eyJhbGciOiJSUzI1NiJ9.temp...",
  "otp": "847291"
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `tempToken` | string | Yes | Token received from the initiate step | Cannot be blank |
| `otp` | string | Yes | 6-digit code sent to the user's email | Exactly 6 digits, numeric only |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Email verified",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_PROFILE_PIC",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "nextMissing": "profilePic",
    "stepsRemaining": 3
  }
}
```

**Standard Error Types**:
- `400 BAD_REQUEST`: OTP is incorrect or expired
- `401 UNAUTHORIZED`: `tempToken` is invalid or expired
- `422 UNPROCESSABLE_ENTITY`: OTP is not 6 numeric digits

---

## 8. Link Email via Google

**Purpose**: Links and verifies a Google-backed email using a Google ID token — skips the OTP step entirely.

**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}/email/google`

**Access Level**: 🔒 Protected (Authenticated user)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <access_token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:
```json
{
  "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `idToken` | string | Yes | Google ID token from the Google Sign-In SDK | Cannot be blank. Must be a valid Google-issued token. |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Email linked via Google",
  "action_time": "2026-04-27T10:30:45",
  "action": "COLLECT_PROFILE_PIC",
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiJ9...",
    "onboarding": {
      "primaryComplete": true,
      "username": true,
      "email": true,
      "profilePic": false,
      "interests": false,
      "bio": false
    },
    "nextMissing": "profilePic",
    "stepsRemaining": 3
  }
}
```

**Standard Error Types**:
- `400 BAD_REQUEST`: Google token is invalid, expired, or email already linked to another account
- `401 UNAUTHORIZED`: Missing or invalid Bearer token
- `422 UNPROCESSABLE_ENTITY`: `idToken` is blank

---

## Flow Overview

```
Primary Onboarding Complete
           │
           ▼
   ┌───────────────┐
   │  Set Username  │  POST /username
   └───────┬───────┘
           │
           ▼
   ┌───────────────┐         ┌──────────────────────┐
   │  Link Email    │─────────│ Option A: Custom      │  POST /email/custom/initiate
   └───────┬───────┘         │  → POST /email/custom/verify
           │                 ├──────────────────────┤
           │                 │ Option B: Google      │  POST /email/google
           │                 └──────────────────────┘
           ▼
   ┌───────────────┐
   │  Profile Pic  │  POST /profile-pic
   └───────┬───────┘
           │
           ▼
   ┌───────────────┐
   │   Interests   │  POST /interests  (min. 3)
   └───────┬───────┘
           │
           ▼
   ┌───────────────┐
   │     Bio       │  POST /bio
   └───────┬───────┘
           │
           ▼
    action: PROCEED
   (onboarding done)
```

> Steps can be completed out of order. The `nextMissing` field in each response always signals the next recommended incomplete step.

---

## Quick Reference

| # | Endpoint | Method | Auth | Purpose |
|---|----------|--------|------|---------|
| 1 | `/username/suggestions` | GET | Bearer | Get suggested usernames |
| 2 | `/username` | POST | Bearer | Set username |
| 3 | `/bio` | POST | Bearer | Set bio |
| 4 | `/interests` | POST | Bearer | Set interests |
| 5 | `/profile-pic` | POST | Bearer | Upload profile picture |
| 6 | `/email/custom/initiate` | POST | Bearer | Send OTP to email |
| 7 | `/email/custom/verify` | POST | Bearer | Verify email with OTP |
| 8 | `/email/google` | POST | Bearer | Link email via Google token |