Onboarding API
Everything a client needs to take a freshly registered user through onboarding β the role choice, the questionnaire payload, and (for mentors) the verification document step.
Onboarding is what makes someone a mentor: there is no separate mentor application in this flow.
Submitting role: "mentor" assigns the mentor role and creates the mentor profile that the
verification documents hang off.
- Base URL:
/api/v1 - Auth: the question set is public; submitting and reading a submission require a Sanctum bearer
token β
Authorization: Bearer {token}β and a completed 2FA challenge (2fa.completed) - One submission per user per role. A user may onboard as both
menteeandmentor; the mentor role is additive and never removes the mentee role.
Response envelope
Success
{ "status": "success", "message": "β¦", "data": { } }
Error
{ "status": "error", "message": "β¦", "data": { } }
Status codes
| Code | Meaning |
|---|---|
| 200 | OK (questions, show) |
| 201 | Onboarding submitted, verification document uploaded |
| 401 | Missing/invalid token |
| 404 | No onboarding record for that role |
| 422 | Validation error, invalid role, or already onboarded for that role |
| 500 | Server error |
The flow
1. POST /api/v1/register β user + token
2. POST /api/v1/verify-email
3. GET /api/v1/onboarding/questions/{role} (optional β the client may use its own steps)
4. POST /api/v1/onboarding role=mentee | mentor
ββ role=mentor β mentor role assigned + mentor profile created (pending)
5. POST /api/v1/mentor/verification-documents mentors only β KYC upload
6. PUT /api/v1/mentor/profile mentors only β legal name, languages, expertiseβ¦
Step 5 is not required to finish onboarding. is_onboarded (returned on GET /api/v1/me and
in the auth payloads) flips to true at step 4, so the client can drop the user into the app right
away; the mentor profile stays pending until an admin approves it, and mentors can upload β or
re-upload β documents at any point after step 4.
1. Onboarding questions
GET /api/v1/onboarding/questions/{role}
Public. role β mentor Β· mentee. Returns the canonical question set. Labels and options
differ per role (a mentor is asked what they can mentor in, a mentee what they want to learn).
200 Response
{
"status": "success",
"message": "Onboarding questions retrieved.",
"data": {
"role": "mentor",
"questions": [
{
"key": "experience_level",
"type": "single_select",
"label": "What is your experience level?",
"options": ["beginner", "intermediate", "advanced", "expert"],
"required": true
}
]
}
}
Question types: single_select Β· multi_select Β· number Β· text Β· textarea.
| Key | Type | Notes |
|---|---|---|
goals |
multi_select | Options differ by role |
experience_level |
single_select | beginner Β· intermediate Β· advanced Β· expert |
preferred_communication |
single_select | video Β· audio Β· chat |
availability |
single_select | weekdays Β· weekends Β· both Β· flexible |
weekly_hours |
number | 1β40 |
topics_of_interest |
multi_select | Options differ by role |
how_did_you_hear |
text | Optional |
expectations |
textarea | Optional |
The current onboarding UI hides these questionnaire steps and collects the profile, social and interests steps instead. The endpoint and its validation rules are kept intact so a step can be switched back on without an API change β see the note on
responsesbelow.
Errors: 422 "Invalid role. Must be mentor or mentee." Β· 500 "Failed to retrieve onboarding questions."
1b. Catalogues the wizard renders
Both are public β the steps render before there is anything user-specific to render against.
GET /api/v1/interests β { "data": { "groups": [ { "label", "interests": [ { id, slug, name } ] } ] } }
GET /api/v1/public/expertise-areas β { "data": { "expertise_areas": [ β¦ ] } }
GET /interests is the suggested vocabulary for responses.interests / user.interests; send the
name back, not the slug, and note it is not a closed set β an interest outside the catalogue
still saves. Full reference: auth-api.md endpoint 39b.
GET /public/expertise-areas is an alias of GET /api/v1/mentor/available-expertise-areas, which
carries no role gate and never did β a mentee may call either. The alias exists so the step,
which runs for both roles, need not call a mentor-prefixed URL.
2. Submit onboarding
POST /api/v1/onboarding
Bearer (2fa.completed). Marks onboarding complete for the given role.
| Field | Type | Required | Notes |
|---|---|---|---|
role |
enum | yes | mentor Β· mentee |
responses |
object | yes | Answer map β may hold any subset of the keys below |
responses.interests |
string[] | no | 3β10 items, each β€ 100 β the "What are you interested in?" step. Also mirrored onto user.interests |
responses.goals |
string[] | no | β₯ 1 item, each β€ 255 |
responses.experience_level |
enum | no | beginner Β· intermediate Β· advanced Β· expert |
responses.preferred_communication |
enum | no | video Β· audio Β· chat |
responses.availability |
enum | no | weekdays Β· weekends Β· both Β· flexible |
responses.weekly_hours |
int | no | 1β40 |
responses.topics_of_interest |
string[] | no | 1β10 items, each β€ 100 |
responses.how_did_you_hear |
string | no | β€ 255 |
responses.expectations |
string | no | β€ 1000 |
responses.certifications |
object[] | no | β€ 10 β same shape as PUT /profile |
responses.education |
object[] | no | β€ 20 β same shape as PUT /profile |
responses.experiences |
object[] | no | β€ 20 β same shape as PUT /profile |
responses.expertise_areas |
object[] | no | β€ 10 β { id, skills[] }, catalogue ids |
Every answer is optional individually β responses itself is required and must be an object β
but anything that is sent still has to be well-formed. This is what lets the UI ship a different
step order without breaking the contract. The required: true flags returned by
GET /onboarding/questions/{role} describe the intended UI; they are not enforced here.
A key with no rule is dropped, not passed through.
responsesis validated as an array with validated children, so Laravel strips any child that has no rule of its own. That is how the four credential collections were silently discarded before they were added to the table above β an unlisted key never reaches the service, and no error is raised. Anything new the wizard starts sending insideresponsesneeds a rule here first.
Request
{
"role": "mentor",
"responses": {
"interests": ["Web Development", "DevOps", "Leadership"]
}
}
201 Response
{
"status": "success",
"message": "Onboarding completed successfully.",
"data": {
"onboarding": {
"id": "9b6fβ¦",
"role": "mentor",
"is_completed": true,
"responses": { "interests": ["Web Development", "DevOps", "Leadership"] }
}
}
}
Side effect for both roles β inside the same transaction as the questionnaire:
- The four credential collections β
certifications,education,experiencesandexpertise_areasβ are written to the user's own rows through the same servicePUT /api/v1/profileuses, and keep its semantics exactly: a key that is present is the complete list (rows with anidupdated, rows without one created, anything omitted deleted), and a key that is absent leaves that collection untouched. So a questionnaire-only submission never wipes what a per-step save already stored. Ids are scoped to the caller's own rows; naming someone else's is a422. The full field-by-field rules are in profile-api-migration.md. - When
responses.interestsis present it is also written tousers.interests, so the rest of the API can read a person's interests without going through their questionnaire. The answer map keeps its own copy; this is a second home, not a move. A user who onboards as both a mentor and a mentee has one list on the user, so the later submission wins. Interests stay editable afterwards throughPUT /api/v1/profile.
Side effects for role: "mentor" β both inside the same transaction as the questionnaire:
- The
mentorrole is assigned (the existingmenteerole is kept). - A mentor profile is created with
approval_status: "pending"and the user's timezone, if the user does not already have one. Its legal identity fields (legal_name,legal_birth_year,legal_profession,languages) are left empty on purpose β the mentor fills them in later viaPUT /api/v1/mentor/profile. A user who already has a profile (e.g. from the legacyPOST /api/v1/mentor/registerapplication) keeps it untouched.
The profile is what unlocks POST /api/v1/mentor/verification-documents; before onboarding as a
mentor, that endpoint answers 404 "Mentor profile not found."
Errors: 422 validation, or "You have already completed onboarding for this role." Β· 500
"Failed to submit onboarding."
3. Show onboarding
GET /api/v1/onboarding/{role}
Bearer (2fa.completed). role β mentor Β· mentee. Returns the stored submission so the
client can resume or display it.
200 Response
{
"status": "success",
"message": "Onboarding retrieved.",
"data": {
"onboarding": { "id": "9b6fβ¦", "role": "mentor", "is_completed": true, "responses": { } }
}
}
Errors: 422 invalid role Β· 404 "No onboarding found for this role." Β· 500 "Failed to retrieve onboarding."
4. Mentor verification documents
Part of the mentor onboarding journey, but served by the mentor module. Full reference: Mentor Verification Document API.
POST /api/v1/mentor/verification-documents multipart/form-data
Bearer. Requires a mentor profile β that is, mentor onboarding must have been submitted first.
| Field | Type | Required | Notes |
|---|---|---|---|
document_type |
enum | yes | id_card Β· passport Β· driving_license |
documents |
file[] | yes | 1β5 files, pdf/jpg/jpeg/png, β€ 5 MB each |
Send several files under one type when a document has more than one page β the front and back of an ID card, for instance:
POST /api/v1/mentor/verification-documents
Content-Type: multipart/form-data
document_type=id_card
documents[][email protected]
documents[][email protected]
201 Response
{
"status": "success",
"message": "Verification documents uploaded successfully.",
"data": {
"verification_documents": [
{
"id": "9b6fβ¦",
"documentType": "id_card",
"documentTypeLabel": "National ID Card",
"originalName": "id-front.jpg",
"status": "pending",
"statusLabel": "Pending Review",
"adminNotes": null,
"downloadUrl": "https://api.example.com/api/v1/mentor/verification-documents/9b6fβ¦/download"
}
]
}
}
Uploads start as pending; an admin approves or rejects each one and the mentor is notified.
A pending or rejected document can be replaced (POST β¦/verification-documents/{id}) or
deleted; an approved type is locked. Files live on a private disk and are only readable through
the download endpoint.
Errors: 401 unauthenticated Β· 404 "Mentor profile not found." (onboarding not submitted)
Β· 422 validation, or the type is already approved Β· 500 "Failed to upload verification document."
Related
- Authentication API β registration, email verification, 2FA, and the
is_onboardedflag on the user payload - Mentor Verification Document API β list, replace, download, delete
- Admin Verification Document API β the review side