# The Schulcloud API, as verified against this instance Everything here was confirmed against `https://schulcloud-thueringen.de` with a real student account on 2026-09-11, not inferred from source. Where upstream source and live behaviour disagreed, live behaviour won. ## Two services, one origin | Service | Source repo | Base path | Self-documenting at | |---|---|---|---| | Main server (NestJS) | [`schulcloud-server`](https://github.com/hpi-schul-cloud/schulcloud-server) | `/api/v3/` | `/api/v3/docs`, `/api/v3/docs-json` | | Files storage | [`file-storage`](https://github.com/hpi-schul-cloud/file-storage) | `/api/v3/file/` | `/api/v3/file/docs`, `/api/v3/file/docs-json` | Both accept the same bearer token. The files service was split out of `schulcloud-server` into its own repository, which is why no `file` paths appear in the main `docs-json` — a detail that will send you in circles if you only read the main spec. Fetch both: ```bash curl -s "$TSC_URL/api/v3/docs-json" -o docs-v3.json # 212 paths curl -s "$TSC_URL/api/v3/file/docs-json" -o docs-file.json # 26 paths ``` These are the authoritative reference for *this* instance's deployed version. Prefer them over the GitHub sources, which track `main` and may be ahead. ## Which repositories matter The `hpi-schul-cloud` org has ~100 repos, most archived or superseded. The live ones relevant here: - **`schulcloud-server`** — the API. Read `apps/server/src/modules//api/` for controllers and DTOs. - **`file-storage`** — the files service, extracted from the above. `src/modules/files-storage/api/controller/files-storage.controller.ts` is the whole surface. - **`nuxt-client`** — the current web front end. Useful for seeing which API calls the real UI makes in which order. - **`schulcloud-client`** — the *legacy* Handlebars front end. Still receives commits, but it is not where new features land. Superseded/archived and worth ignoring: `authorization-service`, `schulcloud-editor`, `nexboard-api-js`, `end-to-end-tests`, `docker-compose`, `H5P-Nodejs-library`, `shd-client`. Note the naming: `/api/v1` is the old Feathers surface. On this instance `/api/v1/docs` 404s, and the v3 NestJS API covers everything this server needs. ## Content model ``` Course ─┬─ column board ─── column ─── card ─── element ─┬─ richText │ ├─ file ──── fileRecord(s) │ ├─ link │ └─ … ├─ lesson (Thema) ─── contents[] + materials[] └─ task (Aufgabe) ─── description + fileRecord(s) ``` On the account this was built against: 26 courses holding 30 column boards, 18 lessons, 42 tasks and 175 files. **Column boards hold the great majority of current material**; lessons are the older format. ## Endpoints this server uses | Purpose | Call | |---|---| | Identity, school id, permissions | `GET /api/v3/me` | | Courses | `GET /api/v3/courses?skip&limit` | | One course's contents | `GET /api/v3/course-rooms/{courseId}/board` | | Dashboard tiles | `GET /api/v3/dashboard` | | Tasks | `GET /api/v3/tasks`, `GET /api/v3/tasks/finished` | | Lesson body | `GET /api/v3/lessons/{lessonId}` | | Lesson's tasks | `GET /api/v3/lessons/{lessonId}/tasks` | | Board structure | `GET /api/v3/boards/{boardId}` | | What a board belongs to | `GET /api/v3/boards/{boardId}/context` | | Card bodies | `GET /api/v3/cards?ids=&ids=` | | Files of an entity | `GET /api/v3/file/list/{storageLocation}/{storageLocationId}/{parentType}/{parentId}` | | One file's metadata | `GET /api/v3/file/{fileRecordId}` | | File bytes | `GET /api/v3/file/download/{fileRecordId}/{fileName}` | | News | `GET /api/v3/news` | ### Gotchas that cost real time **`course-rooms`, not `courses`, for course contents.** `GET /api/v3/courses/{id}` does not exist. The route that returns a course's lessons/tasks/boards is `GET /api/v3/course-rooms/{roomId}/board`, and its `:roomId` is the *course* id. Nothing in the naming suggests this. **`/api/v3/rooms` is a different feature.** "Rooms" are the newer standalone collaboration spaces, unrelated to courses. On this instance the account has none, so `GET /api/v3/rooms` returns `{"data":[]}` — which reads like a broken endpoint but is simply an empty feature. **`limit` maxima are enforced and mis-documented.** The OpenAPI schema says `maximum: 99`; the runtime validator rejects anything `> 100`. Page at 99 to satisfy both. Asking for 200 returns a `400 API_VALIDATION_ERROR`, not a truncated list. **There is no `GET /tasks/{id}`.** Single-task detail has to be assembled: the list endpoints give metadata but *omit `description`*, which appears only on the course page's task element. `get_task` does this join. **Board files need three calls.** A `file` element's `content` carries only `{caption, alternativeText}` — no file id. The bytes are found by listing files-storage with `parentType: 'boardnodes'` and the **element** id as `parentId`. This is the single least discoverable part of the API, and applies equally to `fileFolder` and `drawing` elements. **`storageLocationId` is the school id** (from `/me`), with `storageLocation: 'school'`, for every parent type in normal use. **Lesson ids come back as buffers.** `GET /api/v3/lessons/{id}` returns nested ids as `{buffer:{type:'Buffer',data:[...]}}` rather than hex strings — a leak from the legacy Mongo serialisation. `normalizeObjectId` in `src/render.ts` converts them. **`Content-Disposition` on downloads is malformed.** It comes back as `attachment;; filename="…"` — note the doubled semicolon — and the filename is percent-encoded inside the quotes. Parse defensively. ### Content element types From `ContentElementType` in `schulcloud-server`, all seen live except where noted: `richText`, `file`, `fileFolder`, `link`, `drawing`, `collaborativeTextEditor`, `externalTool`, `videoConference`, `h5p`, `deleted`. Collaborative text editor contents are **not** retrievable through the API — `GET /api/v3/collaborative-text-editor/{parentType}/{parentId}` returns a URL to the Etherpad-style editor, not the document text. ## Re-verifying after an upstream release `npm run probe` re-checks every assumption above against the live instance and prints what it finds, including days left on the token.