{
 "openapi": "3.1.0",
 "info": {
  "title": "KlickCourse Course-Builder API",
  "version": "1.0.0",
  "description": "Create courses, modules and lessons programmatically, and read CRM activity feeds. Authenticate with a tenant-scoped API key (Authorization: Bearer kc_...) minted in each institute's admin. Builder endpoints require the course:write scope; /admin/crm/* read feeds require crm:read. All calls act within the institute of the host you call. See https://developer.klickcourse.com."
 },
 "servers": [
  {
   "url": "https://{institute}.klickcourse.com/api/v1",
   "description": "An institute's API base. Replace {institute} with your subdomain.",
   "variables": {
    "institute": {
     "default": "demo",
     "description": "Your institute subdomain."
    }
   }
  }
 ],
 "security": [
  {
   "bearerAuth": []
  }
 ],
 "tags": [
  {
   "name": "courses",
   "description": "Course builder (course:write)"
  },
  {
   "name": "categories",
   "description": "Catalog categories — storefront sections (course:write)"
  }
 ],
 "paths": {
  "/admin/courses/import": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Atomically create a whole course tree",
    "description": "Creates a course plus its modules and lessons in one transaction. Rolls back entirely on any invalid input. Creates a draft unless publish=true.",
    "operationId": "importCourse",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/CourseImport"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Created",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ImportResult"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   }
  },
  "/admin/courses": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Create a course",
    "operationId": "createCourse",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "title",
         "slug"
        ],
        "properties": {
         "title": {
          "type": "string"
         },
         "slug": {
          "type": "string",
          "pattern": "^[a-z0-9-]+$"
         },
         "priceMinor": {
          "type": "string",
          "default": "0"
         },
         "descriptionHtml": {
          "type": "string"
         },
         "maxAppliedCreditPct": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Created",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   }
  },
  "/admin/courses/{id}": {
   "patch": {
    "tags": [
     "courses"
    ],
    "summary": "Update a course",
    "operationId": "updateCourse",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "priceMinor": {
          "type": "string"
         },
         "descriptionHtml": {
          "type": "string"
         },
         "maxAppliedCreditPct": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100
         },
         "coverUrl": {
          "type": "string",
          "nullable": true,
          "description": "https image URL, or null to clear. 16:9 (2560×1440) fills the catalog card exactly; other ratios letterbox."
         },
         "subtitle": {
          "type": "string",
          "nullable": true
         },
         "instructorName": {
          "type": "string",
          "nullable": true
         },
         "instructorBio": {
          "type": "string",
          "nullable": true
         },
         "instructorAvatarUrl": {
          "type": "string",
          "nullable": true
         },
         "skills": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "tools": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "details": {
          "type": "object"
         },
         "progressionMode": {
          "type": "string",
          "enum": [
           "open",
           "quiz_gated"
          ]
         },
         "categoryId": {
          "type": "string",
          "format": "uuid",
          "nullable": true,
          "description": "Catalog category id from GET /admin/categories, or null to unfile the course."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Updated"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   },
   "delete": {
    "tags": [
     "courses"
    ],
    "summary": "Soft-delete a course",
    "description": "Removes a draft/archived course from the catalog and admin (soft-delete; record kept). Refuses a PUBLISHED course (409 — unpublish/archive first) or one with active learners (409 — archive instead).",
    "operationId": "deleteCourse",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "responses": {
     "200": {
      "description": "Deleted"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   }
  },
  "/admin/courses/{id}/unpublish": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Unpublish a course (back to draft)",
    "description": "Inactivates a live course — off the catalog, back to editable draft. Reversible via publish.",
    "operationId": "unpublishCourse",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "responses": {
     "200": {
      "description": "Unpublished (draft)"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/admin/courses/{id}/archive": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Archive a course (retire from catalog)",
    "description": "Retires a course from the catalog without deleting the record. Reversible via publish. Safe for courses with learners.",
    "operationId": "archiveCourse",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "responses": {
     "200": {
      "description": "Archived"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/admin/courses/{id}/modules": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Add a module to a course",
    "operationId": "createModule",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "title"
        ],
        "properties": {
         "title": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Created",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/admin/modules/{moduleId}/lessons": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Add a lesson to a module",
    "operationId": "createLesson",
    "parameters": [
     {
      "name": "moduleId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/Lesson"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Created",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/admin/lessons/{lessonId}": {
   "patch": {
    "tags": [
     "courses"
    ],
    "summary": "Edit a lesson (title, HTML content below the media, embed URL, preview flag)",
    "description": "Updates an existing lesson. contentHtml is sanitized server-side to the safe subset; on video/audio lessons it renders below the player — use it for descriptions and CTA buttons (a link with class=\"kc-cta\" renders as a tenant-branded button). Omitted fields are left unchanged.",
    "operationId": "editLesson",
    "parameters": [
     {
      "name": "lessonId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "contentHtml": {
          "type": "string",
          "description": "Sanitized HTML shown with the lesson (below the player on video/audio). Supports the kc-cta button pattern."
         },
         "embedUrl": {
          "type": "string",
          "nullable": true,
          "description": "YouTube/Vimeo embed URL (allowlisted); null clears it."
         },
         "isPreview": {
          "type": "boolean"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Lesson updated",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "ok": {
           "type": "boolean"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/admin/lessons/{lessonId}/ingest-video": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Pull-ingest a public video URL into a video/audio lesson",
    "description": "Mux fetches the URL and turns it into a signed, enrollment-gated, completion-tracked asset. The lesson is 'processing' until Mux finishes (a webhook marks it ready). Alternative to passing videoUrl inline on import.",
    "operationId": "ingestLessonVideo",
    "parameters": [
     {
      "name": "lessonId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "url"
        ],
        "properties": {
         "url": {
          "type": "string",
          "format": "uri",
          "description": "Public https video URL"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Ingest started",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "status": {
           "type": "string",
           "enum": [
            "processing"
           ]
          },
          "assetId": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/admin/courses/{id}/generate-metadata": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "AI-draft landing-page metadata from the course's own content",
    "operationId": "generateMetadata",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "responses": {
     "200": {
      "description": "Draft metadata"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/admin/courses/{id}/structure": {
   "get": {
    "tags": [
     "courses"
    ],
    "summary": "Read the course tree (modules + lessons)",
    "operationId": "getCourseStructure",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "responses": {
     "200": {
      "description": "Course structure"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/admin/courses/{id}/publish": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Publish a course (make it live)",
    "operationId": "publishCourse",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "responses": {
     "200": {
      "description": "Published",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "publishedVersion": {
           "type": "integer"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/admin/courses/{id}/quiz-attempts": {
   "get": {
    "summary": "Graded quiz attempts (module checks + final assessment) — poll module-pass events per enrollee",
    "description": "Institute-scoped feed for external systems (e.g. a CRM). Newest first. Use `since` (ISO timestamp, exclusive, on submittedAt) for incremental polling.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      }
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 2000,
       "default": 500
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Graded attempts",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "attempts": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/QuizAttempt"
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "Bad since/limit",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Course not found",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/admin/courses/{id}/module-quizzes": {
   "get": {
    "summary": "All module checks of a course (admin read, includes correct flags + explanations)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Module checks keyed by module",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/admin/modules/{moduleId}/quiz": {
   "post": {
    "summary": "Create a module's check (idempotent)",
    "parameters": [
     {
      "name": "moduleId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "passThresholdPct": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Created (or existing)",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "404": {
      "description": "Module not found",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/admin/quizzes/{quizId}": {
   "delete": {
    "summary": "Delete a MODULE check (refuses the final assessment)",
    "parameters": [
     {
      "name": "quizId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Deleted",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "Not found or course-level",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/admin/crm/enrollments": {
   "get": {
    "summary": "Enrollment feed (crm:read)",
    "description": "Institute-wide signups/enrollments, oldest first, cursor on createdAt. Poll with `since` = the last createdAt you processed.",
    "parameters": [
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "ISO timestamp, exclusive — return only rows after this moment (oldest first)."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 500,
       "default": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Enrollment feed (crm:read)",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "enrollments": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/CrmEnrollment"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Missing/invalid key or missing crm:read scope",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/admin/crm/quiz-attempts": {
   "get": {
    "summary": "Quiz-attempt feed (crm:read)",
    "description": "Institute-wide graded attempts (module checks + finals), oldest first, cursor on submittedAt. The per-course /admin/courses/{id}/quiz-attempts feed remains for course-scoped polling.",
    "parameters": [
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "ISO timestamp, exclusive — return only rows after this moment (oldest first)."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 500,
       "default": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Quiz-attempt feed (crm:read)",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "attempts": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/CrmQuizAttempt"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Missing/invalid key or missing crm:read scope",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/admin/crm/certificates": {
   "get": {
    "summary": "Certificate feed (crm:read)",
    "description": "Certificates issued (revoked excluded), oldest first, cursor on issuedAt.",
    "parameters": [
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "ISO timestamp, exclusive — return only rows after this moment (oldest first)."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 500,
       "default": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Certificate feed (crm:read)",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "certificates": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/CrmCertificate"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Missing/invalid key or missing crm:read scope",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/admin/crm/reviews": {
   "get": {
    "summary": "Course-review feed (crm:read)",
    "description": "Course reviews (all statuses — check `status`), oldest first, cursor on updatedAt. A review row reappears when edited or moderated.",
    "parameters": [
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "format": "date-time"
      },
      "description": "ISO timestamp, exclusive — return only rows after this moment (oldest first)."
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 500,
       "default": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Course-review feed (crm:read)",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "reviews": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/CrmReview"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Missing/invalid key or missing crm:read scope",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/admin/categories": {
   "get": {
    "tags": [
     "categories"
    ],
    "summary": "List catalog categories",
    "description": "Storefront sections in display order, each with the number of courses filed under it.",
    "operationId": "listCategories",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "categories": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "id": {
              "type": "string",
              "format": "uuid"
             },
             "name": {
              "type": "string"
             },
             "sort_order": {
              "type": "integer"
             },
             "course_count": {
              "type": "integer"
             }
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   },
   "post": {
    "tags": [
     "categories"
    ],
    "summary": "Create a category",
    "description": "Added at the end of the section order.",
    "operationId": "createCategory",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "name"
        ],
        "properties": {
         "name": {
          "type": "string",
          "maxLength": 60,
          "description": "Section heading shown on the catalog. Plain text; unique per institute, case-insensitively."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Created",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "id": {
           "type": "string",
           "format": "uuid"
          },
          "name": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   }
  },
  "/admin/categories/{id}": {
   "patch": {
    "tags": [
     "categories"
    ],
    "summary": "Rename a category",
    "operationId": "renameCategory",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "Category id",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "name"
        ],
        "properties": {
         "name": {
          "type": "string",
          "maxLength": 60,
          "description": "Section heading shown on the catalog. Plain text; unique per institute, case-insensitively."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Renamed"
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     },
     "409": {
      "$ref": "#/components/responses/Conflict"
     }
    }
   },
   "delete": {
    "tags": [
     "categories"
    ],
    "summary": "Delete a category",
    "description": "Its courses are NOT deleted — they lose the category and move to the uncategorized block at the end of the catalog.",
    "operationId": "deleteCategory",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "Category id",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Deleted"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/admin/categories/{id}/move": {
   "post": {
    "tags": [
     "categories"
    ],
    "summary": "Move a category one slot in the section order",
    "operationId": "moveCategory",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "description": "Category id",
      "schema": {
       "type": "string",
       "format": "uuid"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "direction"
        ],
        "properties": {
         "direction": {
          "type": "string",
          "enum": [
           "up",
           "down"
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Applied. `moved` is false when it was already at that end of the order (a no-op, not an error).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "ok": {
           "type": "boolean"
          },
          "moved": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  },
  "/admin/courses/{id}/move": {
   "post": {
    "tags": [
     "courses"
    ],
    "summary": "Move a course one slot in the catalog order",
    "description": "The order universe is the institute's full course list (drafts included). The public catalog filters to published courses and keeps the relative order.",
    "operationId": "moveCourse",
    "parameters": [
     {
      "$ref": "#/components/parameters/CourseId"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "direction"
        ],
        "properties": {
         "direction": {
          "type": "string",
          "enum": [
           "up",
           "down"
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Applied. `moved` is false when it was already at that end of the order (a no-op, not an error).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "ok": {
           "type": "boolean"
          },
          "moved": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "$ref": "#/components/responses/NotFound"
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {
   "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "kc_ API key",
    "description": "A kc_ API key minted in the institute admin. Scopes: course:write (builder endpoints) or crm:read (/admin/crm/* feeds)."
   }
  },
  "parameters": {
   "CourseId": {
    "name": "id",
    "in": "path",
    "required": true,
    "schema": {
     "type": "string",
     "format": "uuid"
    }
   }
  },
  "responses": {
   "BadRequest": {
    "description": "Invalid input",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Unauthorized": {
    "description": "Missing, invalid, or revoked API key, or missing scope",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "NotFound": {
    "description": "Not found in this institute",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Conflict": {
    "description": "Slug already exists (safe to treat as already-created)",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   }
  },
  "schemas": {
   "CourseImport": {
    "type": "object",
    "required": [
     "title",
     "slug"
    ],
    "properties": {
     "title": {
      "type": "string"
     },
     "slug": {
      "type": "string",
      "pattern": "^[a-z0-9-]+$",
      "description": "Lowercase, hyphenated, unique per institute."
     },
     "category": {
      "type": "string",
      "maxLength": 60,
      "description": "Catalog section by NAME — matched case-insensitively against the institute's categories, created if it doesn't exist yet."
     },
     "priceMinor": {
      "type": "string",
      "default": "0",
      "description": "Minor units as a string (e.g. \"4900\" = $49.00)."
     },
     "descriptionHtml": {
      "type": "string",
      "description": "Sanitized server-side."
     },
     "maxAppliedCreditPct": {
      "type": "integer",
      "minimum": 0,
      "maximum": 100
     },
     "publish": {
      "type": "boolean",
      "default": false,
      "description": "true = go live immediately; false = draft."
     },
     "coverUrl": {
      "type": "string",
      "format": "uri",
      "description": "Course thumbnail — an https image URL."
     },
     "subtitle": {
      "type": "string",
      "description": "Landing-page subtitle / one-line promise."
     },
     "instructorName": {
      "type": "string"
     },
     "instructorBio": {
      "type": "string"
     },
     "instructorAvatarUrl": {
      "type": "string",
      "format": "uri",
      "description": "https image URL."
     },
     "skills": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Skill chips (max 12)."
     },
     "tools": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Tool chips (max 12)."
     },
     "details": {
      "type": "object",
      "description": "Free-form landing-page detail fields."
     },
     "modules": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Module"
      }
     },
     "quiz": {
      "type": "object",
      "description": "Optional assessment built inline with the course. A default certificate template is created automatically; a learner earns the cert by completing + passing.",
      "properties": {
       "name": {
        "type": "string",
        "default": "Final assessment"
       },
       "progressGatePct": {
        "type": "integer",
        "minimum": 0,
        "maximum": 100,
        "description": "Watch % required before the quiz unlocks."
       },
       "passThresholdPct": {
        "type": "integer",
        "minimum": 1,
        "maximum": 100,
        "description": "Score needed to pass (drives cert issuance)."
       },
       "questions": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/Question"
        }
       },
       "certificate": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "bodyHtml": {
          "type": "string",
          "description": "Cert body; supports __STUDENT_NAME__ etc. Sanitized server-side."
         }
        }
       }
      }
     },
     "progressionMode": {
      "type": "string",
      "enum": [
       "open",
       "quiz_gated"
      ],
      "default": "open",
      "description": "open = learners move freely (default). quiz_gated = module N+1 unlocks when module N is cleared: pass its check if it has one, otherwise complete its lessons. First module always open."
     }
    }
   },
   "Module": {
    "type": "object",
    "required": [
     "title"
    ],
    "properties": {
     "title": {
      "type": "string"
     },
     "lessons": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Lesson"
      }
     },
     "quiz": {
      "$ref": "#/components/schemas/ModuleQuiz"
     }
    }
   },
   "Lesson": {
    "type": "object",
    "required": [
     "title",
     "type"
    ],
    "properties": {
     "title": {
      "type": "string"
     },
     "type": {
      "type": "string",
      "enum": [
       "video",
       "audio",
       "document",
       "text"
      ]
     },
     "contentHtml": {
      "type": "string",
      "description": "For text/document lessons; sanitized server-side."
     },
     "embedUrl": {
      "type": "string",
      "description": "YouTube/Vimeo embed URL (allowlisted). Mutually exclusive with videoUrl."
     },
     "videoUrl": {
      "type": "string",
      "format": "uri",
      "description": "Public https URL to an mp4/video (video/audio lessons). Mux pull-ingests it into a signed, gated, completion-tracked asset; the import response includes an `ingests` array with each lesson's processing status. Mutually exclusive with embedUrl. Requires Mux configured on the institute."
     },
     "durationSeconds": {
      "type": "integer",
      "minimum": 0
     },
     "isPreview": {
      "type": "boolean",
      "default": false
     }
    }
   },
   "ImportResult": {
    "type": "object",
    "properties": {
     "courseId": {
      "type": "string",
      "format": "uuid"
     },
     "moduleCount": {
      "type": "integer"
     },
     "lessonCount": {
      "type": "integer"
     },
     "publishedVersion": {
      "type": [
       "integer",
       "null"
      ]
     },
     "structure": {
      "type": "object",
      "description": "The full created course tree."
     },
     "ingests": {
      "type": "array",
      "description": "Present only if any lesson had a videoUrl. Per-lesson Mux pull-ingest status.",
      "items": {
       "type": "object",
       "properties": {
        "lessonId": {
         "type": "string",
         "format": "uuid"
        },
        "status": {
         "type": "string",
         "enum": [
          "ready",
          "processing",
          "failed"
         ]
        },
        "assetId": {
         "type": "string"
        },
        "playbackId": {
         "type": "string"
        },
        "error": {
         "type": "string"
        }
       }
      }
     },
     "moduleQuizzes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "moduleId": {
         "type": "string"
        },
        "quizId": {
         "type": "string"
        },
        "questionCount": {
         "type": "integer"
        }
       }
      }
     },
     "progressionMode": {
      "type": "string",
      "enum": [
       "open",
       "quiz_gated"
      ]
     }
    }
   },
   "Error": {
    "type": "object",
    "properties": {
     "message": {
      "type": "string"
     },
     "statusCode": {
      "type": "integer"
     },
     "error": {
      "type": "string"
     }
    }
   },
   "Question": {
    "type": "object",
    "required": [
     "prompt",
     "options"
    ],
    "properties": {
     "prompt": {
      "type": "string"
     },
     "explanation": {
      "type": "string",
      "description": "The \"why\" — revealed to the learner after a module-check submission (never before). Max 2000 chars."
     },
     "options": {
      "type": "array",
      "minItems": 2,
      "maxItems": 10,
      "items": {
       "type": "object",
       "required": [
        "text"
       ],
       "properties": {
        "text": {
         "type": "string"
        },
        "isCorrect": {
         "type": "boolean"
        }
       }
      }
     }
    }
   },
   "ModuleQuiz": {
    "type": "object",
    "description": "A module check: a short quiz owned by one module. Unlimited retakes, no certificate; under progressionMode quiz_gated, passing it unlocks the next module. Correct answers + explanations are revealed after each submission.",
    "properties": {
     "name": {
      "type": "string",
      "description": "Defaults to \"<module title> check\"."
     },
     "passThresholdPct": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 80
     },
     "questions": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Question"
      }
     }
    }
   },
   "QuizAttempt": {
    "type": "object",
    "properties": {
     "attemptId": {
      "type": "string"
     },
     "quizId": {
      "type": "string"
     },
     "quizName": {
      "type": "string"
     },
     "scope": {
      "type": "string",
      "enum": [
       "module",
       "final"
      ],
      "description": "module = a module check; final = the course-level final assessment."
     },
     "moduleId": {
      "type": "string",
      "nullable": true
     },
     "moduleTitle": {
      "type": "string",
      "nullable": true
     },
     "userId": {
      "type": "string"
     },
     "studentName": {
      "type": "string",
      "nullable": true
     },
     "email": {
      "type": "string"
     },
     "enrollmentId": {
      "type": "string"
     },
     "scorePct": {
      "type": "integer",
      "nullable": true
     },
     "passed": {
      "type": "boolean",
      "nullable": true
     },
     "startedAt": {
      "type": "string",
      "format": "date-time"
     },
     "submittedAt": {
      "type": "string",
      "format": "date-time",
      "nullable": true
     }
    }
   },
   "CrmEnrollment": {
    "type": "object",
    "properties": {
     "enrollment_id": {
      "type": "string"
     },
     "user_id": {
      "type": "string"
     },
     "email": {
      "type": "string"
     },
     "student_name": {
      "type": "string",
      "nullable": true
     },
     "course_id": {
      "type": "string"
     },
     "course_title": {
      "type": "string"
     },
     "grant_type": {
      "type": "string"
     },
     "status": {
      "type": "string"
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "CrmQuizAttempt": {
    "type": "object",
    "properties": {
     "attempt_id": {
      "type": "string"
     },
     "user_id": {
      "type": "string"
     },
     "email": {
      "type": "string"
     },
     "student_name": {
      "type": "string",
      "nullable": true
     },
     "course_id": {
      "type": "string"
     },
     "course_title": {
      "type": "string"
     },
     "quiz_name": {
      "type": "string"
     },
     "scope": {
      "type": "string",
      "enum": [
       "module",
       "final"
      ]
     },
     "score_pct": {
      "type": "integer"
     },
     "passed": {
      "type": "boolean"
     },
     "started_at": {
      "type": "string",
      "format": "date-time"
     },
     "submitted_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "CrmCertificate": {
    "type": "object",
    "properties": {
     "certificate_id": {
      "type": "string"
     },
     "user_id": {
      "type": "string"
     },
     "email": {
      "type": "string"
     },
     "student_name": {
      "type": "string",
      "nullable": true
     },
     "course_id": {
      "type": "string"
     },
     "course_title": {
      "type": "string"
     },
     "score_pct": {
      "type": "integer"
     },
     "verify_code": {
      "type": "string"
     },
     "issued_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   },
   "CrmReview": {
    "type": "object",
    "properties": {
     "review_id": {
      "type": "string"
     },
     "user_id": {
      "type": "string"
     },
     "email": {
      "type": "string"
     },
     "student_name": {
      "type": "string",
      "nullable": true
     },
     "course_id": {
      "type": "string"
     },
     "course_title": {
      "type": "string"
     },
     "rating": {
      "type": "integer",
      "minimum": 1,
      "maximum": 5
     },
     "comment": {
      "type": "string",
      "nullable": true
     },
     "status": {
      "type": "string",
      "enum": [
       "approved",
       "hidden"
      ]
     },
     "created_at": {
      "type": "string",
      "format": "date-time"
     },
     "updated_at": {
      "type": "string",
      "format": "date-time"
     }
    }
   }
  }
 }
}
