DocsAPI Reference

Notes & Bookmarks API

Per-lesson and aggregate "my notes" / "my bookmarks" endpoints

Per-student lesson notes and bookmarks (Sprint γ Track 3). Two endpoint shapes:

  • Per-lesson (/api/lessons/[id]/notes and /bookmarks) — used by the lesson player side panel
  • Aggregate (/api/me/notes and /api/me/bookmarks) — used by the global "My Notes" and "My Bookmarks" pages

All reads are filtered by studentId = user.id. There is no instructor view of a student's notes and no cross-student visibility.

Creating a note or bookmark requires an ACTIVE enrollment in the lesson's course. Existing notes survive a status drop to DROPPED — they just can't be added to.

Per-lesson notes

GET /api/lessons/[id]/notes

Returns the caller's notes for this lesson, newest first.

{
  "data": [
    {
      "id": "ln_...",
      "lessonId": "...",
      "studentId": "...",
      "enrollmentId": "...",
      "content": "Important — exam tip",
      "timestampSeconds": 432,
      "createdAt": "...",
      "updatedAt": "..."
    }
  ]
}

POST /api/lessons/[id]/notes

Body:

{
  "content": "Note text",
  "timestampSeconds": 120  // optional, video lessons only
}
  • content is trimmed, 1–10,000 chars
  • timestampSeconds is 0–86,400 (24h cap), or null

Errors: 403 ENROLLMENT_REQUIRED if not actively enrolled, 404 NOT_FOUND if the lesson doesn't exist in the institution.

PATCH /api/lessons/[id]/notes/[noteId]

Update content or timestampSeconds. Same validation. Caller must own the note.

DELETE /api/lessons/[id]/notes/[noteId]

Delete. Caller must own the note.

Per-lesson bookmarks

Same shape as notes, with two differences:

  • label is optional (1–120 chars); empty string is treated as null
  • timestampSeconds is the primary sort key — bookmarks list orders by timestamp ascending so the panel reads in playback order

GET /api/lessons/[id]/bookmarks

{
  "data": [
    {
      "id": "lb_...",
      "lessonId": "...",
      "studentId": "...",
      "timestampSeconds": 60,
      "label": "Intro recap",
      "createdAt": "..."
    }
  ]
}

POST /api/lessons/[id]/bookmarks

{
  "timestampSeconds": 60,
  "label": "Intro recap"
}

PATCH /api/lessons/[id]/bookmarks/[bookmarkId]

Update label or timestampSeconds.

DELETE /api/lessons/[id]/bookmarks/[bookmarkId]

Delete.

Aggregate — GET /api/me/notes

Every note across every course the student has touched. Paginated, optionally filtered.

Query

ParamDefaultNotes
page1
limit20Max 100
courseIdFilter to one course
searchCase-insensitive substring on content, 1–200 chars

Response

Standard paginated envelope. Each row is enriched with lesson + course context:

{
  "data": [
    {
      "id": "ln_...",
      "lessonId": "...",
      "content": "...",
      "timestampSeconds": 432,
      "createdAt": "...",
      "updatedAt": "...",
      "lessonTitle": "Cell Membrane",
      "lessonType": "VIDEO",
      "courseId": "...",
      "courseTitle": "Biology Foundations"
    }
  ],
  "meta": { "total": 42, "page": 1, "perPage": 20, "totalPages": 3 }
}

Sort: updatedAt desc — most recently edited first.

Aggregate — GET /api/me/bookmarks

Same shape as /api/me/notes with one extra field per row: timestampLabel (server-formatted m:ss or h:mm:ss) so the UI doesn't reimplement the formatting.

{
  "data": [
    {
      "id": "lb_...",
      "label": "Intro recap",
      "timestampSeconds": 60,
      "timestampLabel": "1:00",
      "lessonTitle": "Cell Membrane",
      "courseTitle": "Biology Foundations",
      ...
    }
  ]
}

Search filters on label. Sort: createdAt desc.

Mobile

Every endpoint above is exposed at the same paths for mobile via the /api/v1/* rewrite layer (bearer-authed). Same payload shapes.