openapi: 3.1.0 info: title: Voya Courses Service API description: | Comprehensive API for managing educational content, imports, and course generation. **Features:** - PDF import and automatic course structure generation - AI-powered content creation using Gemini - Real-time progress tracking via WebSocket notifications - OpenTelemetry + Langfuse observability **Authentication:** Uses header-based authentication with user context propagation. version: 1.0.0 contact: name: Voya Development Team url: https://github.com/Voya-Tutor/voya-lemon-backend servers: - url: http://localhost:8003/courses/api/v1 description: Local development - url: https://dev-api.voyatutor.dev/courses/api/v1 description: Development environment - url: https://api.voyatutor.dev/courses/api/v1 description: Production environment tags: - name: Imports description: PDF import and course generation - name: Content description: Course content and tree structure - name: TreeReorder description: Tree reordering with Redis staging - name: AI description: AI-powered content assistance - name: MicroTest description: Micro-test configuration - name: Subjects description: Subject management - name: Qualifications description: Qualification management - name: UserQualifications description: User qualification management - name: Levels description: Level management - name: AwardingBodies description: Awarding body management - name: TLDraw description: TLDraw diagram management - name: Performance description: Performance-optimized endpoints paths: /imports: get: operationId: listImports summary: List imports description: Returns a paginated list of imports tags: - Imports security: - UserAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' responses: '200': description: Paginated list of imports content: application/json: schema: $ref: '#/components/schemas/ImportPage' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' post: operationId: createImport summary: Create import from file description: | Upload a PDF file and create a new import for course generation. **Process:** 1. File upload and validation 2. Asynchronous L1-L6 structure generation 3. Real-time progress via WebSocket notifications **Supported formats:** PDF tags: - Imports security: - UserAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - file - name properties: file: type: string format: binary description: PDF file to import name: type: string minLength: 1 maxLength: 255 description: Human-readable name for the import responses: '201': description: Import created successfully content: application/json: schema: $ref: '#/components/schemas/Import' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '413': description: File too large '500': $ref: '#/components/responses/InternalError' /imports/updatePreOrderAll: post: operationId: updatePreOrderAll summary: Update pre-order index for all trees description: Updates the pre-order traversal index for all import trees tags: - Imports responses: '200': description: Pre-order indices updated successfully '500': $ref: '#/components/responses/InternalError' /imports/{uid}: parameters: - $ref: '#/components/parameters/ImportUidParam' get: operationId: getImport summary: Get import details description: Retrieve detailed information about a specific import tags: - Imports security: - UserAuth: [] responses: '200': description: Import details content: application/json: schema: $ref: '#/components/schemas/Import' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteImport summary: Delete import description: Delete an import and all associated content tags: - Imports security: - UserAuth: [] responses: '200': description: Import deleted successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/tree: parameters: - $ref: '#/components/parameters/ImportUidParam' get: operationId: getImportTree summary: Get course tree structure description: Retrieve the hierarchical course structure generated from the import tags: - Content security: - UserAuth: [] parameters: - name: simplify in: query description: Return simplified tree structure schema: type: boolean default: false responses: '200': description: Course tree structure content: application/json: schema: type: array items: oneOf: - $ref: '#/components/schemas/ContentNode' - $ref: '#/components/schemas/SimplifiedContentNode' description: Returns ContentNode when simplify=false, SimplifiedContentNode when simplify=true '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' post: operationId: saveImportTree summary: Save tree nodes description: Save changes made to the tree nodes tags: - Content security: - UserAuth: [] parameters: - name: generate in: query description: Generate content after saving schema: type: boolean default: false - name: simplify in: query description: Use simplified input format schema: type: boolean default: false requestBody: required: true content: application/json: schema: type: array items: $ref: '#/components/schemas/ContentNode' responses: '200': description: Tree saved successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/tree/reorder/session: parameters: - $ref: '#/components/parameters/ImportUidParam' post: operationId: startReorderSession summary: Start tree reorder session description: Create a new reorder session that copies the tree to Redis for editing tags: - Content - TreeReorder security: - UserAuth: [] responses: '201': description: Session created successfully content: application/json: schema: $ref: '#/components/schemas/SessionInfo' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/tree/reorder/session/{sessionId}: parameters: - $ref: '#/components/parameters/ImportUidParam' - name: sessionId in: path required: true description: Reorder session ID schema: type: string format: uuid delete: operationId: cancelReorderSession summary: Cancel reorder session description: Cancel an active reorder session and remove it from Redis tags: - Content - TreeReorder security: - UserAuth: [] responses: '204': description: Session cancelled successfully '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/tree/reorder/stage: parameters: - $ref: '#/components/parameters/ImportUidParam' post: operationId: stageReorderOperations summary: Stage reorder operations description: Apply reorder operations to the staged tree in Redis tags: - Content - TreeReorder security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/StageRequest' responses: '200': description: Operations staged successfully content: application/json: schema: $ref: '#/components/schemas/ReorderResponse' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '409': description: Circular reference or invalid operation content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/tree/reorder/reset: parameters: - $ref: '#/components/parameters/ImportUidParam' post: operationId: resetReorderSession summary: Reset reorder session to original state description: Discard all pending operations and restore the staged tree to its original state (at session start) tags: - Content - TreeReorder security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ResetRequest' responses: '200': description: Session reset successfully content: application/json: schema: $ref: '#/components/schemas/ReorderResponse' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/tree/preview/{sessionId}: parameters: - $ref: '#/components/parameters/ImportUidParam' - name: sessionId in: path required: true description: Reorder session ID schema: type: string format: uuid get: operationId: previewStagedTree summary: Preview staged tree description: Get the current state of the staged tree with warnings tags: - Content - TreeReorder security: - UserAuth: [] responses: '200': description: Staged tree preview content: application/json: schema: $ref: '#/components/schemas/TreePreviewResponse' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/tree/reorder/commit: parameters: - $ref: '#/components/parameters/ImportUidParam' post: operationId: commitReorderSession summary: Commit reorder session description: Commit the staged tree changes to the database tags: - Content - TreeReorder security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CommitRequest' responses: '200': description: Changes committed successfully content: application/json: schema: $ref: '#/components/schemas/ReorderResponse' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '409': description: Tree conflict detected content: application/json: schema: $ref: '#/components/schemas/ReorderResponse' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/regeneratePhase1: parameters: - $ref: '#/components/parameters/ImportUidParam' post: operationId: regeneratePhase1 summary: Regenerate L1-L6 structure description: Trigger regeneration of the course structure (L1-L6 levels) tags: - Imports security: - UserAuth: [] responses: '200': description: Regeneration started '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/generatePhase2: parameters: - $ref: '#/components/parameters/ImportUidParam' post: operationId: generatePhase2 summary: Generate L7 content description: Trigger generation of L7 content nodes tags: - Imports security: - UserAuth: [] responses: '200': description: Generation started '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/preOrder: parameters: - $ref: '#/components/parameters/ImportUidParam' get: operationId: getTreePreOrder summary: Get tree in pre-order description: Get the tree structure in pre-order traversal tags: - Content responses: '200': description: Tree in pre-order content: application/json: schema: type: array items: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /imports/{uid}/updatePreOrder: parameters: - $ref: '#/components/parameters/ImportUidParam' post: operationId: updatePreOrder summary: Update pre-order index description: Update the pre-order traversal index for a specific tree tags: - Content responses: '200': description: Pre-order index updated '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /ai/prompts: get: operationId: getPrompts summary: Get helper prompts description: Get 1 default and 4 custom prompts for AI assistance tags: - AI security: - UserAuth: [] responses: '200': description: AI prompts content: application/json: schema: $ref: '#/components/schemas/AIPrompts' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' put: operationId: replacePrompts summary: Replace/update AI prompts description: Replace or update AI prompts tags: - AI security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReplaceAIPromptsRequest' responses: '200': description: Prompts updated successfully '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' /ai/ask: post: operationId: askAI summary: Ask AI LLM to improve text description: Send prompt and input to AI LLM for text improvement tags: - AI security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AskRequest' responses: '200': description: AI response content: application/json: schema: $ref: '#/components/schemas/AskResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' /content: get: operationId: listContent summary: List content nodes description: Returns a paginated list of content nodes with optional UUID filter tags: - Content security: - UserAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - name: uuid in: query required: false description: Filter by content node UUID schema: type: string format: uuid example: 123e4567-e89b-12d3-a456-426614174000 responses: '200': description: Paginated list of content nodes content: application/json: schema: $ref: '#/components/schemas/ContentNodePage' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' post: operationId: createContent summary: Create new content description: Create a new content item for a lesson tags: - Content security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContentNode' responses: '201': description: Content created successfully content: application/json: schema: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' /content/{id}: parameters: - $ref: '#/components/parameters/ContentIdParam' get: operationId: getContent summary: Get content by ID description: Get a content item by its ID tags: - Content security: - UserAuth: [] responses: '200': description: Content details content: application/json: schema: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' put: operationId: updateContent summary: Update content description: Update a content item's details tags: - Content security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContentNode' responses: '200': description: Content updated successfully content: application/json: schema: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteContent summary: Delete content description: Delete a content item by its ID tags: - Content security: - UserAuth: [] responses: '204': description: Content deleted successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /content/{id}/children: parameters: - $ref: '#/components/parameters/ContentIdParam' get: operationId: getContentChildren summary: Get content children description: Get all content items for a specific parent tags: - Content security: - UserAuth: [] responses: '200': description: List of child content items content: application/json: schema: type: array items: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /content/{id}/descendants: parameters: - $ref: '#/components/parameters/ContentIdParam' get: operationId: getContentDescendants summary: Get content descendants description: Get all descendants of a content node using depth-first search traversal. This returns the node itself and all its descendants in a flat list. tags: - Content security: - UserAuth: [] responses: '200': description: List of content node descendants (including the node itself) content: application/json: schema: type: array items: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /content/{id}/reorder: parameters: - $ref: '#/components/parameters/ContentIdParam' post: operationId: reorderContent summary: Reorder content description: Change the position of a content item within its lesson tags: - Content security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReorderContentRequest' responses: '200': description: Content reordered successfully content: application/json: schema: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /content/batch: post: operationId: batchUpdateContent summary: Batch update multiple content nodes description: | Update multiple content nodes in a single transaction for improved performance. Supports partial updates and maintains referential integrity. Maximum 50 nodes per batch. tags: - Content - Performance security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BatchUpdateRequest' responses: '200': description: Batch update results content: application/json: schema: $ref: '#/components/schemas/BatchUpdateResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '409': description: Conflict - version mismatch detected content: application/json: schema: $ref: '#/components/schemas/ConflictError' '500': $ref: '#/components/responses/InternalError' /content/{id}/partial: parameters: - $ref: '#/components/parameters/ContentIdParam' patch: operationId: partialUpdateContent summary: Partial update content node description: | Update specific fields of a content node without sending the entire object. Supports optimistic locking with version control. Only provided fields will be updated. tags: - Content - Performance security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContentNodePartialUpdate' responses: '200': description: Content updated successfully content: application/json: schema: $ref: '#/components/schemas/ContentNode' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '409': description: Conflict - version mismatch detected content: application/json: schema: $ref: '#/components/schemas/ConflictError' '500': $ref: '#/components/responses/InternalError' /content/{id}/tldraw: parameters: - $ref: '#/components/parameters/ContentIdParam' put: operationId: updateContentTLDraw summary: Update TLDraw canvas data description: | Update only the TLDraw canvas data for a content node. Optimized for frequent canvas saves with compression support. tags: - Content - TLDraw - Performance security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TLDrawUpdateRequest' responses: '200': description: TLDraw data updated successfully content: application/json: schema: type: object properties: message: type: string storage_type: type: string enum: - database - s3 compressed_size: type: integer description: Size after compression (bytes) '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '413': description: Payload too large content: application/json: schema: type: object properties: error: type: string max_size_mb: type: number '500': $ref: '#/components/responses/InternalError' /microTest/incorrectConsequenceOptions: get: operationId: getMicroTestOptions summary: Get micro-test options description: Get micro-test incorrect consequence options tags: - MicroTest security: - UserAuth: [] responses: '200': description: List of options content: application/json: schema: type: array items: type: string '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' put: operationId: setMicroTestOptions summary: Replace micro-test options description: Replace micro-test incorrect consequence options tags: - MicroTest security: - UserAuth: [] requestBody: required: true content: application/json: schema: type: array items: type: string responses: '200': description: Options updated successfully '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' /subjects: get: operationId: listSubjects summary: List subjects description: List subjects with pagination tags: - Subjects parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' responses: '200': description: Paginated list of subjects content: application/json: schema: $ref: '#/components/schemas/SubjectPage' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' post: operationId: createSubject summary: Create subject description: Create a new subject tags: - Subjects requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Subject' responses: '200': description: Subject created successfully '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' /subjects/{uid}: parameters: - $ref: '#/components/parameters/SubjectUidParam' get: operationId: getSubject summary: Get subject by ID description: Get subject by ID tags: - Subjects responses: '200': description: Subject details content: application/json: schema: $ref: '#/components/schemas/Subject' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' post: operationId: updateSubject summary: Update subject description: Update subject tags: - Subjects requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Subject' responses: '200': description: Subject updated successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteSubject summary: Delete subject description: Delete subject tags: - Subjects requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Subject' responses: '200': description: Subject deleted successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /qualifications: get: operationId: listQualifications summary: List qualifications description: List qualifications with pagination and filtering tags: - Qualifications parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - name: academic_level in: query description: Filter by academic level ID schema: type: string - name: subject in: query description: Filter by subject UID schema: type: string - name: awarding_body in: query description: Filter by awarding body ID schema: type: integer - name: publish_to_production in: query description: Filter by production publish flag schema: type: boolean - name: publish_to_dev in: query description: Filter by dev publish flag schema: type: boolean - name: search in: query description: Search in name and description schema: type: string responses: '200': description: Paginated list of qualifications content: application/json: schema: $ref: '#/components/schemas/QualificationPage' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' post: operationId: createQualification summary: Create qualification description: Create a new qualification tags: - Qualifications requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/QualificationRequest' responses: '200': description: Qualification created successfully '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' /qualifications/{uid}: parameters: - $ref: '#/components/parameters/QualificationUidParam' get: operationId: getQualification summary: Get qualification by ID description: Get qualification by ID tags: - Qualifications responses: '200': description: Qualification details content: application/json: schema: $ref: '#/components/schemas/Qualification' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' put: operationId: updateQualification summary: Update qualification description: Update qualification tags: - Qualifications requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/QualificationUpdateRequest' responses: '200': description: Qualification updated successfully content: application/json: schema: $ref: '#/components/schemas/Qualification' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteQualification summary: Delete qualification description: Delete qualification tags: - Qualifications responses: '200': description: Qualification deleted successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' /qualifications/filter/levels: get: operationId: getFilterLevels summary: Get filter levels description: Get filter levels for qualifications tags: - Qualifications responses: '200': description: List of levels content: application/json: schema: type: array items: $ref: '#/components/schemas/Level' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' /qualifications/filter/levels/{level_id}/subjects: parameters: - name: level_id in: path required: true description: Level ID schema: type: string example: GCSE get: operationId: getFilterSubjects summary: Get filter subjects by level description: Get filter subjects by level ID tags: - Qualifications responses: '200': description: List of subjects content: application/json: schema: type: array items: $ref: '#/components/schemas/SubjectFilter' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' /qualifications/filter/levels/{level_id}/subjects/{subject_uid}/awarding_bodies: parameters: - name: level_id in: path required: true description: Level ID schema: type: string example: GCSE - name: subject_uid in: path required: true description: Subject UUID schema: type: string format: uuid example: 123e4567-e89b-12d3-a456-426614174000 get: operationId: getFilterAwardingBodies summary: Get filter awarding bodies description: Get filter awarding bodies by level ID and subject ID tags: - Qualifications responses: '200': description: List of awarding bodies content: application/json: schema: type: array items: $ref: '#/components/schemas/AwardingBody' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' /me/qualifications: get: operationId: listUserQualifications summary: List current user's qualifications description: List current user's qualifications tags: - UserQualifications security: - UserAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' responses: '200': description: Paginated list of user qualifications content: application/json: schema: $ref: '#/components/schemas/UserQualificationPage' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' /me/qualifications/{uid}: parameters: - $ref: '#/components/parameters/QualificationUidParam' post: operationId: addUserQualification summary: Add user qualification description: Add user qualification tags: - UserQualifications security: - UserAuth: [] responses: '200': description: User qualification added successfully '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteUserQualification summary: Delete user qualification description: Delete user qualification tags: - UserQualifications security: - UserAuth: [] responses: '200': description: User qualification deleted successfully '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' /me/qualifications/level/{level_id}/subject/{subject_uid}/awarding_body/{awarding_body}: parameters: - name: level_id in: path required: true description: Level ID schema: type: string example: GCSE - name: subject_uid in: path required: true description: Subject UUID schema: type: string format: uuid example: 123e4567-e89b-12d3-a456-426614174000 - name: awarding_body in: path required: true description: Awarding Body ID schema: type: string example: AQA post: operationId: addUserQualificationByComponents summary: Add user qualification with components description: Add user qualification with level, subject and awarding body tags: - UserQualifications security: - UserAuth: [] responses: '200': description: User qualification added successfully '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' /levels: get: operationId: listLevels summary: List levels description: List levels with pagination tags: - Levels parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' responses: '200': description: Paginated list of levels content: application/json: schema: $ref: '#/components/schemas/LevelPage' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' post: operationId: createLevel summary: Create level description: Create a new academic level tags: - Levels requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LevelRequest' responses: '201': description: Level created successfully content: application/json: schema: $ref: '#/components/schemas/Level' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' /levels/{id}: parameters: - name: id in: path required: true description: Level ID schema: type: string get: operationId: getLevel summary: Get level by ID description: Get level by ID tags: - Levels responses: '200': description: Level details content: application/json: schema: $ref: '#/components/schemas/Level' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' put: operationId: updateLevel summary: Update level description: Update an existing level tags: - Levels requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LevelUpdateRequest' responses: '200': description: Level updated successfully content: application/json: schema: $ref: '#/components/schemas/Level' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteLevel summary: Delete level description: Delete a level (fails if qualifications reference it) tags: - Levels responses: '200': description: Level deleted successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '409': description: Level has existing qualifications content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': $ref: '#/components/responses/InternalError' /awarding_bodies: get: operationId: listAwardingBodies summary: List awarding bodies description: List awarding bodies with pagination tags: - AwardingBodies parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' responses: '200': description: Paginated list of awarding bodies content: application/json: schema: $ref: '#/components/schemas/AwardingBodyPage' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' post: operationId: createAwardingBody summary: Create awarding body description: Create a new awarding body tags: - AwardingBodies requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AwardingBodyRequest' responses: '201': description: Awarding body created successfully content: application/json: schema: $ref: '#/components/schemas/AwardingBody' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalError' /awarding_bodies/{id}: parameters: - name: id in: path required: true description: Awarding body ID schema: type: integer get: operationId: getAwardingBody summary: Get awarding body by ID description: Get awarding body by ID tags: - AwardingBodies responses: '200': description: Awarding body details content: application/json: schema: $ref: '#/components/schemas/AwardingBody' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' put: operationId: updateAwardingBody summary: Update awarding body description: Update an existing awarding body tags: - AwardingBodies requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AwardingBodyUpdateRequest' responses: '200': description: Awarding body updated successfully content: application/json: schema: $ref: '#/components/schemas/AwardingBody' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteAwardingBody summary: Delete awarding body description: Delete an awarding body (fails if qualifications reference it) tags: - AwardingBodies responses: '200': description: Awarding body deleted successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '409': description: Awarding body has existing qualifications content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': $ref: '#/components/responses/InternalError' /tldraw/{id}: parameters: - $ref: '#/components/parameters/ContentIdParam' get: operationId: getTLDraw summary: Get node TLDraw data description: Get node TLDraw data tags: - TLDraw security: - UserAuth: [] responses: '200': description: TLDraw data content: application/json: schema: $ref: '#/components/schemas/ContentNodeTLDraw' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' put: operationId: updateTLDraw summary: Replace TLDraw data description: Replace TLDraw data tags: - TLDraw security: - UserAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContentNodeTLDraw' responses: '200': description: TLDraw data updated successfully '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError' components: securitySchemes: UserAuth: type: apiKey in: header name: X-User-ID description: | User authentication via headers: - X-User-ID: User identifier - X-User-Name: User display name - X-User-Email: User email - X-User-Roles: Comma-separated roles (teacher,admin,etc.) parameters: PageParam: name: page in: query description: Page number (1-based) schema: type: integer minimum: 1 maximum: 1000 default: 1 example: 1 LimitParam: name: limit in: query description: Items per page schema: type: integer minimum: 1 maximum: 100 default: 10 example: 20 ImportUidParam: name: uid in: path required: true description: Import UUID schema: type: string format: uuid example: 123e4567-e89b-12d3-a456-426614174000 ContentIdParam: name: id in: path required: true description: Content ID schema: type: integer example: 42 SubjectUidParam: name: uid in: path required: true description: Subject UUID schema: type: string format: uuid example: 123e4567-e89b-12d3-a456-426614174000 QualificationUidParam: name: uid in: path required: true description: Qualification UUID schema: type: string format: uuid example: 123e4567-e89b-12d3-a456-426614174000 LevelIdParam: name: level_id in: path required: true description: Level ID schema: type: string example: GCSE AwardingBodyIdParam: name: awarding_body in: path required: true description: Awarding Body ID schema: type: string example: AQA schemas: ErrorResponse: type: object required: - error - message - status properties: error: type: string description: Error code example: INVALID_FILE_FORMAT message: type: string description: Human-readable error message example: Only PDF files are supported status: type: integer description: HTTP status code example: 400 Import: type: object required: - uid - name - phase - status - progress properties: uid: type: string format: uuid description: Unique import identifier example: 123e4567-e89b-12d3-a456-426614174000 name: type: string description: Import name example: Advanced Mathematics Textbook phase: type: integer enum: - 1 - 2 description: Current processing phase example: 1 status: type: string enum: - UPLOADED - IN_PROGRESS - DONE - ERROR - PUBLISHED description: Import processing status example: IN_PROGRESS progress: type: integer minimum: 0 maximum: 100 description: Overall progress percentage example: 75 progress_number: type: integer description: Current step number example: 3 progress_total: type: integer description: Total steps example: 4 progress_messages: type: string description: Current operation description example: Generating L2 content nodes error: type: string description: Error message if status is ERROR example: '' created_at: type: string format: date-time description: Creation timestamp updated_at: type: string format: date-time description: Last update timestamp phase2_done_count: type: integer description: Completed L7 nodes example: 12 phase2_error_count: type: integer description: Failed L7 nodes example: 0 phase2_total_count: type: integer description: Total L7 nodes example: 15 ImportPage: type: object required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/Import' total: type: integer description: Total number of imports example: 150 page: type: integer description: Current page number example: 1 limit: type: integer description: Items per page example: 20 ContentNode: type: object required: - id - uuid - name - schema_hierarchy_level - version - media properties: id: type: integer description: Unique node identifier (internal) example: 42 uuid: type: string format: uuid description: Unique node identifier (external, stable) example: 123e4567-e89b-12d3-a456-426614174000 import_uid: type: string format: uuid description: Associated import ID name: type: string description: Node name example: Introduction to Algebra schema_hierarchy_level: type: integer minimum: 1 maximum: 7 description: Level in course hierarchy (1=Course, 7=Activity) example: 3 parent_id: type: integer nullable: true description: Parent node ID (null for root nodes) example: 25 has_content: type: boolean description: Whether node contains teaching content example: true branch_type: type: string description: Type of content branch first_created: type: string format: date-time description: Creation timestamp last_update: type: string format: date-time description: Last update timestamp tags: type: array items: type: string description: Content tags example: - algebra - mathematics - beginner internal_notes: type: string description: Internal notes creator: type: string description: Creator user ID last_edited_by: type: string description: Last editor user ID internal_owner: type: string description: Internal owner content: $ref: '#/components/schemas/ContentData' order_index: type: integer description: Position within parent example: 1 children: type: array items: $ref: '#/components/schemas/ContentNode' description: Child nodes phase2_status: type: string description: Phase 2 processing status phase2_error: type: string nullable: true description: Phase 2 error message (null if no error) pre_order_index: type: integer nullable: true description: Pre-order traversal index (null if not calculated) media: $ref: '#/components/schemas/MediaAttributes' version: type: integer description: Version for optimistic locking minimum: 1 example: 1 ContentNodePage: type: object required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/ContentNode' total: type: integer description: Total number of content nodes example: 150 page: type: integer description: Current page number example: 1 limit: type: integer description: Items per page example: 20 ContentData: type: object properties: taxonomy_fields: $ref: '#/components/schemas/TaxonomyFields' content_fields: $ref: '#/components/schemas/ContentFields' TaxonomyFields: type: object properties: overview_text_full_course: type: string description: Overview text for full course overview_text_jump_in: type: string description: Overview text for jump in ContentFields: type: object properties: content_title: type: string description: Content title teaching_content: type: string description: Teaching content learning_outcomes: $ref: '#/components/schemas/LearningOutcomes' special_instructions: type: string description: Special instructions teaching_script: type: string description: Teaching script micro_interaction: $ref: '#/components/schemas/MicroInteraction' LearningOutcomes: type: object properties: explicit: type: string description: Explicit learning outcomes internally_generated: type: string description: Internally generated learning outcomes MicroInteraction: type: object properties: ask_micro_test: type: boolean description: Ask micro test ask_any_questions: type: boolean description: Ask any questions ask_got_it: type: boolean description: Ask got it question: type: string description: Question answer: type: string description: Answer consequence_if_wrong: type: string description: Consequence if wrong version: type: integer description: Version for optimistic locking minimum: 1 example: 1 ContentNodePartialUpdate: type: object description: Partial update request for content nodes - only provided fields will be updated properties: name: type: string description: Content node name branch_type: type: string description: Type of branch (lesson, topic, etc.) has_content: type: boolean description: Whether the node has teaching content content: type: object description: Content data (JSON object) order_index: type: integer description: Position within parent version: type: integer description: Version number for optimistic locking minimum: 1 BatchUpdateRequest: type: object required: - updates properties: updates: type: array maxItems: 50 minItems: 1 description: Array of content node updates (max 50) items: type: object required: - id - changes properties: id: type: integer description: Content node ID to update changes: $ref: '#/components/schemas/ContentNodePartialUpdate' BatchUpdateResponse: type: object properties: success_count: type: integer description: Number of successfully updated nodes error_count: type: integer description: Number of failed updates results: type: array items: type: object properties: id: type: integer success: type: boolean error: type: string description: Error message if success is false updated_node: $ref: '#/components/schemas/ContentNode' description: Updated node data if success is true TLDrawUpdateRequest: type: object required: - tldraw_data properties: tldraw_data: type: object description: TLDraw canvas data (JSON object) compress: type: boolean default: true description: Whether to compress the data before storage version: type: integer description: Version number for optimistic locking ConflictError: type: object properties: error: type: string example: Version conflict detected current_version: type: integer description: Current version in database provided_version: type: integer description: Version provided in request message: type: string example: The content has been modified by another user. Please refresh and try again. SimplifiedContentNode: type: object required: - id - name - schema_hierarchy_level - media properties: id: type: integer description: Unique node identifier example: 42 name: type: string description: Node name example: Introduction to Algebra schema_hierarchy_level: type: integer minimum: 1 maximum: 7 description: Level in course hierarchy (1=Course, 7=Activity) example: 3 media: $ref: '#/components/schemas/MediaAttributes' children: type: array items: $ref: '#/components/schemas/SimplifiedContentNode' description: Child nodes MediaAttributes: type: object required: - attribution - title - license - source - requires_attribution - requires_annual_update properties: attribution: type: string description: Attribution default: '' title: type: string description: Title default: '' license: type: string description: License default: '' source: type: string description: Source default: '' requires_attribution: type: boolean description: Requires attribution default: false requires_annual_update: type: boolean description: Requires annual update default: false internal_notes: type: string description: Internal notes about the media default: '' ReorderContentRequest: type: object required: - order_index properties: order_index: type: integer description: New order index ReorderOperation: type: object required: - node_id - position properties: node_id: type: integer description: ID of the node to move example: 123 new_parent_id: type: integer nullable: true description: ID of the new parent node (null for root level) example: 456 position: type: integer description: Position index under the new parent (0-based) example: 2 minimum: 0 StageRequest: type: object required: - session_id properties: session_id: type: string format: uuid description: Reorder session ID example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 operations: type: array description: Array of reorder/move operations to apply maxItems: 50 items: $ref: '#/components/schemas/ReorderOperation' deletions: type: array description: Array of delete operations to apply (deletes node and all descendants) maxItems: 50 items: $ref: '#/components/schemas/DeleteOperation' renames: type: array description: Array of rename operations to apply maxItems: 50 items: $ref: '#/components/schemas/RenameOperation' creations: type: array description: Array of create operations to add new nodes to the staged tree maxItems: 50 items: $ref: '#/components/schemas/CreateOperation' CreateOperation: type: object required: - name - schema_hierarchy_level - position properties: temp_id: type: string description: Temporary client-side ID for referencing this node in subsequent operations example: temp-node-1 name: type: string description: Name/title for the new node minLength: 1 maxLength: 500 example: 'New Topic: Climate Change' schema_hierarchy_level: type: integer minimum: 1 maximum: 7 description: Level in course hierarchy (1=Course, 7=Activity) example: 3 parent_id: type: integer nullable: true description: ID of the parent node (null for root level) example: 456 position: type: integer description: Position index under the parent (0-based) example: 0 minimum: 0 branch_type: type: string nullable: true description: Type of content branch (optional, defaults to "Normal" if not provided) default: Normal example: Normal DeleteOperation: type: object required: - node_id properties: node_id: type: integer description: ID of the node to delete (will also delete all descendants) example: 123 RenameOperation: type: object required: - node_id - new_name properties: node_id: type: integer description: ID of the node to rename example: 123 new_name: type: string description: New name/title for the node minLength: 1 maxLength: 500 example: Introduction to Advanced Mathematics CommitRequest: type: object required: - session_id properties: session_id: type: string format: uuid description: Reorder session ID example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 force_commit: type: boolean description: Force commit even if validation warnings exist default: false ResetRequest: type: object required: - session_id properties: session_id: type: string format: uuid description: Reorder session ID example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 ValidationWarning: type: object required: - message - severity properties: node_id: type: integer description: ID of the node with the warning example: 123 node_name: type: string description: Name of the node with the warning example: Introduction to Algebra message: type: string description: Warning message example: Branch exceeds maximum allowed depth. Max 7, used 8. severity: type: string description: Warning severity level enum: - INFO - WARNING - ERROR example: ERROR CreatedNodeInfo: type: object required: - id - uuid - title properties: id: type: integer description: Generated node ID (negative for staged nodes, will become positive after commit) example: -1 uuid: type: string format: uuid description: UUID assigned to the new node example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 title: type: string description: Title/name of the created node example: 'New Topic: Climate Change' parent_id: type: integer nullable: true description: Parent node ID example: 456 position: type: integer description: Position under parent example: 0 ReorderResponse: type: object required: - success properties: success: type: boolean description: Whether the operation succeeded example: true warnings: type: array description: Validation warnings items: $ref: '#/components/schemas/ValidationWarning' affected_node_count: type: integer description: Number of nodes affected by the operation example: 15 committed_count: type: integer description: Number of nodes committed (for commit operations) example: 150 conflict_detected: type: boolean description: Whether a tree conflict was detected example: false created_nodes: type: object description: Mapping of temp_id to created node info. Only present when nodes are created via the creations array. additionalProperties: $ref: '#/components/schemas/CreatedNodeInfo' example: temp-node-1: id: -1 uuid: a1b2c3d4-e5f6-7890-abcd-ef1234567890 title: New Topic parent_id: 456 position: 0 staged_tree: type: array description: The updated staged tree after applying operations. Returned by stage operations to keep frontend in sync. Includes all nodes with updated positions and the newly created nodes. items: $ref: '#/components/schemas/ContentNode' session_info: $ref: '#/components/schemas/SessionInfo' description: Updated session information including new expiration time after TTL refresh. SessionInfo: type: object required: - session_id - import_uid - created_at - expires_at - node_count properties: session_id: type: string format: uuid description: Session unique identifier example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 import_uid: type: string format: uuid description: Import unique identifier example: 123e4567-e89b-12d3-a456-426614174000 created_at: type: string format: date-time description: Session creation timestamp example: '2025-01-15T10:30:00Z' expires_at: type: string format: date-time description: Session expiration timestamp example: '2025-01-15T11:00:00Z' node_count: type: integer description: Number of nodes in the tree example: 150 original_tree_hash: type: string description: Hash of the original tree structure example: abc123def456 TreePreviewResponse: type: object required: - nodes - warnings - session_info properties: nodes: type: array description: Array of content nodes in the staged tree items: $ref: '#/components/schemas/ContentNode' warnings: type: array description: Validation warnings for the current tree state items: $ref: '#/components/schemas/ValidationWarning' session_info: $ref: '#/components/schemas/SessionInfo' AIPrompts: type: object properties: prompt_1: type: string description: Default prompt prompt_2: type: string description: Custom prompt 2 prompt_3: type: string description: Custom prompt 3 prompt_4: type: string description: Custom prompt 4 prompt_5: type: string description: Custom prompt 5 ReplaceAIPromptsRequest: type: object properties: prompt_2: type: string description: Custom prompt 2 prompt_3: type: string description: Custom prompt 3 prompt_4: type: string description: Custom prompt 4 prompt_5: type: string description: Custom prompt 5 AskRequest: type: object required: - prompt - input properties: prompt: type: string description: AI prompt input: type: string description: Input text to improve AskResponse: type: object required: - output properties: output: type: string description: AI-improved output Subject: type: object required: - uid - name properties: uid: type: string format: uuid description: Subject unique identifier name: type: string description: Subject name (unique identifier) full_name: type: string description: Full subject name short_name: type: string description: Short subject name icon_url: type: string description: Icon URL small_image_url: type: string description: Small image URL hero_image_url: type: string description: Hero image URL created_at: type: string format: date-time description: Creation timestamp updated_at: type: string format: date-time description: Last update timestamp SubjectPage: type: object required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/Subject' total: type: integer description: Total number of subjects page: type: integer description: Current page number limit: type: integer description: Items per page SubjectFilter: type: object required: - uid - short_name properties: uid: type: string format: uuid description: Subject unique identifier short_name: type: string description: Short subject name Level: type: object required: - id - name - sort_order - student_activation_flag properties: id: type: string description: Level ID maxLength: 50 name: type: string description: Level name maxLength: 255 description: type: string description: Level description sort_order: type: integer description: Sort order default: 0 student_activation_flag: type: boolean description: Flag indicating if visible to students default: true international_standardized_level: type: string description: International standardized level for comparison maxLength: 100 age_range_min: type: integer description: Minimum age for this level (future use) age_range_max: type: integer description: Maximum age for this level (future use) created_at: type: string format: date-time description: Creation timestamp created_by: type: string description: User who created this level updated_at: type: string format: date-time description: Last update timestamp updated_by: type: string description: User who last updated this level LevelRequest: type: object required: - id - name - sort_order properties: id: type: string description: Level ID maxLength: 50 name: type: string description: Level name maxLength: 255 description: type: string description: Level description sort_order: type: integer description: Sort order default: 0 student_activation_flag: type: boolean description: Flag indicating if visible to students default: true international_standardized_level: type: string description: International standardized level maxLength: 100 age_range_min: type: integer description: Minimum age age_range_max: type: integer description: Maximum age created_by: type: string description: User creating this level LevelUpdateRequest: type: object properties: name: type: string description: Level name maxLength: 255 description: type: string description: Level description sort_order: type: integer description: Sort order student_activation_flag: type: boolean description: Flag indicating if visible to students international_standardized_level: type: string description: International standardized level maxLength: 100 age_range_min: type: integer description: Minimum age age_range_max: type: integer description: Maximum age updated_by: type: string description: User updating this level LevelPage: type: object required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/Level' total: type: integer description: Total number of levels page: type: integer description: Current page number limit: type: integer description: Items per page AwardingBody: type: object required: - id - name - code properties: id: type: integer description: Awarding body ID name: type: string description: Awarding body full name maxLength: 255 code: type: string description: Awarding body code/short name maxLength: 50 description: type: string description: Awarding body description created_at: type: string format: date-time description: Creation timestamp updated_at: type: string format: date-time description: Last update timestamp created_by: type: string description: User who created this awarding body updated_by: type: string description: User who last updated this awarding body AwardingBodyRequest: type: object required: - name - code properties: name: type: string description: Awarding body full name maxLength: 255 code: type: string description: Awarding body code/short name maxLength: 50 description: type: string description: Awarding body description created_by: type: string description: User creating this awarding body AwardingBodyUpdateRequest: type: object properties: name: type: string description: Awarding body full name maxLength: 255 code: type: string description: Awarding body code/short name maxLength: 50 description: type: string description: Awarding body description updated_by: type: string description: User updating this awarding body AwardingBodyPage: type: object required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/AwardingBody' total: type: integer description: Total number of awarding bodies page: type: integer description: Current page number limit: type: integer description: Items per page Qualification: type: object required: - uid - subject_uid - academic_level_id - awarding_body_id - qualification_name_system - qualification_name_display properties: uid: type: string format: uuid description: Qualification unique identifier academic_level_id: type: string description: Academic level ID (renamed from level_id) maxLength: 50 subject_uid: type: string format: uuid description: Subject UID awarding_body_id: type: integer description: Awarding body ID qualification_name_system: type: string maxLength: 500 description: System-generated name - {academic_level} {subject} {AwardingBody} - {user_suffix} qualification_name_display: type: string maxLength: 500 description: Display name shown to students qualification_description: type: string description: Long-form rich text description publish_to_production_flag: type: boolean default: false description: Flag to publish to production environment publish_to_dev_student_app_flag: type: boolean default: false description: Flag to publish to dev student app publish_to_test_groups: type: array items: type: string description: Test group IDs for future feature parent_content_library_id: type: string format: uuid nullable: true description: Parent content library ID (renamed from tree_uid) tier: type: string description: Tier (legacy field) maxLength: 50 title: type: string description: Legacy title field (use qualification_name_display instead) deprecated: true maxLength: 500 description: type: string description: Legacy description field (use qualification_description instead) deprecated: true created_by: type: string description: User who created this qualification updated_by: type: string description: User who last updated this qualification created_at: type: string format: date-time description: Creation timestamp updated_at: type: string format: date-time description: Last update timestamp level: nullable: true allOf: - $ref: '#/components/schemas/Level' description: Level details (nullable - populated when expanded) subject: nullable: true allOf: - $ref: '#/components/schemas/Subject' description: Subject details (nullable - populated when expanded) awarding_body: nullable: true allOf: - $ref: '#/components/schemas/AwardingBody' description: Awarding body details (nullable - populated when expanded) QualificationRequest: type: object required: - academic_level_id - subject_uid - awarding_body_id - qualification_name_display properties: academic_level_id: type: string description: Academic level ID maxLength: 50 subject_uid: type: string format: uuid description: Subject UID awarding_body_id: type: integer description: Awarding body ID qualification_name_display: type: string maxLength: 500 description: Display name for students qualification_name_system_suffix: type: string description: User-provided suffix for system name (system will prepend level/subject/body) qualification_description: type: string description: Long-form description publish_to_production_flag: type: boolean default: false publish_to_dev_student_app_flag: type: boolean default: false publish_to_test_groups: type: array items: type: string parent_content_library_id: type: string format: uuid tier: type: string maxLength: 50 created_by: type: string description: User creating this qualification QualificationUpdateRequest: type: object properties: academic_level_id: type: string maxLength: 50 subject_uid: type: string format: uuid awarding_body_id: type: integer qualification_name_display: type: string maxLength: 500 qualification_name_system_suffix: type: string description: User-provided suffix (system regenerates full name) qualification_description: type: string publish_to_production_flag: type: boolean publish_to_dev_student_app_flag: type: boolean publish_to_test_groups: type: array items: type: string parent_content_library_id: type: string format: uuid tier: type: string maxLength: 50 updated_by: type: string description: User updating this qualification QualificationPage: type: object required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/Qualification' total: type: integer description: Total number of qualifications page: type: integer description: Current page number limit: type: integer description: Items per page UserQualification: type: object required: - user_id - qualification_uid - created_at properties: user_id: type: string description: User ID qualification_uid: type: string format: uuid description: Qualification UID qualification: nullable: true allOf: - $ref: '#/components/schemas/Qualification' description: Full qualification details (nullable - populated when expanded) created_at: type: string format: date-time description: Creation timestamp UserQualificationPage: type: object required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/UserQualification' total: type: integer description: Total number of user qualifications page: type: integer description: Current page number limit: type: integer description: Items per page ContentNodeTLDraw: type: object required: - id - has_data properties: id: type: integer description: Node ID has_data: type: boolean description: Whether TLDraw data exists data: type: object nullable: true description: TLDraw data (JSON) - null when has_data is false created_at: type: string format: date-time description: Creation timestamp updated_at: type: string format: date-time description: Last update timestamp responses: BadRequest: description: Bad request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' InternalError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse'