Authentication API
Everything under the Auth module β account registration, login, email verification, social (OAuth) login, password management, two-factor authentication (2FA), passkeys (WebAuthn), phone verification, onboarding, and profile management.
- Base URL:
/api/v1 - Auth: most write endpoints require a Sanctum bearer token β
Authorization: Bearer {token}. Public endpoints (register, login, email verification, OAuth, passkey authentication) are called first to obtain a token. Each endpoint below is tagged Public, Bearer, or Pending 2FA token. - Content type:
application/json(profile media and avatar/cover uploads usemultipart/form-data).
Two token states. A normal login returns a full token (ability
*). When the account has 2FA enabled, login instead returns a short-lived pending token (abilitytwo-factor-challenge, 10-minute expiry). A pending token can only reach the 2FA challenge endpoints β every route guarded by2fa.completedreturns 403 until the challenge is passed and a full token is issued.
Response envelope
Success { "status": "success", "message": "β¦", "data": { } }
Error { "status": "error", "message": "β¦", "data": { } }
Helper signatures: success_response(message, data = [], code = 200) and
error_response(message, code = 500, data = [], exception = null). Endpoints that pass no data
return "data": {} (or []).
Form-request validation failures are Laravel's default 422 shape β
{ "message": "β¦", "errors": { "field": ["β¦"] } } β not the custom envelope above.
Status codes
| Code | Meaning |
|---|---|
| 200 | OK (login, verify, read, mutation) |
| 201 | Resource created (register, onboarding submit, passkey register) |
| 401 | Invalid credentials (login only) / missing token |
| 403 | Pending 2FA token used on a 2fa.completed-guarded route |
| 404 | Onboarding record not found |
| 422 | Validation error, or a business rule rejected via InvalidArgumentException |
| 429 | Throttled (resend email verification, switch 2FA method) |
| 500 | Server error |
Error-mapping quirk. Business errors are thrown as
InvalidArgumentExceptionin the service and mapped to a status per controller method β usually 422, but login maps them to 401 and resend-email-verification to 429. A few paths surface as a generic 500 where a more specific code would be expected (see the notes on Get TOTP QR code, Enable/disable a method withbackup_code, Revoke a trusted device, and Delete a passkey).
Enums
Two-factor method (TwoFactorMethod, string-backed):
| Value | Label |
|---|---|
email |
|
sms |
SMS |
totp |
Authenticator App |
backup_code |
Backup Code |
backup_codepassesin:validation but cannot be selected as a primary/default method β it is a fallback only.
Gender (Gender, integer-backed):
| Value | Label |
|---|---|
1 |
Male |
2 |
Female |
3 |
Other |
4 |
Prefer not to say |
Stored on the user, nullable, and exposed on the user object only β never on the nested
mentor_profile/mentee_profile.nullmeans the user has never told us β distinct from4(Prefer not to say), which is an explicit choice to withhold it. Wherevergenderappears it is paired with a human-readablegender_label, bothnullwhen undeclared. The same values are accepted by the mentee-facing mentor filter (GET /api/v1/mentee/mentors?gender=2).
Onboarding role: mentor Β· mentee
Onboarding question types: single_select Β· multi_select Β· number Β· text Β· textarea
Shared objects
User object (UserResource)
Returned by register, login, update profile, and upload profile media. GET /me
uses the richer public profile object below instead.
{
"id": "β¦",
"full_name": "John Doe",
"username": "john_doe",
"email": "john@example.com",
"avatar_url": null,
"cover_photo_url": null,
"bio": null,
"gender": 1,
"gender_label": "Male",
"social_links": { "facebook": "β¦", "instagram": null, "x": null, "linkedin": "β¦" },
"address": { "address": "12 Gulshan Ave", "city": "Dhaka", "state": "Dhaka", "country": "Bangladesh" },
"timezone": "UTC",
"is_active": true,
"email_verified_at": "2026-01-01T12:00:00+00:00",
"has_password": true,
"two_factor_enabled": false,
"default_two_factor_method": "email",
"enabled_two_factor_methods": [],
"phone_number": null,
"phone_verified_at": null,
"roles": ["mentee"],
"mentor_profile": { "β¦MentorProfileResourceβ¦" },
"mentee_profile": { "β¦MenteeProfileResourceβ¦" },
"created_at": "β¦",
"updated_at": "β¦"
}
rolesis present only when therolesrelation is loaded (value is the array of role names).mentor_profile/mentee_profileappear only when their relations are eager-loaded.social_linksis free-form JSON stored on the user;addressappears only when theaddressrelation is loaded, and isnulluntil the user saves one. Both live on the user, not on the role profiles β see Address & social links below.default_two_factor_methodfalls back to"email"when unset.has_passwordisfalsefor OAuth-only accounts that have never set a password β use it to decide whether to prompt for set password (nocurrent_password) vs change password.
Public profile object (UserProfileResource)
Returned by View a user profile (endpoint 41), and by Current user (me) (endpoint 8) with
the account-only fields appended. This is the cross-user view β on another user's profile it never
carries email, 2FA state, phone number, or the mentor's legal/verification fields.
{
"id": "019f5f0a-d32a-73f3-aa86-45e916ecb015",
"username": "david_chen",
"name": "David Chen",
"bio": "Senior engineer helping mentees grow their careers.",
"gender": 1,
"gender_label": "Male",
"avatar_url": "https://β¦/storage/2/6fbaebcf-β¦.png",
"cover_photo_url": null,
"timezone": "UTC",
"languages": ["English"],
"location": {
"address": "400 S Hope St",
"city": "Los Angeles",
"state": "California",
"country": "United States",
"country_code": "US"
},
"social_links": {
"facebook": "https://facebook.com/david_chen",
"instagram": null,
"x": "https://x.com/david_chen",
"linkedin": "https://linkedin.com/in/david_chen"
},
"connections_count": 1,
"posts_count": 15,
"connection_status": "pending_outgoing",
"is_connected": false,
"is_me": false,
"chat_thread": {
"id": "9c3f1b2a-6d4e-4f7a-9c1d-2e3f4a5b6c7d",
"status": 2,
"status_label": "Accepted",
"is_accepted": true,
"initiated_by_me": true
},
"role": "mentor",
"mentor_profile": { "β¦see belowβ¦" },
"mentee_profile": null
}
| Field | Notes |
|---|---|
name |
The user's full_name. |
gender / gender_label |
The Gender enum value and its label; both null when the user has not declared one. Lives on the user object only β the nested mentor_profile / mentee_profile do not carry it. |
avatar_url / cover_photo_url |
MediaLibrary URL, falling back to the stored column; null when neither is set. |
languages |
Stored on the mentor profile, lifted to user level here; [] for mentees. |
location |
The user's address, flattened. location.address is the street line. |
location.country_code |
ISO-2, resolved from the stored country name via the countries table; null when unresolvable. |
social_links |
Stored on the user. |
social_links |
Always the four keys facebook Β· instagram Β· x Β· linkedin; missing networks are null. A legacy twitter key is read into x. |
connection_status |
The caller's relationship to this user: none Β· pending_outgoing Β· pending_incoming Β· connected. Always none on your own profile. |
is_connected |
Shorthand for connection_status == "connected". |
chat_thread |
The caller's direct chat thread with this user, or null when they have never messaged each other (always null on your own profile). id is the thread/conversation id for the Messaging API (GET /api/v1/chat/threads/{thread}/messages). status is 1 Pending Β· 2 Accepted; is_accepted is the shorthand. initiated_by_me tells the caller whether they started the thread (a pending thread they did not initiate is one they can accept/decline). |
posts_count |
Count of the user's feed posts. |
role |
mentor Β· mentee Β· null. Mentor wins if the account somehow carries both. |
mentor_profile / mentee_profile |
Exactly one is populated, matching role; the other is null. |
mentor_profile (PublicMentorProfileResource)
{
"id": "019f5f0a-d333-70a2-8b8a-f4aae8cc6815",
"headline": "Senior Parts Salesperson at Graham-Legros",
"expertise_areas": [
{
"id": "019f5f0a-d339-724c-bdbe-0dacb01515e3",
"expertise_area_id": "019f5f0a-c952-7142-b795-46dea53cc6c4",
"name": "Funeral Board",
"skills": [{ "id": "019f5f0a-c954-71f1-9e61-832b857765e2", "name": "Embalming" }]
}
],
"years_of_experience": 8,
"hourly_rate": "50.00",
"approval_status": "approved",
"approval_status_label": "Approved",
"average_rating": "4.05",
"total_sessions": 19,
"timezone": "UTC",
"vacationMode": false,
"vacationEndsAt": null,
"experiences": [
{
"id": "β¦",
"organization": "Graham-Legros",
"designation": "Parts Salesperson",
"description": "Leading the parts sales team.",
"currently_working": true,
"start_date": "2021-03-01",
"end_date": null,
"created_at": "β¦",
"updated_at": "β¦"
}
],
"education": [
{
"id": "β¦",
"institution": "University of California",
"degree": "B.Sc. in Business Administration",
"currently_studying": false,
"start_date": "2013-09-01",
"end_date": "2017-06-30",
"created_at": "β¦",
"updated_at": "β¦"
}
],
"certifications": [
{
"id": "β¦",
"title": "AWS Solutions Architect",
"issuing_organization": "Amazon Web Services",
"issued_date": "2025-07-14",
"expiry_date": "2028-07-14",
"credential_url": "https://www.credly.com/example"
}
],
"created_at": "β¦",
"updated_at": "β¦"
}
Deliberately not included, unlike the mentor's own
MentorProfileResource:legal_name,legal_birth_year,legal_profession,company_or_institution,social_links,address, and verification documents.Types to watch:
hourly_rateandaverage_ratingaredecimal:2casts and serialise as strings ("50.00","4.05"), whileyears_of_experienceandtotal_sessionsare integers.approval_statusis the string-backedApprovalStatusenum βpendingΒ·approvedΒ·rejectedΒ·suspendedβ paired with a human-readableapproval_status_label.
mentee_profile (PublicMenteeProfileResource)
{
"id": "019f5f0a-a222-70a2-8b8a-f4aae8cc0002",
"profession": "Frontend Developer",
"company_or_institution": null,
"relationship_status": null,
"relationship_since": null,
"expertise_areas": [ "β¦same shape as the mentor'sβ¦" ],
"experiences": [ "β¦same shape as the mentor'sβ¦" ],
"education": [ "β¦same shape as the mentor'sβ¦" ],
"certifications": [],
"created_at": "β¦",
"updated_at": "β¦"
}
No
genderhere. Gender belongs to the user β readgender/gender_labelfrom the enclosing profile object, and write it through Update profile (endpoint 39). The permanentlynullgenderkey this object used to carry has been removed.Not yet backed by storage.
mentee_profileshas no columns forcompany_or_institution,relationship_status, orrelationship_since, and mentee certifications do not exist (mentor_certificationsis mentor-only). These fields are emitted asnull/[]so the response shape stays stable for clients, and will start returning real values once the columns land.professionmaps to the storedcurrent_role.
Passkey object (PasskeyResource)
{
"id": "β¦",
"device_name": "MacBook Pro",
"attestation_type": "none",
"last_used_at": "β¦",
"created_at": "β¦"
}
credential_id,public_key, andsign_countare never exposed.
Trusted-device object (TrustedDeviceResource)
{
"id": "β¦",
"device_name": "Chrome on Windows",
"ip_address": "203.0.113.4",
"last_used_at": "β¦",
"expires_at": "β¦",
"created_at": "β¦"
}
device_nameis parsed from the User-Agent. The raw token hash and user-agent are never exposed.
Core Authentication
1. Register
POST /api/v1/register β 201 { "data": { "user": { β¦ } } }
Public. Creates a mentee account, assigns the mentee role, creates an empty mentee profile,
and emails a verification link.
| Field | Type | Required | Notes |
|---|---|---|---|
full_name |
string | yes | β€ 255 |
email |
string | yes | Valid email, unique in users |
password |
string | yes | β₯ 8, requires matching password_confirmation |
password_confirmation |
string | yes | Must match password |
timezone |
string | no | Valid timezone, β€ 100 (default UTC) |
Message: "Registration successful. Please check your email for a verification link."
Errors: 422 validation (e.g. "This email is already registered."); 500 "Registration failed."
(wrapped in a DB transaction with rollback).
2. Login
POST /api/v1/login β 200
Public. Optionally reads header X-Trusted-Device: {token} to skip 2FA on a remembered device.
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | Valid email |
password |
string | yes | β |
No 2FA (or trusted device matched) β message "Login successful.":
{ "data": { "user": { β¦ }, "token": "9|plainTextToken", "requires_two_factor": false } }
2FA required β message "Two-factor authentication required.":
{ "data": { "requires_two_factor": true, "two_factor_method": "totp", "token": "8|pendingToken" } }
The token here is a pending token (10-min expiry, two-factor-challenge ability). For email
and sms methods a challenge code is auto-sent. Continue at Two-Factor β Submit challenge.
Errors: 401 "Invalid credentials." (also for banned / inactive / unverified accounts, with the
specific reason as the message); 500 "Login failed."
3. Verify email
POST /api/v1/verify-email β 200 { "data": { "user", "token" } }
Public. Confirms the emailed token, marks the email verified, enables the Email 2FA method, and returns a full token so the user is logged straight in.
| Field | Type | Required | Notes |
|---|---|---|---|
token |
string | yes | The token from the verification email |
Message: "Email verified successfully." Errors: 422 "Invalid verification token." /
"Verification token has expired. Please request a new one."; 500 "Email verification failed."
4. Resend email verification
POST /api/v1/resend-email-verification β 200
Public.
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | Valid email |
Message: "Verification email has been resent." (no data). Errors: 429 (throttled / already
verified / no such account β the reason is in the message); 500
"Failed to resend verification email."
5. OAuth β get redirect URL
GET /api/v1/oauth/{provider}/redirect β 200 { "data": { "redirect_url" } }
Public. provider β google Β· linkedin-openid Β· github. Returns the provider consent URL
for the client to redirect to.
Message: "OAuth redirect URL generated." Errors: 500 "Failed to generate OAuth redirect."
6. OAuth β callback
GET /api/v1/oauth/{provider}/callback β 200 { "data": { "user", "token" } }
Public. Called by the provider after consent. Links the OAuth account (or creates a new verified mentee account on first sign-in β with no password set), then returns a full token.
Message: "OAuth login successful." Errors: 500 "OAuth authentication failed."
OAuth-only accounts have
password = null. They can set a password (without a current one) via Change / set password below, after which normal email+password login also works.
7. Change / set password
PUT /api/v1/password β 200
Bearer. Changes the password for a normal account, or sets one for an OAuth-only account that has none. On success, all other tokens are revoked (the current session's token is kept).
| Field | Type | Required | Notes |
|---|---|---|---|
current_password |
string | conditional | Required if the account already has a password; prohibited if it has none (OAuth-only) |
password |
string | yes | β₯ 8, confirmed, and must differ from the current password |
password_confirmation |
string | yes | Must match password |
Message: "Password changed successfully." (no data). Errors: 422 validation, or
"Current password is incorrect."; 500 "Failed to change password."
8. Current user (me)
GET /api/v1/me β 200 { "data": { "user": { β¦ } } }
Bearer (2fa.completed). Returns the caller's own profile in exactly the shape of
GET /user/{username} β role, the full mentor/mentee profile,
location, social_links, connection and post counts β plus the account-only fields nobody
else may see.
is_me is always true and connection_status always none, since you are not connected to
yourself.
Fields /me adds on top of the public profile object:
| Field | Notes |
|---|---|
email, email_verified_at |
Never present on another user's profile. |
has_password |
false for OAuth-only accounts that have never set a password. |
two_factor_enabled, default_two_factor_method, enabled_two_factor_methods |
2FA state; default_two_factor_method falls back to "email". |
phone_number, phone_verified_at |
|
is_active |
|
roles |
Array of role names. |
address |
The same data as location, keyed to match the update profile payload (address Β· city Β· state Β· country), so an edit form can prefill directly. |
full_name |
Duplicate of name, kept so existing clients reading full_name from /me keep working. Prefer name. |
created_at, updated_at |
Message: "Current user retrieved." Errors: 500 "Failed to retrieve user."
Changed in this release.
/mepreviously returned the leanerUserResource. It now mirrors the public profile endpoint, so a client can render its own profile and someone else's with the same code. The change is additive β every field/mereturned before is still there.
Demo response β mentor calling /me
Public-profile fields come first, in the same order as GET /user/{username}; the account-only
fields follow.
{
"status": "success",
"message": "Current user retrieved.",
"data": {
"user": {
"id": "019fa742-bcce-70fb-9815-926d393682d6",
"username": "david_chen",
"name": "David Chen",
"bio": "Senior engineer helping mentees grow their careers.",
"avatar_url": null,
"cover_photo_url": null,
"timezone": "UTC",
"languages": ["English"],
"location": {
"address": "400 S Hope St",
"city": "Los Angeles",
"state": "California",
"country": "United States",
"country_code": "US"
},
"social_links": {
"facebook": "https://facebook.com/david_chen",
"instagram": null,
"x": "https://x.com/david_chen",
"linkedin": "https://linkedin.com/in/david_chen"
},
"connections_count": 0,
"posts_count": 0,
"connection_status": "none",
"is_connected": false,
"is_me": true,
"role": "mentor",
"mentor_profile": {
"id": "019fa742-bcd5-720d-99f0-7de4050c8609",
"headline": "Senior Parts Salesperson at Graham-Legros",
"expertise_areas": [
{
"id": "019fa742-bcda-71ed-81d3-80bac7cd5bd8",
"expertise_area_id": "019fa742-bcd7-702d-9d61-60a96d70d4cb",
"name": "Funeral Board",
"skills": [{ "id": "019fa742-bcd8-70ee-a811-1bb25f91b232", "name": "Embalming" }]
}
],
"years_of_experience": 8,
"hourly_rate": "50.00",
"approval_status": "approved",
"approval_status_label": "Approved",
"average_rating": "0.00",
"total_sessions": 19,
"timezone": "UTC",
"vacationMode": false,
"vacationEndsAt": null,
"experiences": [
{
"id": "019fa742-bcdb-70fe-82f5-b311e1b95c67",
"organization": "Graham-Legros",
"designation": "Parts Salesperson",
"description": "Leading the parts sales team.",
"currently_working": true,
"start_date": "2021-03-01",
"end_date": null,
"created_at": "2026-07-28T05:46:36+00:00",
"updated_at": "2026-07-28T05:46:36+00:00"
}
],
"education": [
{
"id": "019fa742-bcdc-7124-91d6-a1c3f9011320",
"institution": "University of California",
"degree": "B.Sc. in Business Administration",
"currently_studying": false,
"start_date": "2013-09-01",
"end_date": "2017-06-30",
"created_at": "2026-07-28T05:46:36+00:00",
"updated_at": "2026-07-28T05:46:36+00:00"
}
],
"certifications": [
{
"id": "019fa742-bcdc-7124-91d6-a1c3f9592c5c",
"title": "AWS Solutions Architect",
"issuing_organization": "Amazon Web Services",
"issued_date": "2025-07-14",
"expiry_date": "2028-07-14",
"credential_url": "https://www.credly.com/example"
}
],
"created_at": "2026-07-28T05:46:36+00:00",
"updated_at": "2026-07-28T05:46:36+00:00"
},
"mentee_profile": null,
"full_name": "David Chen",
"address": {
"address": "400 S Hope St",
"city": "Los Angeles",
"state": "California",
"country": "United States"
},
"email": "david@example.com",
"email_verified_at": "2026-07-28T05:46:36+00:00",
"is_active": true,
"has_password": true,
"two_factor_enabled": false,
"default_two_factor_method": "email",
"enabled_two_factor_methods": [],
"phone_number": "+15551234567",
"phone_verified_at": "2026-07-28T05:46:36+00:00",
"roles": ["mentor"],
"created_at": "2026-07-28T05:46:36+00:00",
"updated_at": "2026-07-28T05:46:36+00:00"
}
}
}
Demo response β mentee calling /me
An OAuth-only account with no password set, no phone, and no experience or education yet.
{
"status": "success",
"message": "Current user retrieved.",
"data": {
"user": {
"id": "019fa742-bd1d-71ec-aa29-8fd93c5fdc6d",
"username": "john_doe",
"name": "John Doe",
"bio": "Frontend developer, learning and growing every day.",
"gender": 1,
"gender_label": "Male",
"avatar_url": null,
"cover_photo_url": null,
"timezone": "Asia/Dhaka",
"languages": [],
"location": {
"address": "12 Gulshan Ave",
"city": "Dhaka",
"state": "Dhaka",
"country": "Bangladesh",
"country_code": "BD"
},
"social_links": {
"facebook": null,
"instagram": "https://instagram.com/john_doe",
"x": null,
"linkedin": null
},
"connections_count": 0,
"posts_count": 0,
"connection_status": "none",
"is_connected": false,
"is_me": true,
"role": "mentee",
"mentor_profile": null,
"mentee_profile": {
"id": "019fa742-bd1f-71e8-a067-ba4f19cdd3ac",
"profession": "Frontend Developer",
"company_or_institution": null,
"relationship_status": null,
"relationship_since": null,
"expertise_areas": [
{
"id": "019fa742-bd21-7098-bb57-6e7d90f35608",
"expertise_area_id": "019fa742-bd20-7174-afe0-646922348848",
"name": "Frontend Engineering",
"skills": [{ "id": "019fa742-bd20-7174-afe0-646922a1694e", "name": "React" }]
}
],
"experiences": [],
"education": [],
"certifications": [],
"created_at": "2026-07-28T05:46:36+00:00",
"updated_at": "2026-07-28T05:46:36+00:00"
},
"full_name": "John Doe",
"address": {
"address": "12 Gulshan Ave",
"city": "Dhaka",
"state": "Dhaka",
"country": "Bangladesh"
},
"email": "john@example.com",
"email_verified_at": "2026-07-28T05:46:36+00:00",
"is_active": true,
"has_password": false,
"two_factor_enabled": false,
"default_two_factor_method": "email",
"enabled_two_factor_methods": [],
"phone_number": null,
"phone_verified_at": null,
"roles": ["mentee"],
"created_at": "2026-07-28T05:46:36+00:00",
"updated_at": "2026-07-28T05:46:36+00:00"
}
}
}
languagesis[]for the mentee because only mentor profiles capture languages, andhas_passwordisfalsebecause this account signed up through OAuth. Both samples are real endpoint output, not hand-written.
Demo error responses
// 401 β no bearer token
{ "message": "Unauthenticated." }
// 403 β pending 2FA token used before the challenge is passed
{ "status": "error", "message": "Two-factor authentication required. Please complete the 2FA challenge.", "data": {} }
9. Logout
POST /api/v1/logout β 200
Bearer (2fa.completed). Revokes the current access token.
Message: "Logged out successfully." Errors: 500 "Failed to logout."
Two-Factor Authentication
The flow. Login on a 2FA-enabled account returns requires_two_factor: true and a pending
token. The client submits the code (or a backup code) to a challenge endpoint; on success the
pending token is deleted and a full token is issued. All setup endpoints require a full token
β a pending token gets 403 with "Two-factor authentication required. Please complete the 2FA challenge."
Codes are 6 digits, hashed at rest, expire after 10 minutes, allow 5 attempts, and (for Email/SMS) can be resent no more than once every 60 seconds. Backup codes: 10 codes of 6 alphanumeric characters. Trusted devices last 30 days, capped at 10 per user.
Challenge endpoints
All challenge endpoints are reachable with a pending 2FA token (
auth:sanctumonly, not2fa.completed).
10. Submit challenge
POST /api/v1/2fa/challenge β 200 { "data": { "user", "token" } }
| Field | Type | Required | Notes |
|---|---|---|---|
code |
string | yes | Exactly 6 chars |
method |
enum | no | One of the TwoFactorMethod values; validates the code against that method |
trust_device |
bool | no | When true, also returns a trusted_device_token |
Message: "Two-factor authentication verified." Data: { user, token }, plus a 64-char
trusted_device_token when trust_device was true (send it back on future logins via the
X-Trusted-Device header). Errors: 422 for an invalid method, or
"No verification code found. β¦" / "Verification code has expired. β¦" /
"Too many failed attempts. β¦" / "Invalid verification code."; 500
"Two-factor verification failed."
11. Resend challenge code
POST /api/v1/2fa/challenge/resend β 200
No body. Regenerates and re-sends the code for the current method (no-op for TOTP). Message:
"Verification code resent." Errors: 500 "Failed to resend verification code."
12. Backup-code challenge
POST /api/v1/2fa/challenge/backup β 200 { "data": { "user", "token" } }
| Field | Type | Required | Notes |
|---|---|---|---|
code |
string | yes | A single-use backup code |
Message: "Backup code verified." Errors: 422 "Invalid backup code."; 500
"Backup code verification failed."
13. Alternative methods
GET /api/v1/2fa/challenge/alternative-methods β 200
No body. Lists methods the user can switch to during a challenge.
{ "data": {
"current_method": "totp",
"current_method_label": "Authenticator App",
"alternative_methods": [ { "method": "sms", "label": "SMS" }, { "method": "backup_code", "label": "Backup Code" } ]
} }
Message: "Alternative methods retrieved." Errors: 500
"Failed to retrieve alternative methods."
14. Switch challenge method
POST /api/v1/2fa/challenge/switch-method β 200
Throttled to 1 request/minute per user (throttle:switch-method, else 429).
| Field | Type | Required | Notes |
|---|---|---|---|
method |
enum | yes | Must be in the user's alternative-methods list and differ from the current one |
Message depends on the target: SMS/Email β "Verification code sent via {label}."; BackupCode β
"Please use one of your backup codes."; TOTP β "Please use your authenticator app to retrieve the verification code." Data: { method, method_label }. Errors: 422
"The selected method is not available for your account." / "You are already using this method.";
500 "Failed to switch verification method."
Setup endpoints
All setup endpoints require a full token (
auth:sanctum+2fa.completed).
15. Enable 2FA
POST /api/v1/2fa/enable β 200 { "data": { "method", "method_label" } }
| Field | Type | Required | Notes |
|---|---|---|---|
password |
string | yes | Must be the current password (current_password rule) |
method |
enum | no | email Β· sms Β· totp. Omit to enable using any already-configured method |
The chosen method must be configured first (Email β verified email, SMS β verified phone, TOTP β
confirmed authenticator). Enabling also generates backup codes if none remain. Message:
"Two-factor authentication enabled." Errors: 422 (e.g. "Email is not verified.", "SMS is not available. Phone number is not verified.", "Authenticator app is not configured."); 500
"Failed to enable two-factor authentication."
16. Disable 2FA
POST /api/v1/2fa/disable β 200
| Field | Type | Required | Notes |
|---|---|---|---|
password |
string | yes | Must be the current password |
Clears all 2FA state and deletes backup codes, trusted devices, and pending codes. Message:
"Two-factor authentication disabled." Errors: 422 wrong password; 500
"Failed to disable two-factor authentication."
17. Set up TOTP
POST /api/v1/2fa/totp/setup β 200 { "data": { "secret", "qr_code_url" } }
No body. Generates a new secret and an otpauth:// provisioning URL (render it as a QR code
client-side, or let the user enter secret manually). Not active until confirmed. Message:
"TOTP setup initiated." Errors: 500 "Failed to set up TOTP."
qr_code_urlis theotpauth://URL string, not an image.
18. Get TOTP QR code
GET /api/v1/2fa/totp/qr-code β 200 { "data": { "secret", "qr_code_url" } }
No body. Re-fetches the provisioning URL for an in-progress TOTP setup. Message:
"TOTP QR code retrieved." Errors: 500 "Failed to retrieve TOTP QR code." (also returned when
TOTP has not been set up yet).
19. Confirm TOTP
POST /api/v1/2fa/totp/confirm β 200
| Field | Type | Required | Notes |
|---|---|---|---|
code |
string | yes | Exactly 6 chars from the authenticator app |
Verifies the code and marks TOTP configured/enabled. Message: "TOTP confirmed successfully."
Errors: 422 "TOTP has not been set up. Please run setup first." / "Invalid TOTP code. Please try again."; 500 "Failed to confirm TOTP."
20. List configurable methods
GET /api/v1/2fa/methods β 200
No body. Returns Email, SMS, and TOTP with their configured/enabled state.
{ "data": { "methods": [
{ "method": "email", "label": "Email", "configured": true, "enabled": true },
{ "method": "sms", "label": "SMS", "configured": false, "enabled": false },
{ "method": "totp", "label": "Authenticator App", "configured": false, "enabled": false }
] } }
Message: "Configurable methods retrieved." Errors: 500
"Failed to retrieve configurable methods."
21. Enable / disable a method
POST /api/v1/2fa/methods/enable β 200 { "data": { "method", "enabled" } }
| Field | Type | Required | Notes |
|---|---|---|---|
method |
enum | yes | email Β· sms Β· totp (do not pass backup_code) |
enabled |
bool | yes | Enabling requires the method to be configured |
Message: "Two-factor method updated." Errors: 422 (method not configured); 500
"Failed to update two-factor method." (also returned if backup_code is passed).
22. Set default method
POST /api/v1/2fa/default β 200 { "data": { "method", "method_label" } }
| Field | Type | Required | Notes |
|---|---|---|---|
method |
enum | yes | Must already be an enabled method |
Message: "Default two-factor method set." Errors: 422 "The selected method is not enabled for this account."; 500 "Failed to set default two-factor method."
23. Backup-codes status
GET /api/v1/2fa/backup-codes β 200 { "data": { "remaining_count" } }
No body. Returns how many unused backup codes remain (never the codes themselves). Message:
"Backup codes retrieved." Errors: 500 "Failed to retrieve backup codes."
24. Regenerate backup codes
POST /api/v1/2fa/backup-codes/regenerate β 200 { "data": { "backup_codes": [ β¦ ] } }
No body. Deletes existing codes and returns 10 fresh plaintext codes β shown once. Message:
"Backup codes regenerated. Save these codes securely β they will not be shown again." Errors:
500 "Failed to regenerate backup codes."
25. List trusted devices
GET /api/v1/2fa/trusted-devices β 200 { "data": { "devices": [ β¦ ] } }
No body. Returns TrustedDeviceResource[], newest-used first. Message:
"Trusted devices retrieved." Errors: 500 "Failed to retrieve trusted devices."
26. Revoke a trusted device
DELETE /api/v1/2fa/trusted-devices/{deviceId} β 200
No body. Message: "Trusted device revoked." Errors: 500 "Failed to revoke trusted device."
(a non-existent id also surfaces as 500, not 404).
27. Revoke all trusted devices
DELETE /api/v1/2fa/trusted-devices β 200
No body. Message: "All trusted devices revoked." Errors: 500
"Failed to revoke trusted devices."
Passkeys (WebAuthn)
Passwordless sign-in via WebAuthn credentials (web-auth/webauthn-lib). Registration and
authentication are each a two-step flow: fetch server options, then submit the browser's
credential. The server caches the challenge for 5 minutes under
webauthn_registration_{userId} / webauthn_authentication_{userId}.
RP config lives in config/two-factor.php β webauthn (rp_id = WEBAUTHN_RP_ID, origin =
WEBAUTHN_ORIGIN).
28. Authentication options
POST /api/v1/passkeys/authenticate/options β 200 { "data": { "options": { β¦ } } }
Public.
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | Valid email, must exist in users |
Returns serialized PublicKeyCredentialRequestOptions (challenge, rpId, allowCredentials
from the user's passkeys). Message: "Authentication options generated." Errors: 422 validation;
500 "Failed to generate authentication options."
29. Authenticate
POST /api/v1/passkeys/authenticate β 200 { "data": { "user", "token" } }
Public.
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | Valid email, must exist, β€ 255 |
credential |
object (array) | yes | The browser's assertion response |
On success updates the passkey's sign count / last-used and issues a full api-token. Message:
"Passkey authentication successful." Errors: 422 validation, or "Invalid assertion response."
/ "Passkey not found." / "Authentication challenge expired. Please try again."; 500
"Passkey authentication failed."
30. Registration options
POST /api/v1/passkeys/register/options β 200 { "data": { "options": { β¦ } } }
Bearer (2fa.completed). No body. Returns serialized
PublicKeyCredentialCreationOptions (rp, user entity, challenge, pubKeyCredParams). Message:
"Registration options generated."
31. Register a passkey
POST /api/v1/passkeys/register β 201 { "data": { "passkey": { β¦ } } }
Bearer (2fa.completed).
| Field | Type | Required | Notes |
|---|---|---|---|
credential |
object (array) | yes | The browser's attestation response |
device_name |
string | yes | β€ 255 (shown in the passkey list) |
Message: "Passkey registered successfully." Data: the new PasskeyResource. Errors: 422
validation, or "Invalid attestation response." / "Registration challenge expired. Please try again."; 500 "Failed to register passkey."
32. List passkeys
GET /api/v1/passkeys β 200 { "data": { "passkeys": [ β¦ ] } }
Bearer (2fa.completed). No body. Returns PasskeyResource[], most-recently-used first.
Message: "Passkeys retrieved." Errors: 500 "Failed to retrieve passkeys."
33. Delete a passkey
DELETE /api/v1/passkeys/{passkeyId} β 200
Bearer (2fa.completed). No body. Scoped to the authenticated user. Message:
"Passkey deleted." Errors: 500 "Failed to delete passkey." (a non-existent id also surfaces
as 500, not 404).
Phone Verification
One-time-code phone verification (6-digit code, hashed at rest, 10-minute expiry, max 5 attempts),
delivered by Vonage SMS. Config in config/two-factor.php. Verifying a phone unlocks the SMS
2FA method.
34. Update phone number
POST /api/v1/phone/update β 200
Bearer (2fa.completed). Sets the phone number (resetting any prior verification) and sends a
code.
| Field | Type | Required | Notes |
|---|---|---|---|
phone_number |
string | yes | International format, regex ^\+?[1-9]\d{6,14}$ (e.g. +1234567890) |
Message: "Phone number updated. Verification code sent." Errors: 422 invalid format; 500
"Failed to update phone number."
35. Verify phone number
POST /api/v1/phone/verify β 200
Bearer (2fa.completed).
| Field | Type | Required | Notes |
|---|---|---|---|
code |
string | yes | Exactly 6 chars |
Message: "Phone number verified." Errors: 422 "No verification code found. β¦" /
"Verification code has expired." / "Too many failed attempts. β¦" / "Invalid verification code."; 500 "Phone verification failed."
Onboarding
A role-specific questionnaire (mentor or mentee), stored once per user per role.
36. Onboarding questions
GET /api/v1/onboarding/questions/{role} β 200 { "data": { "role", "questions": [ β¦ ] } }
Public. role β mentor Β· mentee. Returns the question set (labels and options vary by
role). Each question is:
{ "key": "experience_level", "type": "single_select", "label": "β¦", "required": true,
"options": ["beginner","intermediate","advanced","expert"] }
The set is: goals (multi-select), experience_level, preferred_communication
(videoΒ·audioΒ·chat), availability (weekdaysΒ·weekendsΒ·bothΒ·flexible), weekly_hours
(number, 1β40), topics_of_interest (multi-select), how_did_you_hear (text, optional), and
expectations (textarea, optional). Message: "Onboarding questions retrieved." Errors: 422
"Invalid role. Must be mentor or mentee."; 500 "Failed to retrieve onboarding questions."
37. Submit onboarding
POST /api/v1/onboarding β 201 { "data": { "onboarding": { β¦ } } }
Bearer (2fa.completed).
| Field | Type | Required | Notes |
|---|---|---|---|
role |
enum | yes | mentor Β· mentee |
responses |
object | yes | Answer map (below) |
responses.goals |
string[] | yes | β₯ 1 item, each β€ 255 |
responses.experience_level |
enum | yes | beginner Β· intermediate Β· advanced Β· expert |
responses.preferred_communication |
enum | yes | video Β· audio Β· chat |
responses.availability |
enum | yes | weekdays Β· weekends Β· both Β· flexible |
responses.weekly_hours |
int | yes | 1β40 |
responses.topics_of_interest |
string[] | yes | 1β10 items, each β€ 100 |
responses.how_did_you_hear |
string | no | β€ 255 |
responses.expectations |
string | no | β€ 1000 |
Data: { "onboarding": { "id", "role", "is_completed", "responses" } }. Message:
"Onboarding completed successfully." Errors: 422 validation, or "You have already completed onboarding for this role."; 500 "Failed to submit onboarding."
38. Show onboarding
GET /api/v1/onboarding/{role} β 200 { "data": { "onboarding": { β¦ } } }
Bearer (2fa.completed). role β mentor Β· mentee. Returns the stored submission. Message:
"Onboarding retrieved." Errors: 422 invalid role; 404 "No onboarding found for this role.";
500 "Failed to retrieve onboarding."
Profile
39. Update profile
PUT /api/v1/profile β 200 { "data": { "user": { β¦ } } }
Bearer (2fa.completed). Only sent fields are updated (null values are ignored).
Because
nullis ignored, a field cannot be cleared through this endpoint β sending"gender": nullleaves the stored value untouched. Send4(Prefer not to say) to withdraw a gender that was previously set.
| Field | Type | Required | Notes |
|---|---|---|---|
full_name |
string | no | β€ 255 |
username |
string | no | 3β30, lowercase letters/numbers/underscore (^[a-z0-9_]+$), unique |
bio |
string|null | no | β€ 2000 |
gender |
integer|null | no | A Gender value β 1 Male Β· 2 Female Β· 3 Other Β· 4 Prefer not to say. Anything else is a 422. |
timezone |
string | no | Valid timezone, β€ 100 |
avatar_url |
string|null | no | Valid URL, β€ 500 |
social_links |
object|null | no | Keys facebook Β· instagram Β· x Β· linkedin, each a valid URL β€ 255. Stored as JSON on the user. |
country |
string|null | no | β€ 255 |
state |
string|null | no | β€ 255 |
city |
string|null | no | β€ 255 |
address |
string|null | no | Street line, β€ 255 |
Address & social links. Both belong to the user, not to the mentor/mentee profile. The four
address fields are written to the user's single addresses row, created on first use; fields left
out of the payload are untouched, so a partial update never wipes the rest of the address. The
response carries them back as social_links and a nested address object.
// request
{ "country": "Bangladesh", "state": "Dhaka", "city": "Dhaka", "address": "12 Gulshan Ave",
"social_links": { "facebook": "https://facebook.com/john_doe" } }
// response β data.user
{ "β¦": "β¦",
"social_links": { "facebook": "https://facebook.com/john_doe" },
"address": { "address": "12 Gulshan Ave", "city": "Dhaka", "state": "Dhaka", "country": "Bangladesh" } }
Moved in this release.
social_linksused to live onmentor_profiles/mentee_profilesand the address was a polymorphic row hanging off those profiles. Both now belong to the user.MentorProfileResource/MenteeProfileResourcekeep theiraddresskey β it reads the owning user's address β but theirsocial_linkskey is gone; read it from the user object. The mentor and mentee profile update endpoints still acceptcountry/state/cityand write through to that same user-level row.
Message: "Profile updated successfully." Errors: 422 (e.g. "This username is already taken.",
or a social_links.* value that is not a URL); 500 "Failed to update profile."
40. Upload profile media
POST /api/v1/profile/media (multipart/form-data) β 200 { "data": { "user": { β¦ } } }
Bearer (2fa.completed). Uploads an avatar and/or cover photo (Spatie MediaLibrary), then sets
avatar_url / cover_photo_url on the user.
| Field | Type | Required | Notes |
|---|---|---|---|
avatar |
file | no | Image (jpeg,png,jpg,gif,webp), β€ 2 MB β avatar collection |
cover_photo |
file | no | Image (jpeg,png,jpg,gif,webp), β€ 2 MB β cover_photo collection |
Message: "Profile media uploaded successfully." Errors: 422 invalid file; 500
"Failed to upload profile media."
41. View a user profile
GET /api/v1/user/{username} β 200 { "data": { "user": { β¦ } } }
Bearer (2fa.completed). Returns the full public profile of any user, resolved by
username β identity, location, social links, connection/post counts, the caller's
connection_status and chat_thread, and the complete role profile (expertise areas with skills,
experiences, education, certifications). See the public profile object above for the full shape.
Works on your own username too, in which case is_me is true, connection_status is none,
and chat_thread is null.
Message: "Profile retrieved successfully." Errors: 401 missing token; 403 pending 2FA token;
404 unknown username; 500 "Failed to retrieve profile."
Which profile endpoint? This one is the complete profile.
GET /api/v1/feed/users/{username}(see the Feed API docs) returns the same person trimmed to what a feed card needs β identity, counts, and connection state, with no role profile.
Profile sub-resources. Lists that would grow without bound are not embedded here β fetch them per tab, each paginated and resolved by the same username:
| Posts | GET /api/v1/feed/users/{username}/posts (Feed API) |
| Connections | GET /api/v1/feed/users/{username}/connections (Feed API) |
| Courses | GET /api/v1/user/{username}/courses β endpoint 42 |
| Communities | GET /api/v1/user/{username}/communities β endpoint 43 |
Demo response β role: "mentor"
GET /api/v1/user/david_chen
{
"status": "success",
"message": "Profile retrieved successfully.",
"data": {
"user": {
"id": "019f5f0a-d32a-73f3-aa86-45e916ecb015",
"username": "david_chen",
"name": "David Chen",
"bio": "Senior engineer helping mentees grow their careers.",
"gender": 1,
"gender_label": "Male",
"avatar_url": "https://api-meetyy.webmakerslimited.com/storage/2/6fbaebcf-3977-4c9b-a0cf-07b5a5961da0.png",
"cover_photo_url": null,
"timezone": "UTC",
"languages": ["English"],
"location": {
"address": "400 S Hope St",
"city": "Los Angeles",
"state": "California",
"country": "United States",
"country_code": "US"
},
"social_links": {
"facebook": "https://facebook.com/david_chen",
"instagram": null,
"x": "https://x.com/david_chen",
"linkedin": "https://linkedin.com/in/david_chen"
},
"connections_count": 1,
"posts_count": 15,
"connection_status": "pending_outgoing",
"is_connected": false,
"is_me": false,
"chat_thread": null,
"role": "mentor",
"mentor_profile": {
"id": "019f5f0a-d333-70a2-8b8a-f4aae8cc6815",
"headline": "Senior Parts Salesperson at Graham-Legros",
"expertise_areas": [
{
"id": "019f5f0a-d339-724c-bdbe-0dacb01515e3",
"expertise_area_id": "019f5f0a-c952-7142-b795-46dea53cc6c4",
"name": "Funeral Board",
"skills": [
{ "id": "019f5f0a-c954-71f1-9e61-832b857765e2", "name": "Embalming" },
{ "id": "019f5f0a-c956-70c8-8a1b-2df5c2599cda", "name": "Grief counseling" }
]
},
{
"id": "019f5f0a-d34b-733b-bb75-57ccfcf4bc54",
"expertise_area_id": "019f5f0a-c8ae-717d-9f9f-8db7f11c9043",
"name": "Chiropractic Board",
"skills": [
{ "id": "019f5f0a-c8b1-7385-b643-657eed8cdd4f", "name": "Spinal adjustment" },
{ "id": "019f5f0a-c8b3-73a3-8e6a-d3484e631348", "name": "Musculoskeletal assessment" }
]
}
],
"years_of_experience": 8,
"hourly_rate": "50.00",
"approval_status": "approved",
"approval_status_label": "Approved",
"average_rating": "4.05",
"total_sessions": 19,
"timezone": "UTC",
"vacationMode": false,
"vacationEndsAt": null,
"experiences": [
{
"id": "019f5f0a-e111-70aa-8b8a-000000000001",
"organization": "Graham-Legros",
"designation": "Parts Salesperson",
"description": "Leading the parts sales team and mentoring junior staff.",
"currently_working": true,
"start_date": "2021-03-01",
"end_date": null,
"created_at": "2026-07-14T05:12:52+00:00",
"updated_at": "2026-07-14T05:12:52+00:00"
}
],
"education": [
{
"id": "019f5f0a-e222-70aa-8b8a-000000000002",
"institution": "University of California",
"degree": "B.Sc. in Business Administration",
"currently_studying": false,
"start_date": "2013-09-01",
"end_date": "2017-06-30",
"created_at": "2026-07-14T05:12:52+00:00",
"updated_at": "2026-07-14T05:12:52+00:00"
}
],
"certifications": [
{
"id": "019f5f0a-d393-7163-8169-831c371b2eda",
"title": "AWS Solutions Architect",
"issuing_organization": "Amazon Web Services",
"issued_date": "2025-07-14",
"expiry_date": "2028-07-14",
"credential_url": "https://www.credly.com/example"
}
],
"created_at": "2026-07-14T05:12:52+00:00",
"updated_at": "2026-07-14T05:12:52+00:00"
},
"mentee_profile": null
}
}
}
Demo response β role: "mentee"
GET /api/v1/user/john_doe
{
"status": "success",
"message": "Profile retrieved successfully.",
"data": {
"user": {
"id": "019f5f0a-a111-73f3-aa86-45e916ec0001",
"username": "john_doe",
"name": "John Doe",
"bio": "Frontend developer, learning and growing every day.",
"gender": 2,
"gender_label": "Female",
"avatar_url": "https://api-meetyy.webmakerslimited.com/storage/2/example-avatar.png",
"cover_photo_url": null,
"timezone": "Asia/Dhaka",
"languages": [],
"location": {
"address": "12 Gulshan Ave",
"city": "Dhaka",
"state": "Dhaka",
"country": "Bangladesh",
"country_code": "BD"
},
"social_links": {
"facebook": "https://facebook.com/john_doe",
"instagram": "https://instagram.com/john_doe",
"x": null,
"linkedin": "https://linkedin.com/in/john_doe"
},
"connections_count": 24,
"posts_count": 8,
"connection_status": "connected",
"is_connected": true,
"is_me": false,
"role": "mentee",
"mentor_profile": null,
"mentee_profile": {
"id": "019f5f0a-a222-70a2-8b8a-f4aae8cc0002",
"profession": "Frontend Developer",
"company_or_institution": null,
"relationship_status": null,
"relationship_since": null,
"expertise_areas": [
{
"id": "019f5f0a-a333-724c-bdbe-000000000003",
"expertise_area_id": "019f5f0a-c952-7142-b795-46dea53cc0aa",
"name": "Frontend Engineering",
"skills": [
{ "id": "019f5f0a-a334-71f1-9e61-000000000004", "name": "React" },
{ "id": "019f5f0a-a335-70c8-8a1b-000000000005", "name": "TypeScript" },
{ "id": "019f5f0a-a336-7306-a537-000000000006", "name": "Tailwind CSS" }
]
}
],
"experiences": [
{
"id": "019f5f0a-a444-70aa-8b8a-000000000007",
"organization": "TechCorp Ltd.",
"designation": "Senior Frontend Developer",
"description": "Building a mentorship platform with React and TypeScript.",
"currently_working": true,
"start_date": "2023-01-01",
"end_date": null,
"created_at": "2026-07-14T05:12:52+00:00",
"updated_at": "2026-07-14T05:12:52+00:00"
},
{
"id": "019f5f0a-a445-70aa-8b8a-000000000008",
"organization": "Pixel Studio",
"designation": "Frontend Developer",
"description": "Built and maintained client web apps.",
"currently_working": false,
"start_date": "2020-03-01",
"end_date": "2022-12-31",
"created_at": "2026-07-14T05:12:52+00:00",
"updated_at": "2026-07-14T05:12:52+00:00"
}
],
"education": [
{
"id": "019f5f0a-a555-70aa-8b8a-000000000009",
"institution": "University of Dhaka",
"degree": "B.Sc. in Computer Science",
"currently_studying": false,
"start_date": "2015-01-01",
"end_date": "2019-12-31",
"created_at": "2026-07-14T05:12:52+00:00",
"updated_at": "2026-07-14T05:12:52+00:00"
}
],
"certifications": [],
"created_at": "2026-07-14T05:12:52+00:00",
"updated_at": "2026-07-14T05:12:52+00:00"
}
}
}
}
Two deviations from the mentee sample you may see quoted elsewhere:
languagesis[]because only mentor profiles capture languages today, andcertificationsis[]because mentee certifications have no storage yet. Everything else is live data.
Demo error responses
// 404 β unknown username (route-model binding fails before the controller runs)
{ "message": "No query results for model [App\\Models\\User] does_not_exist" }
// 401 β no bearer token
{ "message": "Unauthenticated." }
// 500 β unexpected failure
{ "status": "error", "message": "Failed to retrieve profile.", "data": {} }
42. A user's courses
GET /api/v1/user/{username}/courses β 200 { "data": { "courses": { β¦ } } }
GET /api/v1/user/{username}/courses?type=created
GET /api/v1/user/{username}/courses?type=enrolled&per_page=20
Bearer (2fa.completed). Paginated. Returns the courses this user authored (mentors) and the
ones they enrolled in, merged into a single list; every row carries is_creator and
is_enrolled so the client can tell them apart without a second call.
| Param | Type | Default | Notes |
|---|---|---|---|
type |
string | (both) | created or enrolled to narrow to one slice. Anything else β 422. |
per_page |
integer | 15 | 1β50. |
Each item is the standard course object (same shape the mentor course endpoints return) plus:
| Field | Notes |
|---|---|
is_creator |
true when this user authored the course. |
is_enrolled |
true when this user is enrolled in it. Both can be true if a mentor enrols in their own course. |
Unpublished work stays private. Other viewers see only
status: publishedcourses; the user themselves also sees their drafts and archived courses. So the same URL can legitimately return more rows when you call it on yourself.
Message: "Courses retrieved successfully." Errors: 401 missing token; 403 pending 2FA token;
404 unknown username; 422 bad type / per_page; 500 "Failed to retrieve courses."
Demo response
GET /api/v1/user/david_chen/courses?per_page=2 β one authored, one enrolled. Truncated to the
fields that matter here; the full course object is documented in the Course API.
{
"status": "success",
"message": "Courses retrieved successfully.",
"data": {
"courses": {
"data": [
{
"id": "019fa75f-32b2-7172-ba60-47fe95e79bf6",
"mentor_id": "019fa75f-32af-73df-82f5-02cdc02b18ea",
"community_ids": [],
"title": "Mastering Spinal Adjustment",
"slug": "mastering-spinal-adjustment",
"subtitle": "A practical clinic course",
"summary": "Hands-on technique training.",
"status": "published",
"status_label": "Published",
"visibility": "public",
"visibility_label": "Public",
"is_free": false,
"price": "149.00",
"currency": "USD",
"total_lessons": 24,
"total_duration_minutes": 320,
"enrollment_count": 87,
"average_rating": "4.60",
"thumbnail_url": null,
"published_at": "2026-07-28T06:17:41+00:00",
"created_at": "2026-07-28T06:17:41+00:00",
"updated_at": "2026-07-28T06:17:41+00:00",
"is_creator": true,
"is_enrolled": false
},
{
"id": "019fa75f-32ba-7093-a60c-f5046887f60d",
"mentor_id": "019fa75f-32ba-7093-a60c-f504684c5471",
"title": "Grief Counseling Foundations",
"slug": "grief-counseling-foundations",
"status": "published",
"status_label": "Published",
"visibility": "public",
"price": "114.02",
"currency": "USD",
"enrollment_count": 0,
"average_rating": "0.00",
"is_creator": false,
"is_enrolled": true
}
],
"links": { "first": "β¦?page=1", "last": "β¦?page=1", "prev": null, "next": null },
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "https://api-meetyy.webmakerslimited.com/api/v1/user/david_chen/courses",
"per_page": 2,
"to": 2,
"total": 2
}
}
}
}
meta.totaldoubles as the count for a profile tab label β there is no separate counts endpoint.
// 422 β bad type
{
"message": "The type must be either \"created\" or \"enrolled\".",
"errors": { "type": ["The type must be either \"created\" or \"enrolled\"."] }
}
43. A user's communities
GET /api/v1/user/{username}/communities β 200 { "data": { "communities": { β¦ } } }
GET /api/v1/user/{username}/communities?per_page=20
Bearer (2fa.completed). Paginated. Returns every community the user belongs to, the ones
they created included β a creator also holds an Owner membership row, so one list covers both.
There is no type filter; use the flags instead.
| Param | Type | Default | Notes |
|---|---|---|---|
per_page |
integer | 15 | 1β50. |
Each item is the community summary object plus:
| Field | Notes |
|---|---|
is_creator |
true when this user owns the community. |
member_role |
owner Β· member. |
Only active memberships count β a community the user left, was removed from, or was banned from does not appear. As with courses, unpublished communities are visible only to the owner.
Message: "Communities retrieved successfully." Errors: 401 missing token; 403 pending 2FA
token; 404 unknown username; 422 bad per_page; 500 "Failed to retrieve communities."
Demo response
GET /api/v1/user/david_chen/communities?per_page=2 β one owned, one joined.
{
"status": "success",
"message": "Communities retrieved successfully.",
"data": {
"communities": {
"data": [
{
"id": "019fa75f-32bf-70aa-a028-acf9fec2c169",
"name": "Chiropractic Circle",
"slug": "chiropractic-circle",
"tagline": null,
"status": "published",
"pricing_type": "free",
"pricing_type_label": "Free",
"billing_interval": null,
"price": null,
"currency": "USD",
"member_count": 42,
"cover_url": null,
"icon_url": null,
"mentor": {
"id": "019fa75f-32af-73df-82f5-02cdc02b18ea",
"headline": null,
"average_rating": "0.00",
"name": "David Chen",
"username": "david_chen",
"avatar_url": null
},
"is_creator": true,
"member_role": "owner"
},
{
"id": "019fa75f-32d4-72c9-b12b-7701bf19561a",
"name": "Career Growth Hub",
"slug": "career-growth-hub",
"tagline": null,
"status": "published",
"pricing_type": "free",
"pricing_type_label": "Free",
"billing_interval": null,
"price": null,
"currency": "USD",
"member_count": 310,
"cover_url": null,
"icon_url": null,
"mentor": {
"id": "019fa75f-32d3-7116-81c2-0f8b67c5e86a",
"headline": null,
"average_rating": "0.00",
"name": "Noble Rosenbaum",
"username": "tina29",
"avatar_url": null
},
"is_creator": false,
"member_role": "member"
}
],
"links": { "first": "β¦?page=1", "last": "β¦?page=1", "prev": null, "next": null },
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "https://api-meetyy.webmakerslimited.com/api/v1/user/david_chen/communities",
"per_page": 2,
"to": 2,
"total": 2
}
}
}
}