# Practice Creation API Guide (Lesson Scope) This guide provides the full step-by-step API process to create a lesson practice when using: - system-defined question types (`MCQ`, `TRUE_FALSE`, `SHORT_ANSWER`, `AUDIO`) - dynamic question types (`DYNAMIC` with `question_type_definition_id` + `dynamic_payload`) All endpoints below are relative to `/api/v1` and require bearer authentication. --- ## Standard Response Envelope Most successful responses follow: ```json { "message": "Human-readable message", "data": {}, "success": true, "status_code": 200, "metadata": null } ``` Most errors follow: ```json { "message": "Error summary", "error": "Detailed reason" } ``` --- ## Required Permissions At minimum, your role should have: - `questions.create` - `question_sets.create` - `question_set_items.add` - `practices.create` If you create/update dynamic definitions: - `questions.update` - `questions.delete` (if you also delete definitions) --- ## End-to-End Flow 1. (Optional) Upload media assets 2. Create question(s): - system-defined path, or - dynamic path (definition + question) 3. Create `PRACTICE` question set 4. Add question(s) to the set 5. Create lesson practice linked to that set 6. Verify under lesson --- ## Step 0 (Optional): Upload Media Use this when question content references audio/image URLs. ### Endpoint `POST /files/upload` (multipart form-data) ### Form fields - `file`: binary - `media_type`: `image` or `audio` or `video` ### Example success response (shape) ```json { "message": "Media uploaded successfully", "data": { "url": "https://your-host/static/uploads/audio/abc.mp3", "object_key": "audio/abc.mp3" }, "success": true, "status_code": 201 } ``` ### Common errors - `400` invalid media type/content type - `500` upload/storage failure Capture and reuse: - `data.url` (or equivalent resolved file URL) --- ## Step 1A: Create System-Defined Question(s) ### Endpoint `POST /questions` ### Request example (MCQ) ```json { "question_text": "Choose the correct sentence.", "question_type": "MCQ", "difficulty_level": "EASY", "points": 1, "status": "PUBLISHED", "options": [ { "option_text": "He go to school.", "is_correct": false }, { "option_text": "He goes to school.", "is_correct": true } ] } ``` ### Request example (SHORT_ANSWER) ```json { "question_text": "Write one sentence using the word 'improve'.", "question_type": "SHORT_ANSWER", "difficulty_level": "MEDIUM", "points": 2, "status": "PUBLISHED", "short_answers": [ { "acceptable_answer": "I want to improve my English.", "match_type": "CASE_INSENSITIVE" } ] } ``` ### Example success response (shape) ```json { "message": "Question created successfully", "data": { "id": 456, "question_text": "Choose the correct sentence.", "question_type": "MCQ", "status": "PUBLISHED" }, "success": true, "status_code": 201 } ``` ### Common errors - `400` validation/body errors - `500` create failure Capture: - `data.id` as `question_id` --- ## Step 1B: Dynamic Question Path If you use dynamic questions, follow these sub-steps. ### 1B.1 Validate component-kind selection (optional but recommended) #### Endpoint `POST /questions/validate-question-type-definition` #### Request ```json { "stimulus_component_kinds": ["QUESTION_TEXT", "AUDIO_PROMPT", "IMAGE"], "response_component_kinds": ["AUDIO_RESPONSE", "TEXT_INPUT"] } ``` #### Success response ```json { "message": "Question type definition is valid", "data": { "valid": true } } ``` #### Error response example ```json { "message": "Invalid question type definition", "error": "response: unknown component kind \"AUDIO_PROMPT\"" } ``` --- ### 1B.2 Create or reuse a dynamic type definition #### Endpoint `POST /questions/type-definitions` #### Request ```json { "key": "dialogue_audio_avatar_v1", "display_name": "Dialogue Audio + Avatar", "description": "Question text + prompt audio + two avatar images, with audio/text answer", "stimulus_component_kinds": ["QUESTION_TEXT", "AUDIO_PROMPT", "IMAGE"], "response_component_kinds": ["AUDIO_RESPONSE", "TEXT_INPUT"], "stimulus_schema": [ { "id": "question_text", "kind": "QUESTION_TEXT", "required": true }, { "id": "prompt_audio", "kind": "AUDIO_PROMPT", "required": true }, { "id": "speaker_a_avatar", "kind": "IMAGE", "required": true }, { "id": "speaker_b_avatar", "kind": "IMAGE", "required": true } ], "response_schema": [ { "id": "answer_audio", "kind": "AUDIO_RESPONSE", "required": true }, { "id": "answer_text", "kind": "TEXT_INPUT", "required": true } ], "status": "ACTIVE" } ``` #### Success response example ```json { "message": "Question type definition created", "data": { "id": 123, "key": "dialogue_audio_avatar_v1", "status": "ACTIVE" } } ``` #### Common errors - `400` invalid schema/kinds/mapping - `500` unexpected persistence errors Capture: - `data.id` as `question_type_definition_id` --- ### 1B.3 Create dynamic question #### Endpoint `POST /questions` #### Request ```json { "question_text": "Listen and respond as Speaker B.", "question_type": "DYNAMIC", "question_type_definition_id": 123, "difficulty_level": "MEDIUM", "points": 2, "status": "PUBLISHED", "dynamic_payload": { "stimulus": [ { "id": "question_text", "kind": "QUESTION_TEXT", "value": "Respond to the conversation." }, { "id": "prompt_audio", "kind": "AUDIO_PROMPT", "value": "https://cdn.example.com/audio/prompt-1.mp3" }, { "id": "speaker_a_avatar", "kind": "IMAGE", "value": "https://cdn.example.com/images/a.webp" }, { "id": "speaker_b_avatar", "kind": "IMAGE", "value": "https://cdn.example.com/images/b.webp" } ], "response": [ { "id": "answer_audio", "kind": "AUDIO_RESPONSE", "value": { "instructions": "Record your answer" } }, { "id": "answer_text", "kind": "TEXT_INPUT", "value": { "placeholder": "Type your answer" } } ] } } ``` #### Success response example ```json { "message": "Question created successfully", "data": { "id": 789, "question_type": "DYNAMIC", "question_type_definition_id": 123, "status": "PUBLISHED" }, "success": true, "status_code": 201 } ``` #### Common errors - `400` missing/invalid `dynamic_payload` - `400` missing `question_type_definition_id` - `500` persistence failure Capture: - `data.id` as `question_id` --- ## Step 2: Create PRACTICE Question Set ### Endpoint `POST /question-sets` ### Request ```json { "title": "Lesson 12 - Practice Set", "description": "Question set for lesson-level practice", "set_type": "PRACTICE", "owner_type": "LESSON", "owner_id": 12, "status": "PUBLISHED", "shuffle_questions": false } ``` ### Success response example ```json { "message": "Question set created successfully", "data": { "id": 55, "title": "Lesson 12 - Practice Set", "set_type": "PRACTICE", "owner_type": "LESSON", "owner_id": 12, "status": "PUBLISHED" }, "success": true, "status_code": 201 } ``` ### Common errors - `400` invalid input - `500` create failure Capture: - `data.id` as `set_id` --- ## Step 3: Add Question(s) to Set Run this once per `question_id`. ### Endpoint `POST /question-sets/:setId/questions` ### Request ```json { "question_id": 456, "display_order": 1 } ``` ### Success response example ```json { "message": "Question added to set successfully", "data": { "id": 901, "set_id": 55, "question_id": 456, "display_order": 1 }, "success": true, "status_code": 201 } ``` ### Common errors - `400` invalid `setId` or body - `500` link/create failure --- ## Step 4: Create Lesson Practice This creates the practice record scoped to lesson. ### Endpoint `POST /practices` ### Request Include `publish_status`: `DRAFT` to hide the practice from subscribed learners until you set it to `PUBLISHED` (via create or `PUT /practices/:id`). Omit the field or send `PUBLISHED` to go live immediately (backward compatible). ```json { "parent_kind": "LESSON", "parent_id": 12, "title": "Lesson 12 Conversation Drill", "story_description": "A short two-speaker scenario.", "story_image": "https://cdn.example.com/images/story.webp", "question_set_id": 55, "quick_tips": "Listen carefully before answering.", "publish_status": "DRAFT" } ``` ### Success response example ```json { "message": "Practice created successfully", "data": { "id": 37, "parent_kind": "LESSON", "parent_id": 12, "title": "Lesson 12 Conversation Drill", "question_set_id": 55 }, "success": true, "status_code": 201 } ``` ### Common errors - `400` validation failed / invalid parent kind - `404` lesson not found - `404` question set not found - `500` create failure Capture: - `data.id` as `practice_id` --- ## Step 5: Verify Practice Under Lesson ### Endpoint `GET /lessons/:id/practices` Example: `GET /lessons/12/practices` ### Success response example ```json { "message": "Practices retrieved successfully", "data": { "practices": [ { "id": 37, "parent_kind": "LESSON", "parent_id": 12, "title": "Lesson 12 Conversation Drill", "question_set_id": 55 } ], "total_count": 1, "limit": 20, "offset": 0 }, "success": true, "status_code": 200 } ``` --- ## Optional Learner Completion Step ### Endpoint `POST /progress/practices/:id/complete` Use `practice_id` as `:id` for current behavior. ### Success response example ```json { "message": "Practice completed", "success": true, "status_code": 200 } ``` ### Common errors - `403` sequence gating violation - `404` practice not found - `500` completion/persistence failure --- ## Quick Checklist (IDs to Carry Forward) - From question create: `question_id` - From dynamic definition create (if used): `question_type_definition_id` - From question set create: `set_id` - From practice create: `practice_id` --- ## Notes and Pitfalls - For dynamic questions, `question_type` must be `DYNAMIC`. - For dynamic questions, both `question_type_definition_id` and `dynamic_payload` are required. - `AUDIO_PROMPT` is stimulus-side; response-side audio uses `AUDIO_RESPONSE`. - `question_set_id` in `POST /practices` must reference an existing set. - For lesson practice always use: - `parent_kind = "LESSON"` - `parent_id = ` - Publish questions and question set (`status = "PUBLISHED"`) if learners must complete immediately.