MeetyyAPI
Documentation / API Reference / Authentication

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 use multipart/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 (ability two-factor-challenge, 10-minute expiry). A pending token can only reach the 2FA challenge endpoints β€” every route guarded by 2fa.completed returns 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 InvalidArgumentException in 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 with backup_code, Revoke a trusted device, and Delete a passkey).


Enums

Two-factor method (TwoFactorMethod, string-backed):

Value Label
email Email
sms SMS
totp Authenticator App
backup_code Backup Code

backup_code passes in: 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. null means the user has never told us β€” distinct from 4 (Prefer not to say), which is an explicit choice to withhold it. Wherever gender appears it is paired with a human-readable gender_label, both null when 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": "…"
}

roles is present only when the roles relation is loaded (value is the array of role names). mentor_profile / mentee_profile appear only when their relations are eager-loaded. social_links is free-form JSON stored on the user; address appears only when the address relation is loaded, and is null until the user saves one. Both live on the user, not on the role profiles β€” see Address & social links below. default_two_factor_method falls back to "email" when unset. has_password is false for OAuth-only accounts that have never set a password β€” use it to decide whether to prompt for set password (no current_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_rate and average_rating are decimal:2 casts and serialise as strings ("50.00", "4.05"), while years_of_experience and total_sessions are integers. approval_status is the string-backed ApprovalStatus enum β€” pending Β· approved Β· rejected Β· suspended β€” paired with a human-readable approval_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 gender here. Gender belongs to the user β€” read gender / gender_label from the enclosing profile object, and write it through Update profile (endpoint 39). The permanently null gender key this object used to carry has been removed.

Not yet backed by storage. mentee_profiles has no columns for company_or_institution, relationship_status, or relationship_since, and mentee certifications do not exist (mentor_certifications is mentor-only). These fields are emitted as null / [] so the response shape stays stable for clients, and will start returning real values once the columns land. profession maps to the stored current_role.

Passkey object (PasskeyResource)

{
  "id": "…",
  "device_name": "MacBook Pro",
  "attestation_type": "none",
  "last_used_at": "…",
  "created_at": "…"
}

credential_id, public_key, and sign_count are 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_name is 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. /me previously returned the leaner UserResource. 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 /me returned 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"
    }
  }
}

languages is [] for the mentee because only mentor profiles capture languages, and has_password is false because 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:sanctum only, not 2fa.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_url is the otpauth:// 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 null is ignored, a field cannot be cleared through this endpoint β€” sending "gender": null leaves the stored value untouched. Send 4 (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_links used to live on mentor_profiles / mentee_profiles and the address was a polymorphic row hanging off those profiles. Both now belong to the user. MentorProfileResource / MenteeProfileResource keep their address key β€” it reads the owning user's address β€” but their social_links key is gone; read it from the user object. The mentor and mentee profile update endpoints still accept country / state / city and 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: languages is [] because only mentor profiles capture languages today, and certifications is [] 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: published courses; 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.total doubles 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
      }
    }
  }
}