MeetyyAPI
Documentation / API Reference / Courses

Course API

Endpoints for the course system (Phase 6). Covers a mentor authoring a course (modules → lessons), publishing and pricing it; the public discovering and previewing published courses; and a mentee enrolling and learning with progress tracking.

  • Base URL: /api/v1
  • Auth: mentor and mentee endpoints require a Sanctum bearer token — Authorization: Bearer {token}. Public discovery endpoints need no auth.
  • Content type: application/json (file uploads use multipart/form-data)

Scope note. Real payment settlement is deferred (no gateway exists yet, mirroring the booking checkout): paid enrollment records a price_snapshot and grants access immediately; payment_id stays null until the payments module lands. Community linkage is now live (the community module activates it): community-only serving, level-gated modules (unlock_at_level), and community/specific-community pricing all resolve — see Community linkage. Still deferred: premium-exclusive courses (is_exclusive_to_premium, its column exists but is not yet enforced).


⚠️ Breaking changes — multi-community courses

A course is no longer limited to one community. courses.community_id has been dropped in favour of a community_course pivot, so a course can be carried by any number of communities and priced independently in each. Full mechanics in Community linkage.

Response changes

Endpoint Change
POST/GET/PUT /mentor/courses… Removed data.course.community_id. Added data.course.community_ids (array of uuids, [] when unlinked)
GET /mentee/communities/{community}/courses Removed data.courses[].community_id — the endpoint is already scoped to one community, and my_pricing is now resolved in that community's context
POST /mentee/courses/{course}/enroll Added data.enrollment.community_id — the community the access came through (null for a public enrollment)
  {
    "id": "9d1c…",
    "mentor_id": "8b2a…",
-   "community_id": "7f3e…",
+   "community_ids": ["7f3e…", "5a9b…"],
    "title": "From Junior to Senior Engineer",
    "visibility": "community_only"
  }

Request changes

Endpoint Change
POST /mentor/courses, PUT /mentor/courses/{course} Added community_ids (array of uuids, each a community you own). community_id is still accepted as a deprecated alias and folded into the set
POST /mentee/courses/{course}/enroll Added optional community_id — enroll through a specific community. Omit it and the carrying community where you hold the highest level is chosen
POST/PUT …/modules unlock_at_level is now rejected unless a community carries the course (422)
// Create a course carried by two communities
{
  "title": "From Junior to Senior Engineer",
  "visibility": "community_only",
  "community_ids": ["7f3e…", "5a9b…"]
}

Update semantics: omit community_ids to leave the links untouched; send an explicit array to replace the whole set. "community_ids": [] (or the deprecated "community_id": null) unlinks the course — and is rejected with 422 if its visibility is community_only.

Migration for clients: replace reads of community_id with community_ids[0] where a single value is still assumed, and switch writes to community_ids. Validation errors are reported against whichever key you sent — community_id or community_ids.{index}.


Response envelope

Every response uses the standard envelope.

Success

{ "status": "success", "message": "…", "data": { } }

Error

{ "status": "error", "message": "…", "data": { } }

Validation errors (422) return field errors in data:

{
  "status": "error",
  "message": "The title field is required.",
  "data": { "title": ["The title field is required."] }
}

Status codes

Code Meaning
200 OK (read, update, lifecycle action)
201 Resource created
401 Missing/invalid token
403 Authenticated but not entitled (e.g. content access without enrollment)
404 Not found or not owned by the caller
422 Validation error / invalid state (e.g. enroll in a draft, quiz on a non-quiz lesson, quiz time limit exceeded, late assignment without a note)
500 Server error

Enums

Course status: draft · published · archived

Course visibility

Value Public search Direct slug Notes
public Standard public course
unlisted Share by link only
community_only Members-only — gated to the carrying communities (see Community linkage)

Lesson type: video · text · pdf · quiz · assignment (quiz auto-scores; assignment is a mentor-graded file submission)

Pricing audience: public · community_member · specific_community (all three resolve — within a community's context, a member's community price wins over the public price; see Community linkage)

Enrollment status: active · completed · refunded · cancelled (active and completed grant content access)

Lesson progress status: in_progress · submitted · completed (submitted = an assignment awaiting the mentor's grade)


Shared objects

Course object (mentor / authoring view)

{
  "id": "9b6f…",
  "mentor_id": "8a1c…",
  "community_ids": [],
  "title": "Mastering Product Management",
  "slug": "mastering-product-management",
  "subtitle": "From backlog to roadmap",
  "description": "…",
  "summary": "A concise overview of the course.",
  "learning_outcomes": ["Prioritise a backlog", "Build a roadmap", "Run discovery"],
  "status": "draft",
  "status_label": "Draft",
  "visibility": "public",
  "visibility_label": "Public",
  "is_free": false,
  "price": "49.00",
  "currency": "USD",
  "is_exclusive_to_premium": false,
  "total_lessons": 12,
  "total_duration_minutes": 240,
  "enrollment_count": 0,
  "average_rating": "0.00",
  "thumbnail_url": null,
  "modules": [ { "…module object…" } ],
  "published_at": null,
  "created_at": "2026-07-02T09:00:00+00:00",
  "updated_at": "2026-07-02T09:00:00+00:00"
}

modules (with nested lessons) is included only on the show response. total_lessons / total_duration_minutes are recomputed from published lessons.

Module object

{
  "id": "1a2b…",
  "course_id": "9b6f…",
  "title": "Foundations",
  "description": null,
  "position": 0,
  "unlock_at_level": null,
  "is_published": true,
  "lessons": [ { "…lesson object…" } ],
  "created_at": "…",
  "updated_at": "…"
}

Lesson object (mentor / authoring view)

{
  "id": "3c4d…",
  "module_id": "1a2b…",
  "course_id": "9b6f…",
  "title": "What is a roadmap?",
  "lesson_type": "video",
  "lesson_type_label": "Video",
  "position": 0,
  "text_content": null,
  "video_provider": "youtube",
  "video_url": "https://www.youtube.com/watch?v=…",
  "duration_minutes": 12,
  "time_limit_minutes": null,
  "quiz_payload": { "pass_pct": 70, "questions": [ … ] },
  "assignment_payload": null,
  "is_preview": false,
  "is_published": true,
  "attachment_url": null,
  "assignment_attachments": [],
  "created_at": "…",
  "updated_at": "…"
}

time_limit_minutes (optional) applies to quiz and assignment lessons — see the timer notes. assignment_payload holds { total_marks, passing_marks, instructions }; assignment_attachments is the array of mentor-provided brief files ([{ id, name, url }]).

Pricing object

{
  "id": "7d2e…",
  "course_id": "9b6f…",
  "audience_type": "public",
  "audience_type_label": "Public",
  "community_id": null,
  "is_free": false,
  "price": "19.00",
  "currency": "USD",
  "created_at": "…",
  "updated_at": "…"
}

Enrollment object

{
  "id": "5e6f…",
  "course_id": "9b6f…",
  "community_id": "7f3e…",
  "status": "active",
  "status_label": "Active",
  "audience_type": "public",
  "is_free": false,
  "price_snapshot": "49.00",
  "progress_pct": 50,
  "completed_lessons_count": 6,
  "enrolled_at": "2026-07-02T09:00:00+00:00",
  "completed_at": null,
  "last_accessed_at": "2026-07-02T10:00:00+00:00",
  "course": { "…public course object (when loaded)…" }
}

Mentor endpoints — authoring

All require an approved mentor profile. Ownership is enforced by scoping: another mentor's course (or a missing mentor profile) returns 404.

1. List my courses

GET /api/v1/mentor/courses

Returns the mentor's courses (all statuses), newest first, as a plain collection.

200{ "data": { "courses": [ { "…course object…" } ] } }

Self-only. This lists the caller's own courses. To read another user's courses — or to render a profile page — use GET /api/v1/user/{username}/courses (Authentication API, endpoint 42). That one is paginated, merges authored with enrolled courses behind is_creator / is_enrolled flags, and hides drafts and archived courses from everyone but the author.

2. Create a course

POST /api/v1/mentor/courses          (multipart/form-data if sending a thumbnail)

Creates a draft course and auto-generates a unique slug from the title.

Field Type Required Notes
title string yes Max 255
subtitle string no Max 255
description string no
summary string no Short overview
learning_outcomes string[] no Array of outcomes; each ≤ 255
community_ids uuid[] conditionally Communities carrying the course — each must be one you own. At least one required when visibility = community_only
community_id uuid no Deprecated — single-community alias, folded into community_ids
visibility enum no public (default) · unlisted · community_only
is_free bool no Default false
price decimal conditionally Required unless is_free is true
currency string no Default USD
is_exclusive_to_premium bool no Stored; not yet enforced
thumbnail file no Image, ≤ 5 MB

201{ "data": { "course": { "…course object…" } } } Errors: 422 missing title / missing price when not free / a community_ids entry not owned by you / community_only with no communities · 404 no mentor profile

3. Show a course

GET /api/v1/mentor/courses/{id}

Returns the course with its modules → lessons eager-loaded (all statuses; authoring view).

200{ "data": { "course": { "…course object with modules…" } } } Errors: 404 not found / not owned

4. Update a course

PUT|PATCH /api/v1/mentor/courses/{id}     (multipart/form-data if replacing the thumbnail)

Same fields as create, all optional (sometimes). Changing title regenerates the slug.

Pass community_ids to replace the whole set of communities carrying the course; omit the key to leave the links untouched, or send [] to unlink it entirely. The community-only invariant is checked against the resulting state: switching visibility to community_only on a course no community carries (and none supplied) returns 422, as does clearing the last community of a course that is already community_only.

200{ "data": { "course": { … } } } Errors: 422 a community_ids entry not owned by you / community_only with no communities

5–7. Lifecycle: publish / unpublish / archive

PATCH /api/v1/mentor/courses/{id}/publish     → status: published (sets published_at once)
PATCH /api/v1/mentor/courses/{id}/unpublish   → status: draft
PATCH /api/v1/mentor/courses/{id}/archive     → status: archived

200{ "data": { "course": { … } } }

8. Delete a course

DELETE /api/v1/mentor/courses/{id}

Cascades to modules, lessons, pricing, enrollments, and progress.

200{ "message": "Course deleted successfully." }


Mentor endpoints — modules

Nested under a course the mentor owns. A course not owned by the caller → 404.

9. List / create modules

GET  /api/v1/mentor/courses/{course}/modules          → { "data": { "modules": [ … with lessons ] } }
POST /api/v1/mentor/courses/{course}/modules          → 201 { "data": { "module": { … } } }

Create payload

Field Type Required Notes
title string yes Max 255
description string no
position int no Auto = max(position) + 1 when omitted
unlock_at_level int no ≥ 1; locks the module until the learner reaches this level in the community they enrolled through. Rejected (422) unless a community carries the course (see Community linkage)
is_published bool no Default true

10. Update / delete a module

PUT|PATCH /api/v1/mentor/courses/{course}/modules/{id}     → { "data": { "module": { … } } }
DELETE    /api/v1/mentor/courses/{course}/modules/{id}     → { "message": "Module deleted successfully." }

11. Reorder modules

PATCH /api/v1/mentor/courses/{course}/modules/reorder

Sets position to each id's index in the array (0-based). Ids not belonging to the course are ignored.

{ "ordered_ids": ["moduleB", "moduleA", "moduleC"] }

200{ "data": { "modules": [ … in new order ] } } Errors: 422 empty / non-uuid ordered_ids


Mentor endpoints — lessons

Nested under a module. Creating or deleting a lesson recomputes the course's total_lessons / total_duration_minutes (from published lessons).

12. List / show / create lessons

GET  /api/v1/mentor/courses/{course}/modules/{module}/lessons        → { "data": { "lessons": [ … ] } }
GET  /api/v1/mentor/courses/{course}/modules/{module}/lessons/{id}   → { "data": { "lesson": { … } } }
POST /api/v1/mentor/courses/{course}/modules/{module}/lessons        → 201 { "data": { "lesson": { … } } }

Create payload (multipart/form-data when uploading files)

Field Type Required Notes
title string yes Max 255
lesson_type enum yes video · text · pdf · quiz · assignment
position int no Auto-positioned when omitted
text_content string for text Article body
video_provider string no e.g. youtube
video_url url for video Max 2048
duration_minutes int no Feeds the course total
time_limit_minutes int no ≥ 1; timer for quiz / assignment (see Timer)
quiz_payload object for quiz See below
assignment_payload object for assignment See below
attachment file for pdf mimes:pdf, ≤ 10 MB
assignment_attachments file[] no Brief files for assignment; pdf,doc,docx,ppt,pptx,xls,xlsx,zip,png,jpg,jpeg, ≤ 10 MB each
is_preview bool no Free teaser (shown to non-enrolled)
is_published bool no Default true

Content requirement by type (enforced): videovideo_url; texttext_content; pdfattachment; quizquiz_payload; assignmentassignment_payload.

Quiz payload shape

{
  "pass_pct": 70,
  "questions": [
    { "prompt": "2 + 2?", "options": ["3", "4", "5"], "correct": [1], "explanation": "…" }
  ]
}

correct is an array of the correct option indices (supports multi-select).

Assignment payload shape

{ "total_marks": 100, "passing_marks": 50, "instructions": "Complete the attached brief." }

passing_marks must be ≤ total_marks (validated). Upload the brief files the learner works from as assignment_attachments[]. The learner uploads their solution via Submit an assignment; the mentor then grades it (see assignment submissions & grading).

13. Update / delete a lesson

PUT|PATCH /api/v1/mentor/courses/{course}/modules/{module}/lessons/{id}
DELETE    /api/v1/mentor/courses/{course}/modules/{module}/lessons/{id}

Update fields are all optional; on update the course totals are recomputed.

14. Reorder lessons

PATCH /api/v1/mentor/courses/{course}/modules/{module}/lessons/reorder

Same contract as module reorder, scoped to the module.

{ "ordered_ids": ["lessonB", "lessonA"] }

Mentor endpoints — assignment submissions & grading

Nested under an assignment lesson the mentor owns. Learners submit files via endpoint 29; these endpoints let the mentor review and grade them.

Submission object

{
  "id": "6f7a…",
  "enrollment_id": "5e6f…",
  "lesson_id": "3c4d…",
  "status": "submitted",
  "status_label": "Submitted",
  "learner": { "id": "8a1c…", "name": "Sam Lee", "email": "sam@example.com" },
  "note": "Here is my work.",
  "file_url": "https://…/solution.pdf",
  "is_late": false,
  "submitted_at": "2026-07-03T09:00:00+00:00",
  "awarded_marks": null,
  "graded_at": null,
  "feedback": null
}

15. List submissions

GET /api/v1/mentor/courses/{course}/modules/{module}/lessons/{lesson}/submissions

Returns every learner submission for the assignment lesson, newest first (each with the submitting learner and their uploaded file).

200{ "data": { "submissions": [ { "…submission object…" } ] } } Errors: 404 lesson not found / not owned

16. Grade a submission

PATCH /api/v1/mentor/courses/{course}/modules/{module}/lessons/{lesson}/submissions/{submission}/grade

Records the awarded marks. A pass (awarded_marks ≥ passing_marks) marks the lesson completed and rolls up the enrollment; a fail returns it to in_progress so the learner may resubmit.

Payload

Field Type Required Notes
awarded_marks int yes ≥ 0 and ≤ total_marks
feedback string no Max 2000

200

{
  "data": {
    "passed": true,
    "submission": { "…submission object, now graded…" }
  }
}

Errors: 422 awarded_marks exceeds the lesson's total_marks · lesson isn't an assignment · submission not yet submitted · 404 lesson / submission not found


Mentor endpoints — pricing

Per-audience price overrides for a course. The (course, audience_type, community_id) triple is unique. Overrides for all three audiences are consulted at enrollment — for a member, a matching community price wins over the public price (see Community linkage).

17. List / create pricing

GET  /api/v1/mentor/courses/{course}/pricing     → { "data": { "pricing": [ { …pricing object… } ] } }
POST /api/v1/mentor/courses/{course}/pricing     → 201 { "data": { "pricing": { … } } }

Create payload

Field Type Required Notes
audience_type enum yes public · community_member · specific_community
community_id uuid conditionally Forbidden for public; required for the community audiences — and must be a community you own
is_free bool no
price decimal conditionally Required unless is_free is true
currency string no Default USD

Errors: 422 public pricing with a community_id, a community audience without one, or a community_id you don't own.

18. Update / delete pricing

PUT|PATCH /api/v1/mentor/courses/{course}/pricing/{id}     → { "data": { "pricing": { … } } }
DELETE    /api/v1/mentor/courses/{course}/pricing/{id}     → { "message": "Pricing deleted successfully." }

Public endpoints — discovery

No authentication required.

19. Browse courses

GET /api/v1/public/courses

Lists published + public courses only (drafts, unlisted, and community_only are excluded). Paginated.

Query parameters

Param Type Notes
search string Matches title / subtitle / description
is_free bool Filter by free/paid
mentor_id uuid Courses by one mentor
sort enum newest (default) · popular · rating · price_low · price_high
per_page int 1–100 (default 15)

200 Response

{
  "status": "success",
  "message": "Courses retrieved successfully.",
  "data": {
    "courses": {
      "data": [ { "…public course object…" } ],
      "links": { "…" },
      "meta": { "current_page": 1, "last_page": 1, "total": 2, "per_page": 15 }
    }
  }
}

Errors: 422 invalid sort

20. Show a course by slug

GET /api/v1/public/courses/{slug}

Returns a published public or unlisted course with its published modules → lessons. community_only, draft, and archived courses return 404.

Public lesson gating: non-is_preview lessons are returned with "locked": true and their content fields (text_content, video_url, quiz_payload, attachment_url) omitted. Preview lessons expose their content.

200 (excerpt)

{
  "data": {
    "course": {
      "id": "9b6f…", "title": "…", "slug": "…", "is_free": false, "price": "49.00",
      "mentor": { "id": "8a1c…", "name": "Jane Doe", "headline": "…", "avatar_url": null },
      "modules": [
        { "id": "1a2b…", "title": "Foundations",
          "lessons": [
            { "id": "3c4d…", "title": "Preview", "lesson_type": "video", "locked": false,
              "is_preview": true, "video_url": "https://…" },
            { "id": "4d5e…", "title": "Locked", "lesson_type": "video", "locked": true,
              "is_preview": false }
          ] }
      ]
    }
  }
}

Errors: 404 not found / not directly accessible


Mentee endpoints — enrollment

All require a Sanctum token.

21. Resolve pricing for me

GET /api/v1/mentee/courses/{course}/pricing

Returns the effective price the current user would pay. Resolution: a public override (if the mentor set one) beats the base price; then, for an active member of a community that prices this course, a matching community price wins — precedence specific_communitycommunity_member, ties broken by the cheapest option. Non-members and anonymous callers resolve to the public price. See Community linkage.

200 (a member whose community grants free access)

{
  "data": { "pricing": { "audience_type": "community_member", "is_free": true, "price": null, "currency": "USD" } }
}

Errors: 404 course not found

22. Enroll

POST /api/v1/mentee/courses/{course}/enroll

Enrolls the user. Free (or free-for-audience) courses grant access immediately; paid courses snapshot the resolved price (is_free: false, price_snapshot) and grant access — real gateway settlement / payment_id is deferred. Increments the course enrollment_count.

Body (optional)

Field Type Required Rules
community_id uuid no Enroll through this community — it must carry the course and have you as an active member. Omitted, the carrying community where you hold the highest level is chosen

The chosen community is recorded on the enrollment and fixes the price charged, the level context for module gating, and where progress XP lands. See Community linkage.

201{ "data": { "enrollment": { "…enrollment object…" } } }

Errors

Code When
422 Already enrolled · course not published · community_only course and you're not an active member of any carrying community · supplied community_id does not carry the course or you're not an active member of it
404 Course not found
401 Unauthenticated

Active members of any community carrying a community_only course can enroll (that community's member price applies); only outsiders are blocked.

23. List / show my enrollments

GET /api/v1/mentee/courses/enrollments            → paginated { "data": { "enrollments": { data, links, meta } } }
GET /api/v1/mentee/courses/enrollments/{id}       → { "data": { "enrollment": { …incl. course… } } }

Query (list): status (enrollment status), per_page (default 15). Errors (show): 404 not the caller's enrollment.

Self-only, and enrollment-shaped. These return enrollment records (progress, price snapshot, status) for the caller. To list the courses another user is enrolled in, use GET /api/v1/user/{username}/courses?type=enrolled (Authentication API, endpoint 42) — it returns course objects, not enrollments, and carries no progress or pricing detail.


Mentee endpoints — learning & progress

Access requires an active/completed enrollment for the course; otherwise 403 (content) or 404 (progress actions resolve the enrollment first).

24. Get course content

GET /api/v1/mentee/courses/{course}/content

Returns the full unlocked curriculum (published modules → lessons) with the learner's per-lesson progress, plus the enrollment. Touches last_accessed_at.

Quiz safety: the learner content strips each quiz question's correct and explanation keys — answer keys are never sent to the client. Assignment lessons expose their brief (total_marks, passing_marks, instructions, attachments) but no grading internals.

200 (excerpt)

{
  "data": {
    "course": {
      "id": "9b6f…", "title": "…",
      "modules": [
        { "id": "1a2b…", "title": "Foundations",
          "lessons": [
            { "id": "3c4d…", "title": "Intro", "lesson_type": "video", "video_url": "https://…",
              "time_limit_minutes": null, "quiz": null, "assignment": null,
              "progress": { "status": "completed", "completed_at": "…", "last_position_seconds": 620, "quiz_score_pct": null } },
            { "id": "4d5e…", "title": "Quiz", "lesson_type": "quiz", "time_limit_minutes": 30,
              "quiz": { "pass_pct": 70, "questions": [ { "prompt": "…", "options": ["…"] } ] },
              "assignment": null, "progress": null },
            { "id": "5e6f…", "title": "Assignment", "lesson_type": "assignment", "time_limit_minutes": 60,
              "quiz": null,
              "assignment": { "total_marks": 100, "passing_marks": 50, "instructions": "…",
                "attachments": [ { "id": "…", "name": "brief.pdf", "url": "https://…" } ] },
              "progress": null }
          ] }
      ]
    },
    "enrollment": { "…enrollment object…" }
  }
}

The progress object also carries assignment/timer fields when present: started_at, submitted_at, submission_note, submission_file_url, is_late, awarded_marks, graded_at, grade_feedback.

Errors: 403 not enrolled

25. Get a single lesson

GET /api/v1/mentee/courses/{course}/lessons/{lesson}

Returns one lesson's full content with this learner's progress attached. The same quiz-answer stripping as endpoint 24 applies. Only published lessons of published modules are reachable; a lesson inside a level-locked module is withheld. Touches last_accessed_at.

200

{
  "data": {
    "lesson": {
      "id": "3c4d…", "module_id": "1a2b…", "title": "Intro",
      "lesson_type": "video", "lesson_type_label": "Video",
      "position": 1, "duration_minutes": 12, "time_limit_minutes": null, "is_preview": false,
      "text_content": null, "video_provider": "youtube", "video_url": "https://…",
      "quiz": null,
      "assignment": null,
      "attachment_url": null,
      "progress": { "status": "completed", "completed_at": "…", "last_position_seconds": 620, "quiz_score_pct": null }
    }
  }
}

Errors: 404 not enrolled / lesson not found (or unpublished) · 403 lesson's module is locked

26. Mark a lesson complete

POST /api/v1/mentee/courses/{course}/lessons/{lesson}/complete

Marks the lesson completed (idempotent) and rolls up the enrollment: completed_lessons_count, progress_pct (floor(completed / published_total × 100)), and — at 100% — flips the enrollment to completed and sets completed_at.

200{ "data": { "enrollment": { … updated … } } } Errors: 404 not enrolled / lesson not in course · 422 lesson not part of the course

27. Start a lesson (timer)

POST /api/v1/mentee/courses/{course}/lessons/{lesson}/start

Stamps started_at on the learner's progress so a timed lesson's window can be enforced. Required before submitting a quiz/assignment that has a time_limit_minutes. Calling it again does not reset the clock. Untimed lessons don't need it.

200{ "data": { "started_at": "2026-07-03T09:00:00+00:00", "time_limit_minutes": 30 } } Errors: 404 not enrolled / lesson not found

28. Submit a quiz

POST /api/v1/mentee/courses/{course}/lessons/{lesson}/quiz

Scores the submission and, on a pass (score ≥ pass_pct), marks the lesson complete. A question is correct when the selected indices exactly match the expected set. If the lesson has a time_limit_minutes, it must have been started and the window not exceeded — otherwise 422 (a quiz is a hard cutoff; see Timer).

Payload

{ "answers": [ [1], [0, 2] ] }

answers[i] is the array of selected option indices for question i.

200

{
  "data": {
    "score_pct": 100,
    "passed": true,
    "enrollment": { "…enrollment object…" }
  }
}

Errors: 422 lesson is not a quiz · not started / time limit exceeded · 404 not enrolled / lesson not found

29. Submit an assignment

POST /api/v1/mentee/courses/{course}/lessons/{lesson}/assignment     (multipart/form-data)

Uploads the learner's solution file and sets the lesson submitted, awaiting the mentor's grade. Resubmitting replaces the file and clears any prior grade.

A submission past the lesson's time_limit_minutes is still accepted but flagged is_late: true, and the note becomes required as the reason (see Timer).

Payload

Field Type Required Notes
file file yes pdf,doc,docx,ppt,pptx,xls,xlsx,zip,png,jpg,jpeg, ≤ 10 MB
note string conditionally Optional normally; required when the submission is late

201

{
  "data": { "submission": { "…submission object (status: submitted)…" } }
}

Errors: 422 lesson is not an assignment · late submission without a note · missing/invalid file · 404 not enrolled / lesson not found

30. Save video position

PATCH /api/v1/mentee/courses/{course}/lessons/{lesson}/position

Persists a resume position without completing the lesson (won't demote an already-completed lesson).

Payload

{ "position_seconds": 125 }

200{ "message": "Position saved." } Errors: 422 missing/negative position_seconds · 404 not enrolled


Progress model reference

enroll ─► ACTIVE ──(mark complete / pass quiz / assignment graded pass)──► progress_pct rises
                        │
                        └─(all published lessons complete)─► COMPLETED (+ completed_at)
  • progress_pct = floor(completed_lessons / published_lesson_count × 100).
  • A lesson counts as completed via mark-complete, a passing quiz, or an assignment graded as a pass.
  • video/text/pdf lessons complete via endpoint 26; quiz lessons complete by passing endpoint 28 (or can be force-completed via 26).
  • assignment lessons go in_progresssubmitted (learner uploads via endpoint 29) → completed when the mentor grades a pass (endpoint 16). A graded fail drops back to in_progress so the learner can resubmit.

Timer / time limits

A quiz or assignment lesson may carry a time_limit_minutes. Enforcement is server-side, keyed off started_at (set by endpoint 27):

  • No time_limit_minutes → no timer; start isn't needed.
  • Quiz (hard cutoff) — the lesson must be started, and a submission after started_at + time_limit_minutes is rejected with 422.
  • Assignment (late allowed) — a late submission is accepted but flagged is_late: true and its note becomes required (the reason for lateness). The mentor sees is_late when grading.

Community linkage

A course can be carried by several communities, priced independently in each. Three pieces combine; all are live.

1. Carrying communities — community_course pivot. The mentor sets the set via community_ids on create/update (§2, §4); every entry must be a community they own, and at least one is required for community_only visibility. The set drives:

  • Visibilitycommunity_only courses stay hidden from public browse/slug lookup and are surfaced only through a carrying community's gated catalog (GET /{mentee|mentor}/communities/{community}/courses — same endpoint on both surfaces). Enrolling requires active membership of any carrying community.
  • Level gatingunlock_at_level is only accepted on a course some community carries; levels are per-community, so an unlinked course has no level to check against.

2. Enrolling community — course_enrollments.community_id. Because levels, XP and price are all per-community, an enrollment records which community the access came through. It is fixed at enrollment and drives:

  • Module locking — a module's unlock_at_level locks it until the learner reaches that level in the enrolling community. In learner content, locked modules are withheld.
  • XPlesson_completed (+20) / course_completed (+100) are awarded in the enrolling community only. Awarding across every carrying community would multiply one lesson's XP.
  • Price — see below.

Resolution order when enrolling: an explicit community_id in the request body (which must both carry the course and have you as an active member), otherwise the carrying community where you hold the highest level, otherwise null for a public/standalone enrollment. A null enrolling community means no level context (gated modules stay locked) and no XP.

3. Per-community pricing — course_pricing.community_id. A mentor adds pricing rows (§17) scoped to any community they own. Pricing resolves in the community's context: browsing or enrolling through a community quotes that community's price, not the best price the member holds elsewhere. A course free in community A and $9 in community B costs a member of both nothing in A and $9 in B.

Within a single community, precedence is specific_communitycommunity_memberpublic (ties broken by the cheapest option). Non-members, anonymous callers, and contexts with no community always resolve to the public price.

Community deletion. Deleting a community unlinks its courses. A community_only course left carried by nothing would be visible to nobody, so it is automatically downgraded to unlisted — still hidden from search, still reachable by direct link. Courses carried by another community are unaffected.

See the Community API doc's "Course gating" section for the membership/level/XP mechanics from the community side.

Deferred / cross-phase (not yet enforced)

Item Column / enum present Unblocks with
Premium-exclusive courses courses.is_exclusive_to_premium Phase 7 — Platform Subscriptions
Real paid settlement / escrow course_enrollments.payment_id Payments module
"New lesson published" notification Phase 10 — Notifications
Reviews / ratings (feeds average_rating) courses.average_rating Not yet built (standalone-buildable)