openapi: 3.1.0
info:
  title: MegaExams REST API
  version: "1.0.0"
  description: >
    Programmatic access to an organization's question bank, exams, results, and
    library on MegaExams. All requests are authenticated with an organization
    scoped API key sent as a Bearer token. Responses are JSON and use a small set
    of envelopes: list endpoints return `{ "data": [...], "meta": {...} }`,
    single resources return `{ "data": {...} }`, and errors return
    `{ "error": { "code": "...", "message": "..." } }`.
  contact:
    name: MegaExams
    url: https://megaexams.com/developers
  license:
    name: Proprietary
servers:
  - url: http://localhost:8000/megaexams/api/v1
    description: Local development
  - url: https://megaexams.com/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Questions
    description: Read and create questions in the organization's bank.
  - name: Taxonomy
    description: The organization's Subject > Chapter > Topic tree.
  - name: Exams
    description: List and inspect exams.
  - name: Results
    description: Published, ranked results for an exam.
  - name: Library
    description: Pre-made papers visible to the organization.

paths:
  /questions:
    get:
      tags: [Questions]
      summary: List questions
      description: >
        Returns the organization's non-deleted questions. Requires the
        `questions:read` scope. Supports filtering and pagination.
      operationId: listQuestions
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: type
          in: query
          description: Filter by question type.
          schema:
            type: string
            enum: [mcq, fill_blank, essay]
        - name: difficulty
          in: query
          description: Filter by difficulty.
          schema:
            type: string
            enum: [easy, medium, hard]
        - name: grade
          in: query
          description: Filter by grade label, for example "Class 9".
          schema:
            type: string
        - name: subject_id
          in: query
          description: Filter by subject UUID.
          schema:
            type: string
            format: uuid
        - name: chapter_id
          in: query
          description: Filter by chapter UUID.
          schema:
            type: string
            format: uuid
        - name: topic_id
          in: query
          description: >
            Filter by topic UUID. Matches the topic and its subtree, mirroring
            the in-app behavior.
          schema:
            type: string
            format: uuid
        - name: tag
          in: query
          description: Filter by a single tag.
          schema:
            type: string
        - name: q
          in: query
          description: Full text search over question text.
          schema:
            type: string
        - name: language
          in: query
          description: Filter by language code, for example "en".
          schema:
            type: string
      responses:
        '200':
          description: A page of questions.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/QuestionSummary'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
              example:
                data:
                  - id: f17c4bc1-3e16-476a-aa48-1b91b0739aae
                    type: mcq
                    difficulty: easy
                    grade: null
                    language: en
                    question_text: What is 2+2?
                    explanation: null
                    points: 1
                    negative_marks: 0
                    curriculum:
                      grade: null
                      subject: Mathematics
                      chapter: Arithmetic
                      topic: Addition
                    tags: [api-test, math]
                    created_at: "2026-06-11T15:11:17Z"
                    updated_at: "2026-06-11T15:11:17Z"
                  - id: b1294fb0-c400-415a-97cb-eac77594519f
                    type: mcq
                    difficulty: easy
                    grade: null
                    language: en
                    question_text: Capital of France?
                    explanation: null
                    points: 1
                    negative_marks: 0.25
                    curriculum:
                      grade: null
                      subject: null
                      chapter: null
                      topic: null
                    tags: []
                    created_at: "2026-06-11T14:45:04Z"
                    updated_at: "2026-06-11T14:45:04Z"
                meta:
                  page: 1
                  per_page: 3
                  total: 8
                  total_pages: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags: [Questions]
      summary: Create a question
      description: >
        Creates a question in the organization's bank. Requires the
        `questions:write` scope. Validation mirrors the in-app question editor.
        If the organization is at its plan's question-bank limit, a 422
        `plan_limit_reached` error is returned.
      operationId: createQuestion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuestionCreate'
            example:
              type: mcq
              question_text: Which planet is known as the Red Planet?
              difficulty: easy
              points: 1
              category_path: "Physics > Laws of Motion > Newton's First Law"
              tags: [astronomy, api-docs]
              explanation: Mars appears red due to iron oxide on its surface.
              options:
                - option_text: Mars
                  is_correct: true
                - option_text: Venus
                  is_correct: false
                - option_text: Jupiter
                  is_correct: false
      responses:
        '201':
          description: The created question.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/QuestionDetail'
              example:
                data:
                  id: 86b9e62e-80ca-421c-b4ff-5a6703d13768
                  type: mcq
                  difficulty: easy
                  grade: null
                  language: en
                  question_text: Which planet is known as the Red Planet?
                  explanation: Mars appears red due to iron oxide on its surface.
                  points: 1
                  negative_marks: 0
                  curriculum:
                    grade: null
                    subject: Physics
                    chapter: Laws of Motion
                    topic: Newton's First Law
                  tags: [api-docs, astronomy]
                  created_at: "2026-06-11T15:17:16Z"
                  updated_at: "2026-06-11T15:17:16Z"
                  options:
                    - id: 5534564f-28bf-40d1-8f67-31fe8035ffe1
                      option_text: Mars
                      is_correct: true
                      sort_order: 0
                    - id: b5bec947-94ad-4ddb-aea0-0c18e885c038
                      option_text: Venus
                      is_correct: false
                      sort_order: 1
                    - id: a609ab8c-c984-4f1b-9d09-bb4ba8fdeb0a
                      option_text: Jupiter
                      is_correct: false
                      sort_order: 2
                  images: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: Validation failed, or the plan question-bank limit was reached.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                validation_failed:
                  value:
                    error:
                      code: validation_failed
                      message: Question text is required.
                plan_limit_reached:
                  value:
                    error:
                      code: plan_limit_reached
                      message: Your plan's question-bank limit has been reached.
        '429':
          $ref: '#/components/responses/RateLimited'

  /questions/{id}:
    get:
      tags: [Questions]
      summary: Get a question
      description: >
        Returns the full question, including type-specific fields: `options` for
        MCQ, `accepted_answers` for fill_blank, and `rubric` / `model_answer` /
        `word_limit` for essay. Requires the `questions:read` scope.
      operationId: getQuestion
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: The question.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/QuestionDetail'
              examples:
                mcq:
                  value:
                    data:
                      id: f17c4bc1-3e16-476a-aa48-1b91b0739aae
                      type: mcq
                      difficulty: easy
                      grade: null
                      language: en
                      question_text: What is 2+2?
                      explanation: null
                      points: 1
                      negative_marks: 0
                      curriculum:
                        grade: null
                        subject: Mathematics
                        chapter: Arithmetic
                        topic: Addition
                      tags: [api-test, math]
                      created_at: "2026-06-11T15:11:17Z"
                      updated_at: "2026-06-11T15:11:17Z"
                      options:
                        - id: a90fe079-61ad-41ac-be7e-03c291499556
                          option_text: "4"
                          is_correct: true
                          sort_order: 0
                        - id: 451e807d-ee6e-4bed-ad7f-240a0509fc90
                          option_text: "5"
                          is_correct: false
                          sort_order: 1
                      images: []
                fill_blank:
                  value:
                    data:
                      id: 9cdb265b-453e-4b79-82bd-0d0b492573e1
                      type: fill_blank
                      difficulty: easy
                      grade: null
                      language: en
                      question_text: The acceleration due to gravity on Earth is approximately ____ m/s^2.
                      explanation: null
                      points: 1
                      negative_marks: 0
                      curriculum:
                        grade: null
                        subject: null
                        chapter: null
                        topic: null
                      tags: [physics]
                      created_at: "2026-06-10T11:19:40Z"
                      updated_at: "2026-06-10T11:19:40Z"
                      accepted_answers: ["9.8", "9.81"]
                      images: []
                essay:
                  value:
                    data:
                      id: 56df807b-d6b5-445a-bca1-ca3201339163
                      type: essay
                      difficulty: medium
                      grade: null
                      language: en
                      question_text: Describe the water cycle. Attach a photo of your diagram if you wish.
                      explanation: null
                      points: 5
                      negative_marks: 0
                      curriculum:
                        grade: null
                        subject: null
                        chapter: null
                        topic: null
                      tags: []
                      created_at: "2026-06-11T14:45:04Z"
                      updated_at: "2026-06-11T14:45:04Z"
                      rubric: null
                      model_answer: null
                      word_limit: null
                      images: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /taxonomy:
    get:
      tags: [Taxonomy]
      summary: Get the taxonomy tree
      description: >
        Returns the full Subject > Chapter > Topic tree for the organization.
        Requires the `questions:read` scope.
      operationId: getTaxonomy
      responses:
        '200':
          description: The taxonomy tree.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Subject'
              example:
                data:
                  - id: e081ee4e-b655-447f-9b5d-0a7e20b5774c
                    name: Mathematics
                    chapters:
                      - id: 24b941c7-76c9-4263-ac52-11e57fe473b5
                        name: Arithmetic
                        topics:
                          - id: a1fac685-bb54-4242-9455-0e5e027266d2
                            name: Addition
                  - id: c7239b87-2113-4f38-b5e3-de6df0023bf6
                    name: Physics
                    chapters:
                      - id: 0fe958b7-e4fa-4cd9-9197-7ed62f51b115
                        name: Laws of Motion
                        topics:
                          - id: 9c4ed712-2dea-4269-beef-28b672894453
                            name: Newton's First Law
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /exams:
    get:
      tags: [Exams]
      summary: List exams
      description: >
        Returns the organization's exams. Requires the `exams:read` scope.
      operationId: listExams
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          description: Filter by exam status.
          schema:
            type: string
            enum: [draft, active, inactive]
      responses:
        '200':
          description: A page of exams.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExamSummary'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
              example:
                data:
                  - id: d4850a00-7ff6-479d-812a-a961b7d5bc3b
                    title: PlayerTest Essay Free
                    description: null
                    slug: ptst02
                    status: active
                    visibility: link
                    question_count: 2
                    time_limit_mins: 30
                    pass_percentage: null
                    negative_marking: true
                    shuffle_scope: section
                    share_url: http://localhost:8001/megaexams/e/ptst02
                    created_at: "2026-06-11T14:45:04Z"
                  - id: 315c41d9-b4f5-4590-80d6-d4c9b0159115
                    title: PlayerTest IBPS Fast
                    description: null
                    slug: ptst01
                    status: active
                    visibility: link
                    question_count: 4
                    time_limit_mins: null
                    pass_percentage: null
                    negative_marking: true
                    shuffle_scope: section
                    share_url: http://localhost:8001/megaexams/e/ptst01
                    created_at: "2026-06-11T14:39:09Z"
                meta:
                  page: 1
                  per_page: 5
                  total: 10
                  total_pages: 2
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /exams/{id}:
    get:
      tags: [Exams]
      summary: Get an exam
      description: >
        Returns the exam with its `sections` and `questions`. Requires the
        `exams:read` scope. Option `is_correct` is only included when the key
        also holds the `questions:read` scope.
      operationId: getExam
      parameters:
        - $ref: '#/components/parameters/IdPath'
      responses:
        '200':
          description: The exam.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/ExamDetail'
              example:
                data:
                  id: 315c41d9-b4f5-4590-80d6-d4c9b0159115
                  title: PlayerTest IBPS Fast
                  description: null
                  slug: ptst01
                  status: active
                  visibility: link
                  question_count: 4
                  time_limit_mins: null
                  pass_percentage: null
                  negative_marking: true
                  shuffle_scope: section
                  share_url: http://localhost:8001/megaexams/e/ptst01
                  created_at: "2026-06-11T14:39:09Z"
                  sections:
                    - id: 05064e13-26a8-416f-8028-dff9c5622f4c
                      name: Reasoning
                      sort_order: 0
                      time_limit_mins: 1
                      instructions: Answer all questions in order. No going back once the section timer ends.
                    - id: 235b526b-7045-412b-ab73-01b7bec8ca1f
                      name: Quant
                      sort_order: 1
                      time_limit_mins: 1
                      instructions: null
                  questions:
                    - id: b0845d82-9e75-4f7a-ba83-70ddc427ff3a
                      type: mcq
                      difficulty: easy
                      question_text: Which number completes the series 2, 4, 8, 16, ?
                      points: 2
                      negative_marks: 0.5
                      language: en
                      section_id: 05064e13-26a8-416f-8028-dff9c5622f4c
                      sort_order: 0
                      points_override: null
                      options:
                        - id: 614dc3d0-fb14-4459-bb34-5e933ff248a6
                          option_text: "24"
                          sort_order: 0
                          is_correct: false
                        - id: 20761d38-3fcd-4c43-93d2-fb368edf3185
                          option_text: "32"
                          sort_order: 1
                          is_correct: true
                    - id: a0c89a41-569a-44f6-9f98-193dc001f77b
                      type: essay
                      difficulty: medium
                      question_text: Explain your method for solving simple interest problems. You may attach a photo of your working.
                      points: 5
                      negative_marks: 0
                      language: en
                      section_id: 235b526b-7045-412b-ab73-01b7bec8ca1f
                      sort_order: 3
                      points_override: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /exams/{id}/results:
    get:
      tags: [Results]
      summary: List exam results
      description: >
        Returns published, ranked results for an exam. Requires the
        `results:read` scope.
      operationId: listExamResults
      parameters:
        - $ref: '#/components/parameters/IdPath'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: A page of results.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Result'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
              example:
                data:
                  - id: 17475d85-ec10-42d1-b2e0-8711d76a21f8
                    student:
                      id: 3109b09a-9c83-422f-bd93-c2772c738396
                      name: Ravi Tester
                      email: ravi.tester.audit@example.com
                      city: Chennai
                      class_grade: "9"
                      institution: Demo Public School
                    marks_obtained: 0
                    total_marks: 10
                    percentage: 0
                    is_passed: null
                    correct: 0
                    incorrect: 0
                    unanswered: 4
                    rank_overall: 1
                    total_time_secs: 121
                    submitted_at: "2026-06-11T14:44:41Z"
                    section_scores:
                      - section_name: Quant
                        marks_obtained: 0
                        total_marks: 7
                      - section_name: Reasoning
                        marks_obtained: 0
                        total_marks: 3
                meta:
                  page: 1
                  per_page: 5
                  total: 1
                  total_pages: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /library:
    get:
      tags: [Library]
      summary: List library papers
      description: >
        Returns pre-made papers visible to the organization (platform papers and
        parent-organization shares). Requires the `exams:read` scope. This list
        is unpaginated; it is returned in the list envelope for consistency, with
        a single page.
      operationId: listLibrary
      responses:
        '200':
          description: The visible library papers.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/LibraryPaper'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
              example:
                data:
                  - id: add7bcee-7d2c-46cc-8aa2-11099a337699
                    title: Class 9 Physics - Laws of Motion (Unit Test)
                    description: A ready-made 20-minute unit test covering force, gravity, and Newton's laws.
                    source: platform
                    question_count: 3
                    total_points: 7
                    time_limit_mins: 20
                    language: en
                meta:
                  page: 1
                  per_page: 1
                  total: 1
                  total_pages: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: mx_live key
      description: >
        Send your API key as a Bearer token:
        `Authorization: Bearer mx_live_<40 hex chars>`. Keys are created in
        Settings > API access (owner only) and shown once. Each key holds a
        subset of the scopes `questions:read`, `questions:write`, `exams:read`,
        and `results:read`.

  parameters:
    Page:
      name: page
      in: query
      description: Page number, starting at 1.
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      description: Items per page.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    IdPath:
      name: id
      in: path
      required: true
      description: Resource UUID.
      schema:
        type: string
        format: uuid

  headers:
    RateLimitLimit:
      description: The request ceiling per minute for this key.
      schema:
        type: integer
        example: 120
    RateLimitRemaining:
      description: Requests remaining in the current 60-second window.
      schema:
        type: integer
        example: 119

  responses:
    BadRequest:
      description: The request body is not a JSON object.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: invalid_request
              message: Request body must be a JSON object.
    Unauthorized:
      description: The API key is missing, malformed, unknown, or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                error:
                  code: invalid_api_key
                  message: Missing or malformed API key.
            invalid:
              value:
                error:
                  code: invalid_api_key
                  message: Invalid API key.
    Forbidden:
      description: The key is missing the required scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: insufficient_scope
              message: "This API key is missing the required scope: questions:write."
    NotFound:
      description: The resource was not found in the key's organization.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: not_found
              message: Question not found.
    RateLimited:
      description: More than 120 requests per minute for this key.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          description: Always 0 when rate limited.
          schema:
            type: integer
            example: 0
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: Rate limit exceeded. Try again shortly.

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: A stable, machine-readable error code.
              enum:
                - invalid_request
                - invalid_api_key
                - insufficient_scope
                - not_found
                - method_not_allowed
                - validation_failed
                - plan_limit_reached
                - rate_limited
                - server_error
            message:
              type: string
              description: A human-readable description of the error.

    PaginationMeta:
      type: object
      required: [page, per_page, total, total_pages]
      properties:
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 25
        total:
          type: integer
          example: 8
        total_pages:
          type: integer
          example: 1

    Curriculum:
      type: object
      properties:
        grade:
          type: [string, "null"]
        subject:
          type: [string, "null"]
        chapter:
          type: [string, "null"]
        topic:
          type: [string, "null"]

    QuestionSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [mcq, fill_blank, essay]
        difficulty:
          type: [string, "null"]
          enum: [easy, medium, hard, null]
        grade:
          type: [string, "null"]
        language:
          type: string
        question_text:
          type: string
        explanation:
          type: [string, "null"]
        points:
          type: number
        negative_marks:
          type: number
        curriculum:
          $ref: '#/components/schemas/Curriculum'
        tags:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    QuestionOption:
      type: object
      properties:
        id:
          type: string
          format: uuid
        option_text:
          type: string
        is_correct:
          type: boolean
        sort_order:
          type: integer

    QuestionDetail:
      allOf:
        - $ref: '#/components/schemas/QuestionSummary'
        - type: object
          properties:
            options:
              description: Present for MCQ questions.
              type: array
              items:
                $ref: '#/components/schemas/QuestionOption'
            accepted_answers:
              description: Present for fill_blank questions.
              type: array
              items:
                type: string
            rubric:
              description: Present for essay questions.
              type: [string, "null"]
            model_answer:
              description: Present for essay questions.
              type: [string, "null"]
            word_limit:
              description: Present for essay questions.
              type: [integer, "null"]
            images:
              type: array
              items:
                type: string

    QuestionCreate:
      type: object
      required: [type, question_text]
      properties:
        type:
          type: string
          enum: [mcq, fill_blank, essay]
        question_text:
          type: string
        difficulty:
          type: string
          enum: [easy, medium, hard]
        points:
          type: number
        negative_marks:
          type: number
        grade:
          type: string
        category_path:
          type: string
          description: 'Resolves a "Subject > Chapter > Topic" path. Created if missing.'
          example: "Mathematics > Arithmetic > Addition"
        tags:
          type: array
          items:
            type: string
        explanation:
          type: string
        options:
          description: Required for type=mcq. At least 2 options, at least 1 correct.
          type: array
          items:
            type: object
            required: [option_text, is_correct]
            properties:
              option_text:
                type: string
              is_correct:
                type: boolean
        accepted_answers:
          description: Required for type=fill_blank. At least 1 accepted answer.
          type: array
          items:
            type: string
        rubric:
          description: Optional for type=essay.
          type: string
        model_answer:
          description: Optional for type=essay.
          type: string
        word_limit:
          description: Optional for type=essay.
          type: integer

    Topic:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string

    Chapter:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        topics:
          type: array
          items:
            $ref: '#/components/schemas/Topic'

    Subject:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        chapters:
          type: array
          items:
            $ref: '#/components/schemas/Chapter'

    ExamSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: [string, "null"]
        slug:
          type: string
        status:
          type: string
          enum: [draft, active, inactive]
        visibility:
          type: string
          enum: [private, link, public]
        question_count:
          type: integer
        time_limit_mins:
          type: [integer, "null"]
        pass_percentage:
          type: [number, "null"]
        negative_marking:
          type: boolean
        shuffle_scope:
          type: string
          enum: [none, exam, section]
        share_url:
          type: string
          format: uri
        created_at:
          type: string
          format: date-time

    ExamSectionRef:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        sort_order:
          type: integer
        time_limit_mins:
          type: [integer, "null"]
        instructions:
          type: [string, "null"]

    ExamQuestionRef:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [mcq, fill_blank, essay]
        difficulty:
          type: [string, "null"]
        question_text:
          type: string
        points:
          type: number
        negative_marks:
          type: number
        language:
          type: string
        section_id:
          type: [string, "null"]
          format: uuid
        sort_order:
          type: integer
        points_override:
          type: [number, "null"]
        options:
          description: >
            Present for MCQ questions. The `is_correct` field on each option is
            only included when the key holds the `questions:read` scope.
          type: array
          items:
            $ref: '#/components/schemas/QuestionOption'

    ExamDetail:
      allOf:
        - $ref: '#/components/schemas/ExamSummary'
        - type: object
          properties:
            sections:
              type: array
              items:
                $ref: '#/components/schemas/ExamSectionRef'
            questions:
              type: array
              items:
                $ref: '#/components/schemas/ExamQuestionRef'

    Student:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: [string, "null"]
        city:
          type: [string, "null"]
        class_grade:
          type: [string, "null"]
        institution:
          type: [string, "null"]

    SectionScore:
      type: object
      properties:
        section_name:
          type: string
        marks_obtained:
          type: number
        total_marks:
          type: number

    Result:
      type: object
      properties:
        id:
          type: string
          format: uuid
        student:
          $ref: '#/components/schemas/Student'
        marks_obtained:
          type: number
        total_marks:
          type: number
        percentage:
          type: number
        is_passed:
          type: [boolean, "null"]
        correct:
          type: integer
        incorrect:
          type: integer
        unanswered:
          type: integer
        rank_overall:
          type: [integer, "null"]
        total_time_secs:
          type: [integer, "null"]
        submitted_at:
          type: string
          format: date-time
        section_scores:
          type: array
          items:
            $ref: '#/components/schemas/SectionScore'

    LibraryPaper:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: [string, "null"]
        source:
          type: string
          description: Where the paper comes from, for example "platform" or a parent share.
        question_count:
          type: integer
        total_points:
          type: number
        time_limit_mins:
          type: [integer, "null"]
        language:
          type: string
