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]/notesand/bookmarks) — used by the lesson player side panel - Aggregate (
/api/me/notesand/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
}
contentis trimmed, 1–10,000 charstimestampSecondsis 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:
labelis optional (1–120 chars); empty string is treated as nulltimestampSecondsis 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
| Param | Default | Notes |
|---|---|---|
page | 1 | |
limit | 20 | Max 100 |
courseId | — | Filter to one course |
search | — | Case-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.
Related
- Lesson types and content: Architecture — Lessons
- Captions API — sister panel on the lesson player