{"openapi":"3.1.0","info":{"title":"Chalkenso website API","version":"1.0.0","summary":"Serve your courses on your own website, send learner feedback back, and keep learners' notes.","description":"Your website keeps its design and its URLs and reads the courses you publish in Chalkenso. Learner feedback from your pages goes back into the lesson margin in Chalkenso, next to your instructors' notes.\n\n**Authentication.** Every endpoint except images needs a site key: `Authorization: Bearer site_…`. Create one in Chalkenso under *Website*. A site key can read published courses (`content:read`), and send learner feedback and keep learners' private notes (`feedback:write`); it can't change courses. Use it from your server only, never in browser code.\n\n**Responses.** Success is `{ \"data\": … }`; errors are `{ \"error\": { \"code\", \"message\" } }` with the HTTP status. Successful reads carry a weak `ETag`: send it back as `If-None-Match` to get an empty `304 Not Modified`.\n\n**Loading lessons.** Outlines (`GET /courses/{courseSlug}`, `GET /access/{token}`) carry titles only. Load a lesson's text when a reader opens it, one request per lesson (`GET …/lessons/{lesson}`), and cache it: don't fetch every lesson to build a page.\n\n**Lesson text.** Always Markdown that renders anywhere: images at stable URLs, links to other lessons as `#lesson-<slug>`, callouts as `> [!note]`. Add `?include=html` to get it rendered as well.\n\n**Refreshing.** Register a webhook in *Website* → *Publish notifications* and refresh the pages each event names: `content.published`, `content.unpublished` and `course.updated` (see *Webhooks* below). Treat any other event as \"refresh everything\", and keep a time-based refresh as a fallback.\n\n**For AI coding agents.** Start at [/llms.txt](https://chalkenso.com/llms.txt); the step-by-step integration guide is [/docs/website.md](https://chalkenso.com/docs/website.md)."},"externalDocs":{"description":"Integration guide","url":"https://chalkenso.com/docs/website"},"servers":[{"url":"https://chalkenso.com/api/v1","description":"Chalkenso"}],"security":[{"siteKey":[]}],"tags":[{"name":"Courses","description":"Published courses and their free lessons, for public pages."},{"name":"Learner links","description":"What one learner reads, through their personal link: their group's own copy of the course."},{"name":"Feedback","description":"Learner feedback on a passage. It appears in the lesson's margin in Chalkenso. Needs `feedback:write`."},{"name":"Notes","description":"A learner's private notes on passages: the same notes they see on Chalkenso's own learner pages. Only the learner sees them, never the team. Needs `feedback:write`."},{"name":"Learner signals","description":"The one-tap lesson question, check questions and surveys, for sites that draw their own. Most sites use the drop-in learner script instead (`https://chalkenso.com/embed/learner.v1.js`), which also measures anonymous reading statistics. Needs `feedback:write`."},{"name":"Images","description":"Stable image URLs used in lesson HTML and course covers."},{"name":"About","description":"Where this API and its docs are."}],"paths":{"/":{"get":{"tags":["About"],"operationId":"getApiIndex","summary":"API index","description":"Links to this spec and the docs. No key needed.","security":[],"responses":{"200":{"description":"Links","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApiIndex"}}},"example":{"data":{"name":"Chalkenso website API","version":"1.0.0","openapi":"https://chalkenso.com/api/v1/openapi.json","reference":"https://chalkenso.com/docs/reference","guide":"https://chalkenso.com/docs/website","llms":"https://chalkenso.com/llms.txt"}}}}}}}},"/courses":{"get":{"tags":["Courses"],"operationId":"listCourses","summary":"List published courses","description":"Every course with a published main version, pinned first, then by title. Show the ones with `listed: true` in a public catalogue; unlisted courses are still readable by slug.","parameters":[{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"Published courses","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Course"}}}},"example":{"data":[{"id":"cmg8k2v1x0000qz0l8f3k1m2n","slug":"web-development-2","title":"Web Development II","summary":"Build and ship a full-stack recipe app with Flask, Postgres and Docker.","outcome":"You deploy a working web app with a database, an API and tests.","tags":["Python","Flask","Docker"],"githubUrl":"https://github.com/northwind-academy/web-development-2","listed":true,"pinned":false,"enrolUrl":"https://example.com/enrol/web-development-2","coverImageUrl":"https://chalkenso.com/api/v1/assets/cmg8k2v1x0009qz0l5b6c7d8e","cover":{"url":"https://chalkenso.com/api/v1/assets/cmg8k2v1x0009qz0l5b6c7d8e","alt":"The finished Recipe Keeper app in a browser","width":1200,"height":630},"partners":[{"id":"cmg8k2v1x0040qz0l6p7a8r9t","name":"Northwind Labs","websiteUrl":"https://example.com/northwind-labs","logoUrl":"https://example.com/northwind-labs/logo.svg","description":"Hosts the evening groups."}],"join":{"type":"link","url":"https://example.com/enrol/web-development-2","partner":null},"freeLessons":2,"lessonCount":5,"version":{"id":"cmg8k2v1x0002qz0l1a2b3c4d","label":"v1","publishedAt":"2026-10-01T09:00:00.000Z"},"reviews":{"score":4.6,"count":18,"quotes":[{"text":"The check-your-work steps after every exercise meant I always knew I was on track.","date":"2026-10-08"}]},"updatedAt":"2026-10-08T11:08:00.000Z"}]}}}},"304":{"$ref":"#/components/responses/NotModified"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/courses/{courseSlug}":{"get":{"tags":["Courses"],"operationId":"getCourse","summary":"Get a course with its outline","description":"The course, its parts (with descriptions), and every published lesson in order, titles only. Render this as the course page, then load each lesson's text with `GET /courses/{courseSlug}/lessons/{lesson}` when a reader opens it. `free` says which lessons anyone may read.","parameters":[{"$ref":"#/components/parameters/CourseSlug"},{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"The course","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CourseDetail"}}},"example":{"data":{"id":"cmg8k2v1x0000qz0l8f3k1m2n","slug":"web-development-2","title":"Web Development II","summary":"Build and ship a full-stack recipe app with Flask, Postgres and Docker.","outcome":"You deploy a working web app with a database, an API and tests.","tags":["Python","Flask","Docker"],"githubUrl":"https://github.com/northwind-academy/web-development-2","listed":true,"pinned":false,"enrolUrl":"https://example.com/enrol/web-development-2","coverImageUrl":"https://chalkenso.com/api/v1/assets/cmg8k2v1x0009qz0l5b6c7d8e","cover":{"url":"https://chalkenso.com/api/v1/assets/cmg8k2v1x0009qz0l5b6c7d8e","alt":"The finished Recipe Keeper app in a browser","width":1200,"height":630},"partners":[{"id":"cmg8k2v1x0040qz0l6p7a8r9t","name":"Northwind Labs","websiteUrl":"https://example.com/northwind-labs","logoUrl":"https://example.com/northwind-labs/logo.svg","description":"Hosts the evening groups."}],"join":{"type":"link","url":"https://example.com/enrol/web-development-2","partner":null},"freeLessons":2,"lessonCount":5,"version":{"id":"cmg8k2v1x0002qz0l1a2b3c4d","label":"v1","publishedAt":"2026-10-01T09:00:00.000Z"},"reviews":{"score":4.6,"count":18,"quotes":[{"text":"The check-your-work steps after every exercise meant I always knew I was on track.","date":"2026-10-08"}]},"updatedAt":"2026-10-08T11:08:00.000Z","parts":[{"id":"cmg8k2v1x0001qz0l2c0p4r1a","title":"Part I – Server-rendered","description":"Pages rendered on the server with Flask templates, then a JSON API."}],"lessons":[{"id":"cmg8k2v1x0004qz0l7d3h9a2b","slug":"course-intro-docker-setup","number":1,"title":"Course intro, Docker setup and your first page","shortDescription":"Run Flask and Postgres with one command and serve your first page.","part":{"id":"cmg8k2v1x0001qz0l2c0p4r1a","title":"Part I – Server-rendered"},"publishedAt":"2026-10-01T09:00:00.000Z","free":true}]}}}}},"304":{"$ref":"#/components/responses/NotModified"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No published course with this slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"course_not_found":{"summary":"course_not_found","value":{"error":{"code":"course_not_found","message":"No published course with this slug."}}}}}}}}}},"/courses/{courseSlug}/lessons/{lesson}":{"get":{"tags":["Courses"],"operationId":"getFreeLesson","summary":"Get a free lesson","description":"One free lesson, as last published. Call it when a reader opens the lesson, not for every lesson up front. Lessons past the free ones answer `403 learner_link_required`: read them with the learner's link (`GET /access/{token}/lessons/{lesson}`), or show the course's `enrolUrl`.","parameters":[{"$ref":"#/components/parameters/CourseSlug"},{"$ref":"#/components/parameters/Lesson"},{"$ref":"#/components/parameters/Include"},{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"The lesson","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FreeLesson"}}},"example":{"data":{"id":"cmg8k2v1x0004qz0l7d3h9a2b","slug":"course-intro-docker-setup","number":1,"title":"Course intro, Docker setup and your first page","shortDescription":"Run Flask and Postgres with one command and serve your first page.","part":{"id":"cmg8k2v1x0001qz0l2c0p4r1a","title":"Part I – Server-rendered"},"publishedAt":"2026-10-01T09:00:00.000Z","markdown":"## Run Flask and Postgres with one command\n\nEverything runs in Docker, so the whole room has the same setup.\n\n![](https://chalkenso.com/api/v1/assets/cmg8k2v1x0010qz0l3e4f5g6h)\n\n> [!tip] Check your work\n> Run `docker compose ps`: both `web` and `db` should be running.\n\nNext, [build the API](#lesson-rest-api-crud).","revision":3,"sections":["Run Flask and Postgres with one command"],"previous":null,"next":{"slug":"rest-api-crud","title":"A REST API with full CRUD"},"free":true,"course":{"slug":"web-development-2","title":"Web Development II"}}}}}},"304":{"$ref":"#/components/responses/NotModified"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key lacks `content:read`, or the lesson is for learners only.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"learner_link_required":{"summary":"learner_link_required","value":{"error":{"code":"learner_link_required","message":"This lesson is for learners: read it through their learner link."}}},"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}}}}}},"404":{"description":"No such published course or lesson.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"course_not_found":{"summary":"course_not_found","value":{"error":{"code":"course_not_found","message":"No published course with this slug."}}},"lesson_not_found":{"summary":"lesson_not_found","value":{"error":{"code":"lesson_not_found","message":"No published lesson with this slug or id in the course."}}}}}}}}}},"/access/{token}":{"get":{"tags":["Learner links"],"operationId":"getLearnerAccess","summary":"Resolve a learner link","description":"Who the link belongs to, their group, the parts and every lesson they may read, titles only: the group's own copy of the course, which can differ from the main course. Load each lesson's text with `GET /access/{token}/lessons/{lesson}` when the learner opens it. `preview: true` marks a team preview link: it reads drafts and can't send feedback. Show the feedback box only when `canSendFeedback` is true, and notes only when `canKeepNotes` is true. Show `group.partners` under \"In partnership with\" when there are any, otherwise `course.partners`.","parameters":[{"$ref":"#/components/parameters/Token"},{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"The learner link","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Access"}}},"example":{"data":{"learner":{"name":"Nina Kos"},"canSendFeedback":true,"canKeepNotes":true,"preview":false,"expiresAt":"2026-11-08T23:59:59.000Z","group":{"id":"cmg8k2v1x0030qz0l1q2w3e4r","name":"Evening group · Autumn 2026","startsAt":"2026-09-19T00:00:00.000Z","endsAt":"2026-11-08T00:00:00.000Z","slackHandle":"#web-dev-autumn","description":"Tuesdays and Thursdays, 18:00–21:00.","partners":[]},"course":{"id":"cmg8k2v1x0000qz0l8f3k1m2n","slug":"web-development-2","title":"Web Development II","summary":"Build and ship a full-stack recipe app with Flask, Postgres and Docker.","githubUrl":"https://github.com/northwind-academy/web-development-2","partners":[{"id":"cmg8k2v1x0040qz0l6p7a8r9t","name":"Northwind Labs","websiteUrl":"https://example.com/northwind-labs","logoUrl":"https://example.com/northwind-labs/logo.svg","description":"Hosts the evening groups."}]},"version":{"id":"cmg8k2v1x0031qz0l5t6y7u8i","label":"Evening group · Autumn 2026"},"parts":[{"id":"cmg8k2v1x0001qz0l2c0p4r1a","title":"Part I – Server-rendered","description":"Pages rendered on the server with Flask templates, then a JSON API."}],"lessons":[{"id":"cmg8k2v1x0004qz0l7d3h9a2b","slug":"course-intro-docker-setup","number":1,"title":"Course intro, Docker setup and your first page","shortDescription":"Run Flask and Postgres with one command and serve your first page.","part":{"id":"cmg8k2v1x0001qz0l2c0p4r1a","title":"Part I – Server-rendered"},"publishedAt":"2026-10-01T09:00:00.000Z"}],"signals":{"switches":{"lessonQuestion":true,"checks":true,"surveys":true,"reading":true},"readingAllowed":true,"readingOptOut":false,"quietMode":false,"lessonOrder":["cmg8k2v1x0004qz0l7d3h9a2b"],"lessons":{"cmg8k2v1x0004qz0l7d3h9a2b":{"revision":3,"answer":null,"updatedAfterFeedback":true,"check":{"question":"Where does the database connection string belong?","options":["In .env, read by the server","In the frontend code","In the code, committed to Git"]}}},"survey":{"end":{"open":false,"answered":false,"extraQuestion":null},"mid":{"open":true,"answered":false,"extraQuestion":null}},"quickChecks":[],"classNote":{"text":"Several of you found volumes confusing: lesson 1 now has a check-your-work step.","at":"2026-10-09T08:00:00.000Z"},"lessonTitles":{"cmg8k2v1x0004qz0l7d3h9a2b":"Course intro, Docker setup and your first page"}}}}}}},"304":{"$ref":"#/components/responses/NotModified"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The learner link does not exist (or belongs to another organisation).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"course_not_ready":{"summary":"course_not_ready","value":{"error":{"code":"course_not_ready","message":"The course for this link has no curriculum yet."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}}},"/access/{token}/lessons/{lesson}":{"get":{"tags":["Learner links"],"operationId":"getLearnerLesson","summary":"Get a lesson through a learner's link","description":"One lesson from the learner's group copy, as last published (preview links read the current draft). Call it when the learner opens the lesson, not for every lesson up front.","parameters":[{"$ref":"#/components/parameters/Token"},{"$ref":"#/components/parameters/Lesson"},{"$ref":"#/components/parameters/Include"},{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"The lesson","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/LessonContent"}}},"example":{"data":{"id":"cmg8k2v1x0004qz0l7d3h9a2b","slug":"course-intro-docker-setup","number":1,"title":"Course intro, Docker setup and your first page","shortDescription":"Run Flask and Postgres with one command and serve your first page.","part":{"id":"cmg8k2v1x0001qz0l2c0p4r1a","title":"Part I – Server-rendered"},"publishedAt":"2026-10-01T09:00:00.000Z","markdown":"## Run Flask and Postgres with one command\n\nEverything runs in Docker, so the whole room has the same setup.\n\n![](https://chalkenso.com/api/v1/assets/cmg8k2v1x0010qz0l3e4f5g6h)\n\n> [!tip] Check your work\n> Run `docker compose ps`: both `web` and `db` should be running.\n\nNext, [build the API](#lesson-rest-api-crud).","revision":3,"sections":["Run Flask and Postgres with one command"],"previous":null,"next":{"slug":"rest-api-crud","title":"A REST API with full CRUD"}}}}}},"304":{"$ref":"#/components/responses/NotModified"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such learner link, or no such lesson in it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"lesson_not_found":{"summary":"lesson_not_found","value":{"error":{"code":"lesson_not_found","message":"This learner link has no lesson with this slug or id."}}},"course_not_ready":{"summary":"course_not_ready","value":{"error":{"code":"course_not_ready","message":"The course for this link has no curriculum yet."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}}},"/access/{token}/lessons/{lesson}/pulse":{"post":{"tags":["Learner signals"],"operationId":"sendLessonPulse","summary":"Answer the lesson question or check question","description":"At the end of a lesson: \"Could you explain this lesson to someone else?\" (`answer`), or the approved check question (`optionIndex` / `notSure`) when `signals.lessons[id].check` is set. Then, optionally, \"Which part?\" (`sectionIndex` into the lesson's `sections`) after either answer, and a one-line `note` after Not yet. One answer per learner and lesson revision; a later request changes it.","parameters":[{"$ref":"#/components/parameters/Token"},{"$ref":"#/components/parameters/Lesson"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PulseInput"},"example":{"answer":"NOT_YET"}}}},"responses":{"200":{"description":"Saved","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PulseResult"}}},"example":{"data":{"check":null}}}}},"400":{"description":"Not JSON, or not a valid answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_json":{"summary":"invalid_json","value":{"error":{"code":"invalid_json","message":"Send a JSON body."}}},"invalid_answer":{"summary":"invalid_answer","value":{"error":{"code":"invalid_answer","message":"Unknown answer."}}},"answer_required":{"summary":"answer_required","value":{"error":{"code":"answer_required","message":"Answer the question first."}}},"invalid_option":{"summary":"invalid_option","value":{"error":{"code":"invalid_option","message":"Pick one of the options."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key lacks `feedback:write`, or the link is not a personal learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}},"personal_link_required":{"summary":"personal_link_required","value":{"error":{"code":"personal_link_required","message":"Only personal learner links can answer."}}},"preview_link":{"summary":"preview_link","value":{"error":{"code":"preview_link","message":"Preview links don’t collect answers."}}}}}}},"404":{"description":"No such learner link, lesson or check question.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"lesson_not_found":{"summary":"lesson_not_found","value":{"error":{"code":"lesson_not_found","message":"This learner link has no lesson with this slug or id."}}},"check_not_found":{"summary":"check_not_found","value":{"error":{"code":"check_not_found","message":"This lesson has no check question."}}}}}}},"409":{"description":"The feature is off for this course or group, or the survey is not open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"feature_off":{"summary":"feature_off","value":{"error":{"code":"feature_off","message":"The lesson question is off for this group."}}},"survey_closed":{"summary":"survey_closed","value":{"error":{"code":"survey_closed","message":"The halfway check is not open."}}},"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}},"429":{"description":"Too many requests for this learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again in a minute."}}}}}}}}}},"/access/{token}/survey":{"post":{"tags":["Learner signals"],"operationId":"sendSurveyAnswer","summary":"Answer the end-of-course survey or halfway check","description":"Show the survey when `signals.survey.end.open` (or at the end of the last lesson) or `signals.survey.mid.open`, and not `answered`. The score (1–5, labelled faces) is the only required answer; send each optional answer as it comes. Scores from at least five learners, and quotes they allowed, appear as `reviews` on `GET /courses/{courseSlug}`.","parameters":[{"$ref":"#/components/parameters/Token"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SurveyInput"},"example":{"kind":"END","score":5}}}},"responses":{"200":{"description":"Saved","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SurveyResult"}}},"example":{"data":{"checks":null}}}}},"400":{"description":"Not JSON, or not a valid answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_json":{"summary":"invalid_json","value":{"error":{"code":"invalid_json","message":"Send a JSON body."}}},"invalid_kind":{"summary":"invalid_kind","value":{"error":{"code":"invalid_kind","message":"Unknown survey."}}},"invalid_score":{"summary":"invalid_score","value":{"error":{"code":"invalid_score","message":"Pick one of the five faces."}}},"score_required":{"summary":"score_required","value":{"error":{"code":"score_required","message":"Pick one of the five faces first."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key lacks `feedback:write`, or the link is not a personal learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}},"personal_link_required":{"summary":"personal_link_required","value":{"error":{"code":"personal_link_required","message":"Only personal learner links can answer."}}},"preview_link":{"summary":"preview_link","value":{"error":{"code":"preview_link","message":"Preview links don’t collect answers."}}}}}}},"404":{"description":"The learner link does not exist (or belongs to another organisation).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"course_not_ready":{"summary":"course_not_ready","value":{"error":{"code":"course_not_ready","message":"The course for this link has no curriculum yet."}}}}}}},"409":{"description":"The feature is off for this course or group, or the survey is not open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"feature_off":{"summary":"feature_off","value":{"error":{"code":"feature_off","message":"The lesson question is off for this group."}}},"survey_closed":{"summary":"survey_closed","value":{"error":{"code":"survey_closed","message":"The halfway check is not open."}}},"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}},"429":{"description":"Too many requests for this learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"rate_limited","value":{"error":{"code":"rate_limited","message":"Too many requests. Try again in a minute."}}}}}}}}}},"/access/{token}/feedback":{"get":{"tags":["Feedback"],"operationId":"listLearnerFeedback","summary":"List the learner's feedback on a lesson","description":"Feedback this learner left on the lesson, newest first, with any reply from the team (`adminResponse`).","parameters":[{"$ref":"#/components/parameters/Token"},{"name":"lesson","in":"query","required":true,"description":"Lesson slug or id","schema":{"type":"string"},"example":"course-intro-docker-setup"},{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"Feedback, newest first","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Feedback"}}}},"example":{"data":[{"id":"cmg8k2v1x0020qz0l9h8g7f6e","selectedText":"The named volume keeps the database files outside the container.","context":"The named volume keeps the database files outside the container, so your recipes survive a restart.","body":"Why does the data survive docker compose down? Isn't down supposed to delete everything?","status":"OPEN","adminResponse":null,"adminRespondedAt":null,"createdAt":"2026-10-08T18:42:00.000Z"}]}}}},"304":{"$ref":"#/components/responses/NotModified"},"400":{"description":"The `lesson` query parameter is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"lesson_required":{"summary":"lesson_required","value":{"error":{"code":"lesson_required","message":"Say which lesson: ?lesson=<lesson slug or id>."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such learner link, or no such lesson in it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"lesson_not_found":{"summary":"lesson_not_found","value":{"error":{"code":"lesson_not_found","message":"This learner link has no lesson with this slug or id."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}},"post":{"tags":["Feedback"],"operationId":"sendLearnerFeedback","summary":"Send feedback on a passage","description":"Feedback on the passage the learner selected. It appears in the lesson's margin in Chalkenso, attributed to the learner. Only personal learner links can send feedback (`canSendFeedback`). Send it from your server: the browser posts to your own route, your route calls this.","parameters":[{"$ref":"#/components/parameters/Token"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackInput"},"example":{"lesson":"course-intro-docker-setup","selectedText":"The named volume keeps the database files outside the container.","context":"The named volume keeps the database files outside the container, so your recipes survive a restart.","body":"Why does the data survive docker compose down? Isn't down supposed to delete everything?"}}}},"responses":{"201":{"description":"Saved","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Feedback"}}},"example":{"data":{"id":"cmg8k2v1x0020qz0l9h8g7f6e","selectedText":"The named volume keeps the database files outside the container.","context":"The named volume keeps the database files outside the container, so your recipes survive a restart.","body":"Why does the data survive docker compose down? Isn't down supposed to delete everything?","status":"OPEN","adminResponse":null,"adminRespondedAt":null,"createdAt":"2026-10-08T18:42:00.000Z"}}}}},"400":{"description":"The body is not JSON, or the feedback is empty or too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_json":{"summary":"invalid_json","value":{"error":{"code":"invalid_json","message":"Send a JSON body."}}},"feedback_rejected":{"summary":"feedback_rejected (empty or too long)","value":{"error":{"code":"feedback_rejected","message":"Comment message is required."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key lacks `feedback:write`, or the link is not a personal learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}},"feedback_rejected":{"summary":"feedback_rejected (not a personal link)","value":{"error":{"code":"feedback_rejected","message":"Commenting is only available on personal access links."}}}}}}},"404":{"description":"No such learner link, or no such lesson in it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"lesson_not_found":{"summary":"lesson_not_found","value":{"error":{"code":"lesson_not_found","message":"This learner link has no lesson with this slug or id."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}}},"/access/{token}/feedback/{feedbackId}":{"delete":{"tags":["Feedback"],"operationId":"deleteLearnerFeedback","summary":"Delete the learner's own feedback","parameters":[{"$ref":"#/components/parameters/Token"},{"name":"feedbackId","in":"path","required":true,"description":"Feedback id","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such learner link, or no feedback with this id from this learner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"feedback_not_found":{"summary":"feedback_not_found","value":{"error":{"code":"feedback_not_found","message":"Comment not found."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}}},"/access/{token}/notes":{"get":{"tags":["Notes"],"operationId":"listLearnerNotes","summary":"List the learner's notes on a lesson","description":"The learner's private notes on the lesson, newest first. A link that can't keep notes (`canKeepNotes: false`) gets an empty list.","parameters":[{"$ref":"#/components/parameters/Token"},{"name":"lesson","in":"query","required":true,"description":"Lesson slug or id","schema":{"type":"string"},"example":"course-intro-docker-setup"},{"$ref":"#/components/parameters/IfNoneMatch"}],"responses":{"200":{"description":"Notes, newest first","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Note"}}}},"example":{"data":[{"id":"cmg8k2v1x0025qz0l4k5j6h7g","selectedText":"docker compose ps","context":"Run `docker compose ps`: both `web` and `db` should be running.","body":"Run this before every class, the db container sometimes stops on my laptop.","createdAt":"2026-10-08T19:05:00.000Z","updatedAt":"2026-10-08T19:05:00.000Z"}]}}}},"304":{"$ref":"#/components/responses/NotModified"},"400":{"description":"The `lesson` query parameter is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"lesson_required":{"summary":"lesson_required","value":{"error":{"code":"lesson_required","message":"Say which lesson: ?lesson=<lesson slug or id>."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such learner link, or no such lesson in it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"lesson_not_found":{"summary":"lesson_not_found","value":{"error":{"code":"lesson_not_found","message":"This learner link has no lesson with this slug or id."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}},"post":{"tags":["Notes"],"operationId":"addLearnerNote","summary":"Add a note on a passage","description":"A private note on the passage the learner selected. Only the learner sees it, here and on Chalkenso's learner pages. Only personal learner links keep notes (`canKeepNotes`). Send it from your server: the browser posts to your own route, your route calls this.","parameters":[{"$ref":"#/components/parameters/Token"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteInput"},"example":{"lesson":"course-intro-docker-setup","selectedText":"docker compose ps","context":"Run `docker compose ps`: both `web` and `db` should be running.","body":"Run this before every class, the db container sometimes stops on my laptop."}}}},"responses":{"201":{"description":"Saved","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Note"}}},"example":{"data":{"id":"cmg8k2v1x0025qz0l4k5j6h7g","selectedText":"docker compose ps","context":"Run `docker compose ps`: both `web` and `db` should be running.","body":"Run this before every class, the db container sometimes stops on my laptop.","createdAt":"2026-10-08T19:05:00.000Z","updatedAt":"2026-10-08T19:05:00.000Z"}}}}},"400":{"description":"The body is not JSON, or the note is empty or too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_json":{"summary":"invalid_json","value":{"error":{"code":"invalid_json","message":"Send a JSON body."}}},"note_rejected":{"summary":"note_rejected (empty or too long)","value":{"error":{"code":"note_rejected","message":"Note text is required."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key lacks `feedback:write`, or the link is not a personal learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}},"notes_unavailable":{"summary":"notes_unavailable","value":{"error":{"code":"notes_unavailable","message":"Notes are only available on personal access links."}}}}}}},"404":{"description":"No such learner link, or no such lesson in it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"lesson_not_found":{"summary":"lesson_not_found","value":{"error":{"code":"lesson_not_found","message":"This learner link has no lesson with this slug or id."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}}},"/access/{token}/notes/{noteId}":{"patch":{"tags":["Notes"],"operationId":"editLearnerNote","summary":"Edit the text of the learner's note","description":"Changes what the note says. The passage it is anchored to stays as it was, so the note keeps its place in the lesson.","parameters":[{"$ref":"#/components/parameters/Token"},{"name":"noteId","in":"path","required":true,"description":"Note id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteUpdate"},"example":{"body":"Run this before every class, the db container sometimes stops on my laptop."}}}},"responses":{"200":{"description":"Saved","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Note"}}},"example":{"data":{"id":"cmg8k2v1x0025qz0l4k5j6h7g","selectedText":"docker compose ps","context":"Run `docker compose ps`: both `web` and `db` should be running.","body":"Run this before every class, the db container sometimes stops on my laptop.","createdAt":"2026-10-08T19:05:00.000Z","updatedAt":"2026-10-08T19:05:00.000Z"}}}}},"400":{"description":"The body is not JSON, or the note is empty or too long.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid_json":{"summary":"invalid_json","value":{"error":{"code":"invalid_json","message":"Send a JSON body."}}},"note_rejected":{"summary":"note_rejected (empty or too long)","value":{"error":{"code":"note_rejected","message":"Note text is required."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key lacks `feedback:write`, or the link is not a personal learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}},"notes_unavailable":{"summary":"notes_unavailable","value":{"error":{"code":"notes_unavailable","message":"Access is not valid."}}}}}}},"404":{"description":"No such learner link, or no note with this id from this learner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"note_not_found":{"summary":"note_not_found","value":{"error":{"code":"note_not_found","message":"Note not found."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}},"delete":{"tags":["Notes"],"operationId":"deleteLearnerNote","summary":"Delete the learner's note","parameters":[{"$ref":"#/components/parameters/Token"},{"name":"noteId","in":"path","required":true,"description":"Note id","schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key lacks `feedback:write`, or the link is not a personal learner link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}},"notes_unavailable":{"summary":"notes_unavailable","value":{"error":{"code":"notes_unavailable","message":"Access is not valid."}}}}}}},"404":{"description":"No such learner link, or no note with this id from this learner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_not_found":{"summary":"link_not_found","value":{"error":{"code":"link_not_found","message":"This learner link does not exist."}}},"note_not_found":{"summary":"note_not_found","value":{"error":{"code":"note_not_found","message":"Note not found."}}}}}}},"409":{"description":"A one-time team preview link that has not been opened in Chalkenso yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_unclaimed":{"summary":"link_unclaimed","value":{"error":{"code":"link_unclaimed","message":"This one-time preview link has to be opened in Chalkenso first."}}}}}}},"410":{"description":"The learner link can no longer be used. Show a friendly page with a way to ask for a new link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"link_expired":{"summary":"link_expired","value":{"error":{"code":"link_expired","message":"This learner link is expired."}}},"link_revoked":{"summary":"link_revoked","value":{"error":{"code":"link_revoked","message":"This learner link is revoked."}}},"link_used":{"summary":"link_used","value":{"error":{"code":"link_used","message":"This learner link is already used."}}}}}}}}}},"/assets/{assetId}":{"get":{"tags":["Images"],"operationId":"getAsset","summary":"Get an image","description":"The stable URL behind every image in a lesson and every `coverImageUrl`. Redirects to a short-lived file URL, so lesson text can be cached for as long as you like. No key needed: browsers load these. Only images used in a published lesson or as a course cover are served.","security":[],"parameters":[{"name":"assetId","in":"path","required":true,"description":"Image id","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to the file","headers":{"Location":{"description":"Short-lived file URL","schema":{"type":"string","format":"uri"}},"Cache-Control":{"description":"`public, max-age=300`","schema":{"type":"string"}}}},"404":{"description":"No such published image.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"not_found":{"summary":"not_found","value":{"error":{"code":"not_found","message":"No such image."}}}}}}}}}}},"webhooks":{"content.published":{"post":{"operationId":"onContentPublished","summary":"Lessons were published","description":"Chalkenso posts this to the address in *Website* → *Publish notifications* whenever lessons are published: in the workspace, by publishing a version, or from Obsidian. Refresh the course list, the course page and each lesson listed.\n\n`version.group` is set when a group's copy was published: then only that group's learner pages changed.\n\n**Verify before acting.** `Chalkenso-Signature` is `t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256 of `\"<t>.<raw body>\"` with your webhook secret. Compare in constant time and reject anything older than five minutes.\n\nAnswer `2xx` within five seconds; refresh after answering if it takes longer.","parameters":[{"$ref":"#/components/parameters/WebhookEvent"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentPublishedEvent"},"example":{"id":"evt_Jr2M0wPq8sLx3VbNc1Yz","event":"content.published","createdAt":"2026-10-09T08:15:00.000Z","course":{"id":"cmg8k2v1x0000qz0l8f3k1m2n","slug":"web-development-2","title":"Web Development II"},"version":{"id":"cmg8k2v1x0002qz0l1a2b3c4d","label":"v1","group":null},"lessons":[{"id":"cmg8k2v1x0004qz0l7d3h9a2b","slug":"course-intro-docker-setup"}]}}}},"responses":{"200":{"description":"Received. Any 2xx works."}}}},"content.unpublished":{"post":{"operationId":"onContentUnpublished","summary":"Lessons were unpublished","description":"Published lessons left a version: they were deleted (in the workspace or from Obsidian), or the version went back to draft. Drop the cached course list, the course page and each lesson listed. The lessons may no longer exist, so don't fetch them.\n\n`version.group` is set for a group's copy: then only that group's learner pages changed. Signed like `content.published`.","parameters":[{"$ref":"#/components/parameters/WebhookEvent"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentUnpublishedEvent"},"example":{"id":"evt_Lp3Nq8Rw1zKd5TsVb7Xc","event":"content.unpublished","createdAt":"2026-10-09T08:20:00.000Z","course":{"id":"cmg8k2v1x0000qz0l8f3k1m2n","slug":"web-development-2","title":"Web Development II"},"version":{"id":"cmg8k2v1x0002qz0l1a2b3c4d","label":"v1","group":null},"lessons":[{"id":"cmg8k2v1x0004qz0l7d3h9a2b","slug":"course-intro-docker-setup"}]}}}},"responses":{"200":{"description":"Received. Any 2xx works."}}}},"course.updated":{"post":{"operationId":"onCourseUpdated","summary":"A course changed","description":"The course's settings, cover, partners or Join button changed, or a partner it shows was edited or deleted. Drop the cached course list and that course's page; lessons are unchanged. Signed like `content.published`.","parameters":[{"$ref":"#/components/parameters/WebhookEvent"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseUpdatedEvent"},"example":{"id":"evt_Vb5Hx2Mk7qPz1RcLw9Sd","event":"course.updated","createdAt":"2026-10-09T08:25:00.000Z","course":{"id":"cmg8k2v1x0000qz0l8f3k1m2n","slug":"web-development-2","title":"Web Development II"}}}}},"responses":{"200":{"description":"Received. Any 2xx works."}}}},"test":{"post":{"operationId":"onTest","summary":"Test notification","description":"Sent by *Send a test* in *Website* → *Publish notifications*. Signed like every event; answer 2xx.","parameters":[{"$ref":"#/components/parameters/WebhookEvent"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEvent"},"example":{"id":"evt_Qm7Tz1Kc9dVw2HsLp4Xa","event":"test","createdAt":"2026-10-09T08:10:00.000Z","message":"Test from Northwind Academy. Publishing lessons sends \"content.published\"."}}}},"responses":{"200":{"description":"Received. Any 2xx works."}}}}},"components":{"securitySchemes":{"siteKey":{"type":"http","scheme":"bearer","bearerFormat":"site_…","description":"A site key from Chalkenso → *Website*. Keep it on your server."}},"parameters":{"CourseSlug":{"name":"courseSlug","in":"path","required":true,"description":"Course slug, from `GET /courses`","schema":{"type":"string"},"example":"web-development-2"},"Lesson":{"name":"lesson","in":"path","required":true,"description":"The lesson's slug or id, from the outline","schema":{"type":"string"},"example":"course-intro-docker-setup"},"Include":{"name":"include","in":"query","required":false,"description":"Extra representations. `html` adds the lesson rendered as HTML; the Markdown is always there.","schema":{"type":"string","enum":["html"]}},"Token":{"name":"token","in":"path","required":true,"description":"The learner's link token: the last part of the personal link Chalkenso gives each learner. Keep it out of analytics, logs and referrers.","schema":{"type":"string"}},"IfNoneMatch":{"name":"If-None-Match","in":"header","required":false,"description":"The `ETag` of your cached copy. If nothing changed, the answer is `304` with no body.","schema":{"type":"string"}},"WebhookEvent":{"name":"Chalkenso-Event","in":"header","required":true,"description":"The event name, same as `event` in the body. Treat names you don't know as \"refresh everything\".","schema":{"type":"string","enum":["content.published","content.unpublished","course.updated","test"]}},"WebhookSignature":{"name":"Chalkenso-Signature","in":"header","required":true,"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with your webhook secret>`","schema":{"type":"string"},"example":"t=1791500000,v1=5f2b9c…"}},"headers":{"ETag":{"description":"Weak validator of this response. Send it back as `If-None-Match`.","schema":{"type":"string"},"example":"W/\"3q2-7Kc9…\""},"CacheControl":{"description":"`private, no-cache`: cache it, but revalidate with `If-None-Match`.","schema":{"type":"string"}}},"responses":{"NotModified":{"description":"Not modified: your copy with this `ETag` is current. No body."},"Unauthorized":{"description":"No site key, or not a valid one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing_key":{"summary":"missing_key","value":{"error":{"code":"missing_key","message":"Send your site key as \"Authorization: Bearer <key>\"."}}},"invalid_key":{"summary":"invalid_key","value":{"error":{"code":"invalid_key","message":"Invalid token."}}}}}}},"Forbidden":{"description":"The key was revoked, or lacks the permission this endpoint needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"forbidden":{"summary":"forbidden","value":{"error":{"code":"forbidden","message":"This key is not allowed to do that."}}},"invalid_key":{"summary":"invalid_key (revoked)","value":{"error":{"code":"invalid_key","message":"Token has been revoked."}}}}}}}},"schemas":{"Error":{"type":"object","description":"Every error has this shape.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable, machine-readable: `missing_key`, `invalid_key`, `forbidden`, `invalid_json`, `lesson_required`, `learner_link_required`, `course_not_found`, `course_not_ready`, `lesson_not_found`, `link_not_found`, `link_unclaimed`, `link_expired`, `link_revoked`, `link_used`, `feedback_rejected`, `feedback_not_found`, `note_rejected`, `notes_unavailable`, `note_not_found`, `not_found`.","examples":["course_not_found"]},"message":{"type":"string","description":"For people; may change."}}}}},"ApiIndex":{"type":"object","required":["name","version","openapi","reference","guide","llms"],"properties":{"name":{"type":"string"},"version":{"type":"string"},"openapi":{"type":"string","format":"uri","description":"This spec"},"reference":{"type":"string","format":"uri","description":"Interactive reference (Swagger UI)"},"guide":{"type":"string","format":"uri","description":"Integration guide"},"llms":{"type":"string","format":"uri","description":"llms.txt for AI agents"}}},"Part":{"type":"object","description":"A part (arc) of the course that lessons are grouped into.","required":["id","title"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"title":{"type":"string"}}},"PartDetail":{"type":"object","description":"A part (arc) of the course, with its description.","required":["id","title","description"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"title":{"type":"string"},"description":{"type":["string","null"]}}},"Partner":{"type":"object","description":"A partner the course (or group) is taught with, shown under \"In partnership with\".","required":["id","name","websiteUrl","logoUrl","description"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"name":{"type":"string"},"websiteUrl":{"type":["string","null"]},"logoUrl":{"type":["string","null"],"description":"An image on the web, best a single-colour logo"},"description":{"type":["string","null"]}}},"Cover":{"type":"object","description":"The course's cover image.","required":["url","alt","width","height"],"properties":{"url":{"type":"string","format":"uri","description":"Stable image URL (`/api/v1/assets/{id}`)"},"alt":{"type":["string","null"],"description":"What the image shows"},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]}}},"Join":{"type":"object","description":"Where the course's \"Join\" button leads.","required":["type","url","partner"],"properties":{"type":{"type":"string","enum":["default","partner","link"],"description":"`partner`: the join partner's website. `link`: the course's own link (`enrolUrl`). `default`: your site decides (a contact page, say); also when the chosen partner has no website or the link is empty."},"url":{"type":["string","null"],"description":"Where the button leads; null for `default`"},"partner":{"oneOf":[{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"name":{"type":"string"}}},{"type":"null"}]}}},"Version":{"type":"object","description":"The published main version the course is read from.","required":["id","label","publishedAt"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"label":{"type":"string","examples":["v1"]},"publishedAt":{"type":["string","null"],"format":"date-time"}}},"Course":{"type":"object","description":"A course with a published version.","required":["id","slug","title","summary","outcome","tags","githubUrl","listed","pinned","enrolUrl","coverImageUrl","cover","partners","join","freeLessons","lessonCount","version","reviews","updatedAt"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string","examples":["web-development-2"]},"title":{"type":"string"},"summary":{"type":["string","null"]},"outcome":{"type":["string","null"],"description":"What a learner can do after the course"},"tags":{"type":"array","items":{"type":"string"}},"githubUrl":{"type":["string","null"],"description":"The course's code on GitHub"},"listed":{"type":"boolean","description":"Show in a public catalogue"},"pinned":{"type":"boolean","description":"Show first"},"enrolUrl":{"type":["string","null"],"description":"Where to send people who want to join"},"coverImageUrl":{"type":["string","null"],"description":"Stable image URL (`/api/v1/assets/{id}`); the same as `cover.url`"},"cover":{"oneOf":[{"$ref":"#/components/schemas/Cover"},{"type":"null"}]},"partners":{"type":"array","items":{"$ref":"#/components/schemas/Partner"},"description":"In their order on the course (\"In partnership with\")"},"join":{"$ref":"#/components/schemas/Join"},"freeLessons":{"oneOf":[{"type":"integer","minimum":0},{"const":"all"}],"description":"How many lessons, from the start, anyone may read; or `\"all\"`."},"lessonCount":{"type":"integer","minimum":0,"description":"Published lessons"},"version":{"$ref":"#/components/schemas/Version"},"reviews":{"oneOf":[{"$ref":"#/components/schemas/Reviews"},{"type":"null"}]},"updatedAt":{"type":"string","format":"date-time"}}},"OutlineLesson":{"type":"object","description":"A lesson in a course outline.","required":["id","slug","number","title","shortDescription","part","publishedAt"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string","examples":["course-intro-docker-setup"]},"number":{"type":"integer","minimum":1,"description":"Position in the course, from 1"},"title":{"type":"string"},"shortDescription":{"type":["string","null"]},"part":{"oneOf":[{"$ref":"#/components/schemas/Part"},{"type":"null"}]},"publishedAt":{"type":["string","null"],"format":"date-time","description":"Null only on preview links, for lessons not published yet"}}},"CourseLesson":{"type":"object","description":"A lesson in the public course outline.","required":["id","slug","number","title","shortDescription","part","publishedAt","free"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string","examples":["course-intro-docker-setup"]},"number":{"type":"integer","minimum":1,"description":"Position in the course, from 1"},"title":{"type":"string"},"shortDescription":{"type":["string","null"]},"part":{"oneOf":[{"$ref":"#/components/schemas/Part"},{"type":"null"}]},"publishedAt":{"type":["string","null"],"format":"date-time","description":"Null only on preview links, for lessons not published yet"},"free":{"type":"boolean","description":"Anyone may read it (`GET /courses/{courseSlug}/lessons/{lesson}`)"}}},"CourseDetail":{"type":"object","description":"A course with its parts and lesson outline.","required":["id","slug","title","summary","outcome","tags","githubUrl","listed","pinned","enrolUrl","coverImageUrl","cover","partners","join","freeLessons","lessonCount","version","reviews","updatedAt","parts","lessons"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string","examples":["web-development-2"]},"title":{"type":"string"},"summary":{"type":["string","null"]},"outcome":{"type":["string","null"],"description":"What a learner can do after the course"},"tags":{"type":"array","items":{"type":"string"}},"githubUrl":{"type":["string","null"],"description":"The course's code on GitHub"},"listed":{"type":"boolean","description":"Show in a public catalogue"},"pinned":{"type":"boolean","description":"Show first"},"enrolUrl":{"type":["string","null"],"description":"Where to send people who want to join"},"coverImageUrl":{"type":["string","null"],"description":"Stable image URL (`/api/v1/assets/{id}`); the same as `cover.url`"},"cover":{"oneOf":[{"$ref":"#/components/schemas/Cover"},{"type":"null"}]},"partners":{"type":"array","items":{"$ref":"#/components/schemas/Partner"},"description":"In their order on the course (\"In partnership with\")"},"join":{"$ref":"#/components/schemas/Join"},"freeLessons":{"oneOf":[{"type":"integer","minimum":0},{"const":"all"}],"description":"How many lessons, from the start, anyone may read; or `\"all\"`."},"lessonCount":{"type":"integer","minimum":0,"description":"Published lessons"},"version":{"$ref":"#/components/schemas/Version"},"reviews":{"oneOf":[{"$ref":"#/components/schemas/Reviews"},{"type":"null"}]},"updatedAt":{"type":"string","format":"date-time"},"parts":{"type":"array","items":{"$ref":"#/components/schemas/PartDetail"},"description":"Parts in order, those with published lessons"},"lessons":{"type":"array","items":{"$ref":"#/components/schemas/CourseLesson"},"description":"Published lessons in order"}}},"LessonContent":{"type":"object","description":"One lesson, loaded when a reader opens it.","required":["id","slug","number","title","shortDescription","part","publishedAt","markdown","revision","sections","previous","next"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string","examples":["course-intro-docker-setup"]},"number":{"type":"integer","minimum":1,"description":"Position in the course, from 1"},"title":{"type":"string"},"shortDescription":{"type":["string","null"]},"part":{"oneOf":[{"$ref":"#/components/schemas/Part"},{"type":"null"}]},"publishedAt":{"type":["string","null"],"format":"date-time","description":"Null only on preview links, for lessons not published yet"},"markdown":{"type":"string","description":"The lesson, always as Markdown (CommonMark with GFM tables). It renders in any Markdown renderer: images are absolute, stable URLs; links to other lessons are `[Title](#lesson-<slug>)`; callouts are `> [!note] Title` blockquotes (GitHub alert syntax); code blocks are fenced with their language."},"html":{"type":"string","description":"Only with `?include=html`: the same lesson rendered. Code blocks are `div.lesson-code-block` with highlighted `hljs-…` spans, callouts are `div.callout`, and links to other lessons are `<a href=\"#lesson-<slug>\" data-lesson-slug>`."},"revision":{"type":"integer","minimum":1,"description":"Which text of the lesson this is. Learner answers and reading statistics are kept per revision."},"sections":{"type":"array","items":{"type":"string"},"description":"The lesson's sections in order: its `##` and `###` headings as plain text (the `h2`/`h3` of `html`)."},"previous":{"oneOf":[{"$ref":"#/components/schemas/Neighbour"},{"type":"null"}]},"next":{"oneOf":[{"$ref":"#/components/schemas/Neighbour"},{"type":"null"}]}}},"FreeLesson":{"type":"object","description":"A free lesson, with the course it belongs to.","required":["id","slug","number","title","shortDescription","part","publishedAt","markdown","revision","sections","previous","next","free","course"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string","examples":["course-intro-docker-setup"]},"number":{"type":"integer","minimum":1,"description":"Position in the course, from 1"},"title":{"type":"string"},"shortDescription":{"type":["string","null"]},"part":{"oneOf":[{"$ref":"#/components/schemas/Part"},{"type":"null"}]},"publishedAt":{"type":["string","null"],"format":"date-time","description":"Null only on preview links, for lessons not published yet"},"markdown":{"type":"string","description":"The lesson, always as Markdown (CommonMark with GFM tables). It renders in any Markdown renderer: images are absolute, stable URLs; links to other lessons are `[Title](#lesson-<slug>)`; callouts are `> [!note] Title` blockquotes (GitHub alert syntax); code blocks are fenced with their language."},"html":{"type":"string","description":"Only with `?include=html`: the same lesson rendered. Code blocks are `div.lesson-code-block` with highlighted `hljs-…` spans, callouts are `div.callout`, and links to other lessons are `<a href=\"#lesson-<slug>\" data-lesson-slug>`."},"revision":{"type":"integer","minimum":1,"description":"Which text of the lesson this is. Learner answers and reading statistics are kept per revision."},"sections":{"type":"array","items":{"type":"string"},"description":"The lesson's sections in order: its `##` and `###` headings as plain text (the `h2`/`h3` of `html`)."},"previous":{"oneOf":[{"$ref":"#/components/schemas/Neighbour"},{"type":"null"}]},"next":{"oneOf":[{"$ref":"#/components/schemas/Neighbour"},{"type":"null"}]},"free":{"const":true},"course":{"type":"object","required":["slug","title"],"properties":{"slug":{"type":"string"},"title":{"type":"string"}}}}},"Neighbour":{"type":"object","description":"The lesson before or after.","required":["slug","title"],"properties":{"slug":{"type":"string"},"title":{"type":"string"}}},"Access":{"type":"object","description":"A learner link and what it may read.","required":["learner","canSendFeedback","canKeepNotes","preview","expiresAt","group","course","version","parts","lessons","signals"],"properties":{"learner":{"oneOf":[{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}},{"type":"null"}]},"canSendFeedback":{"type":"boolean","description":"A personal (not preview) link: show the feedback box"},"canKeepNotes":{"type":"boolean","description":"A personal (not preview) link: offer private notes"},"preview":{"type":"boolean","description":"A team preview link: reads drafts, cannot send feedback"},"expiresAt":{"type":"string","format":"date-time"},"group":{"oneOf":[{"type":"object","required":["id","name","startsAt","endsAt","slackHandle","description","partners"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"name":{"type":"string"},"startsAt":{"type":["string","null"],"format":"date-time"},"endsAt":{"type":["string","null"],"format":"date-time"},"slackHandle":{"type":["string","null"],"description":"The group's chat channel or handle"},"description":{"type":["string","null"]},"partners":{"type":"array","items":{"$ref":"#/components/schemas/Partner"},"description":"The group's own partners, in order: show them instead of the course's when there are any"}}},{"type":"null"}]},"course":{"type":"object","required":["id","slug","title","summary","githubUrl","partners"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string"},"title":{"type":"string"},"summary":{"type":["string","null"]},"githubUrl":{"type":["string","null"]},"partners":{"type":"array","items":{"$ref":"#/components/schemas/Partner"},"description":"The course's partners, in order"}}},"version":{"type":"object","description":"The version this link reads: usually the group's own copy","required":["id","label"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"label":{"type":"string"}}},"parts":{"type":"array","items":{"$ref":"#/components/schemas/PartDetail"},"description":"Parts of that version in order, those with lessons here"},"lessons":{"type":"array","items":{"$ref":"#/components/schemas/OutlineLesson"},"description":"Every lesson the learner may read, in order"},"signals":{"oneOf":[{"$ref":"#/components/schemas/LearnerSignals"},{"type":"null"}]}}},"LearnerSignals":{"type":"object","description":"Personal links only (null otherwise): what to ask this learner. `switches` says which features the course or group has on; `lessons` has, per lesson id, the revision learners read, this learner's answer (`YES`, `NOT_YET`, `NOT_READ`, `DISMISSED`, `CHECK` or null) and the approved check question; `survey` says whether the end-of-course survey and the halfway check are open.","required":["switches","readingAllowed","readingOptOut","quietMode","lessonOrder","lessons","survey","quickChecks","classNote","lessonTitles"],"properties":{"switches":{"type":"object","required":["lessonQuestion","checks","surveys","reading"],"properties":{"lessonQuestion":{"type":"boolean"},"checks":{"type":"boolean"},"surveys":{"type":"boolean"},"reading":{"type":"boolean"}}},"readingAllowed":{"type":"boolean","description":"Reading statistics may run for this learner"},"readingOptOut":{"type":"boolean","description":"The learner chose \"Don't count my reading\""},"quietMode":{"type":"boolean","description":"Closed the question three times in a row: ask only at the start of the next lesson"},"lessonOrder":{"type":"array","items":{"type":"string"}},"lessons":{"type":"object","additionalProperties":{"type":"object"}},"survey":{"type":"object"},"quickChecks":{"type":"array","items":{"type":"object"}},"classNote":{"oneOf":[{"type":"object"},{"type":"null"}],"description":"The instructor's note to the group"},"lessonTitles":{"type":"object","additionalProperties":{"type":"string"}}}},"Reviews":{"type":"object","description":"End-of-course reviews: null until at least five learners gave a score.","required":["score","count","quotes"],"properties":{"score":{"type":"number","minimum":1,"maximum":5,"description":"Average end-of-course score, one decimal"},"count":{"type":"integer","minimum":5,"description":"Learners who gave a score"},"quotes":{"type":"array","items":{"type":"object","required":["text","date"],"properties":{"text":{"type":"string"},"date":{"type":"string","format":"date"}}},"description":"Answers learners allowed you to quote, newest first, without names"}}},"PulseInput":{"type":"object","description":"The lesson question (`answer`), a check question (`optionIndex` or `notSure`), or the optional follow-up (`sectionIndex`, `note`) after an answer. Send the answer first; send the follow-up as a second request.","properties":{"answer":{"type":"string","enum":["YES","NOT_YET","NOT_READ","DISMISSED"]},"optionIndex":{"type":"integer","minimum":0},"notSure":{"type":"boolean"},"sectionIndex":{"type":"integer","minimum":-1,"description":"Index in `sections`; -1 for \"Most of it\""},"sectionTitle":{"type":"string"},"note":{"type":"string","maxLength":280},"delayed":{"type":"boolean","description":"Asked at the start of the next lesson"}}},"PulseResult":{"type":"object","description":"Saved. For check questions, whether the answer was right.","required":["check"],"properties":{"check":{"oneOf":[{"type":"object","required":["correct","correctIndex","sectionIndex"],"properties":{"correct":{"type":"boolean"},"correctIndex":{"type":"integer","minimum":0},"sectionIndex":{"type":["integer","null"],"description":"The section that teaches it"}}},{"type":"null"}]}}},"SurveyInput":{"type":"object","required":["kind"],"description":"Send the score first (1–5), then any optional answer as it comes. `declined: true` is \"No thanks\".","properties":{"kind":{"type":"string","enum":["END","MID"]},"declined":{"type":"boolean"},"score":{"type":"integer","minimum":1,"maximum":5},"improveLessonId":{"type":["string","null"],"description":"\"Which lesson should we improve first?\""},"improveNone":{"type":"boolean"},"answer":{"type":"string","maxLength":2000},"quoteConsent":{"type":"boolean","description":"May be quoted without a name (scores 4–5 only)"},"named":{"type":"boolean"},"extraAnswer":{"type":"string","maxLength":2000},"checkAnswers":{"type":"array","maxItems":3,"items":{"type":"object","required":["lessonId"],"properties":{"lessonId":{"type":"string"},"optionIndex":{"type":["integer","null"]},"notSure":{"type":"boolean"}}}}}},"SurveyResult":{"type":"object","description":"Saved. For \"3 quick questions\", which answers were right.","required":["checks"],"properties":{"checks":{"oneOf":[{"type":"array","items":{"type":"object","required":["lessonId","correct","correctIndex"],"properties":{"lessonId":{"type":"string"},"correct":{"type":"boolean"},"correctIndex":{"type":"integer"}}}},{"type":"null"}]}}},"FeedbackInput":{"type":"object","required":["lesson","selectedText","body"],"properties":{"lesson":{"type":"string","description":"Lesson slug or id"},"selectedText":{"type":"string","minLength":1,"maxLength":2000,"description":"The passage the learner selected"},"context":{"type":["string","null"],"maxLength":4000,"description":"The paragraph around it, so the team sees it in context"},"body":{"type":"string","minLength":1,"maxLength":4000,"description":"The learner's feedback"}}},"Feedback":{"type":"object","description":"A learner's feedback on a passage.","required":["id","selectedText","context","body","status","adminResponse","adminRespondedAt","createdAt"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"selectedText":{"type":"string"},"context":{"type":["string","null"]},"body":{"type":"string"},"status":{"type":"string","enum":["OPEN","RESOLVED"]},"adminResponse":{"type":["string","null"],"description":"The team's reply"},"adminRespondedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"NoteInput":{"type":"object","required":["lesson","selectedText","body"],"properties":{"lesson":{"type":"string","description":"Lesson slug or id"},"selectedText":{"type":"string","minLength":1,"maxLength":2000,"description":"The passage the learner selected: the note is anchored to it"},"context":{"type":["string","null"],"maxLength":4000,"description":"The paragraph around it, to find the passage again if it appears more than once"},"body":{"type":"string","minLength":1,"maxLength":8000,"description":"The learner's note"}}},"NoteUpdate":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":8000,"description":"The new text"}}},"Note":{"type":"object","description":"A learner's private note on a passage.","required":["id","selectedText","context","body","createdAt","updatedAt"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"selectedText":{"type":"string"},"context":{"type":["string","null"]},"body":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ContentPublishedEvent":{"type":"object","required":["id","event","createdAt","course","version","lessons"],"properties":{"id":{"type":"string","description":"Event id (`evt_…`); use it to ignore duplicates"},"event":{"const":"content.published"},"createdAt":{"type":"string","format":"date-time"},"course":{"type":"object","required":["id","slug","title"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string"},"title":{"type":"string"}}},"version":{"type":"object","required":["id","label","group"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"label":{"type":"string"},"group":{"oneOf":[{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"name":{"type":"string"}}},{"type":"null"}]}}},"lessons":{"type":"array","items":{"type":"object","required":["id","slug"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string"}}},"description":"The lessons that were published"}}},"ContentUnpublishedEvent":{"type":"object","required":["id","event","createdAt","course","version","lessons"],"properties":{"id":{"type":"string","description":"Event id (`evt_…`); use it to ignore duplicates"},"event":{"const":"content.unpublished"},"createdAt":{"type":"string","format":"date-time"},"course":{"type":"object","required":["id","slug","title"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string"},"title":{"type":"string"}}},"version":{"type":"object","required":["id","label","group"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"label":{"type":"string"},"group":{"oneOf":[{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"name":{"type":"string"}}},{"type":"null"}]}}},"lessons":{"type":"array","items":{"type":"object","required":["id","slug"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string"}}},"description":"The lessons that are no longer published (they may be deleted)"}}},"CourseUpdatedEvent":{"type":"object","required":["id","event","createdAt","course"],"properties":{"id":{"type":"string","description":"Event id (`evt_…`); use it to ignore duplicates"},"event":{"const":"course.updated"},"createdAt":{"type":"string","format":"date-time"},"course":{"type":"object","required":["id","slug","title"],"properties":{"id":{"type":"string","description":"Stable id","examples":["cmg8k2v1x0004qz0l7d3h9a2b"]},"slug":{"type":"string"},"title":{"type":"string"}}}}},"TestEvent":{"type":"object","required":["id","event","createdAt","message"],"properties":{"id":{"type":"string"},"event":{"const":"test"},"createdAt":{"type":"string","format":"date-time"},"message":{"type":"string"}}}}}}