---
title: "Update questions"
method: PATCH
path: "/v2/questions/"
tags: ["Questions"]
---

# Update questions

`PATCH /v2/questions/`

Update up to BATCH_MAX_ITEMS questions (QUESTION FIELDS ONLY) in one request.

Answers are IMMUTABLE on update, so this edits the question's own fields only.
A question is homed under a quiz XOR a question bank, so the write route accepts
either ``quizzes:write`` OR ``question-banks:write`` (OR logic). The ``data``
array must contain at least one item and at most ``BATCH_MAX_ITEMS`` items. Each
item carries the obfuscated question ``id`` and an ``attributes`` object with the
fields to update, and is processed independently in its own atomic block — a
failure on one row does not abort the rest. Response is 200 if all items succeed,
207 if any fail, 400 if the request envelope is empty or over the cap.

Read-only fields (question_type, quiz_id / question_bank_id, answers, order) are
rejected with a document-level 422 via ``extra="forbid"``. A cross-state FLAG
conflict against the stored ``question_type`` (e.g. case_sensitive on a stored
MULTIPLE_CHOICE) surfaces as a per-item ``validation_error`` (207), not a 422.

Duplicate ids within one batch are NOT de-duplicated: each item is applied in
order, so repeated ids are last-write-wins (the final item for that id wins, and
each occurrence emits its own update event). This matches ``batch_update_students``;
per ``api_v2/docs/batch-responses.md`` within-batch dedup is the endpoint's choice
and update endpoints accept last-write-wins rather than pre-marking duplicates.

See ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention and
per-item error code vocabulary.

**Required OAuth scope:** `question-banks:write` or `quizzes:write`

## Request body

- QuestionBatchUpdateRequestEnvelope — JSON:API envelope for PATCH /v2/questions/ batch updates. The ``data`` field is always an array. Cap enforcement (<=100 items) lives in the endpoint so violations produce HTTP 400 rather than Pydantic 422.
  - `data` QuestionBatchUpdateData[], required — List of question update items.
    - `type` 'questions', required — Must be "questions".
    - `id` string, required — Obfuscated ID of the question to update.
    - `attributes` QuestionUpdateRequest, required — Request schema for partial updates of a question — QUESTION FIELDS ONLY. Answers are IMMUTABLE on update (matching v1, which makes answers read-only on a question update), so no ``answers`` field is exposed. ``question_type``, ``quiz_id`` / ``question_bank_id`` (the parent — a question cannot be moved between a quiz and a bank, mirroring lessons where ``course_id`` is not editable), and ``order`` are likewise read-only and simply absent from the schema; ``extra="forbid"`` rejects any of them with a 422 at the schema layer. PATCH semantics are preserved by Python-level sentinel defaults combined with ``model_fields_set``: an omitted field keeps its default but is absent from ``model_fields_set``, so the endpoint forwards only explicitly-provided fields via ``model_dump(exclude_unset=True)``. Sentinel defaults are stripped from the emitted JSON Schema so SDK generators do not advertise them. Only ``question_html`` carries a non-empty rule (mirrors the create-side rule); the three feedback fields deliberately accept empty string as the clear affordance. The cross-state per-type FLAG re-validation (case_sensitive / requires_manual_grading / feedback rules against the STORED ``question_type``) lives in ``services.update_question``, not here — it depends on the stored type, which the schema cannot see.
      - `question_html` string — New HTML body of the question prompt. Must be non-empty when provided.
      - `correct_answer_feedback_html` string — New HTML feedback shown when the student answers correctly. Empty string clears it. Not allowed on FREEFORM questions (rejected against the stored type).
      - `incorrect_answer_feedback_html` string — New HTML feedback shown when the student answers incorrectly. Empty string clears it. Not allowed on FREEFORM questions (rejected against the stored type).
      - `answer_feedback_html` string — New HTML feedback shown regardless of correctness. Empty string clears it.
      - `case_sensitive` boolean — If true, student responses must match the answer's case. Only valid on FILL_IN_THE_BLANK (rejected against the stored type for other types).
      - `requires_manual_grading` boolean — If true, responses must be graded by a dashboard user. Not allowed on MULTIPLE_CHOICE / MULTIPLE_ANSWER / FILL_IN_THE_BLANK (rejected against the stored type).
      - `is_graded` boolean — Whether the question counts toward the quiz score.
      - `is_optional` boolean — Whether the question may be skipped.

## Response `200`

OK

- BatchResultEnvelopeQuestionResource
  - `data` union[], required — Per-item results in request order.
    - union
      - BatchSucceededItemQuestionResource
        - `status` 'succeeded'
        - `id` string, nullable — Resource ID of the created or updated resource.
        - `result` QuestionResource, required — JSON:API resource object for a question.
          - `type` 'questions' — Always "questions".
          - `id` string, required — Opaque question ID. Use in URL paths.
          - `attributes` QuestionAttributes, required — Attributes of a question resource object. Fields mirror the ``quiz.models.Question`` columns exposed on the public API. ``organization`` and internal primary keys are deliberately not exposed — ``id`` (obfuscated) and ``external_id`` (UUID5) are the only identifiers a client sees. A question is homed under exactly one parent: ``quiz_id`` XOR ``question_bank_id``. Only the parent that is actually set is populated; the other is ``None``. The single-resource GET (``exclude_none=True``) omits the unset id; the list GET does not set ``exclude_none`` (it would strip the null pagination ``links`` keys), so the unset id serializes as ``null`` there.
            - `question_html` string, required — HTML body of the question prompt.
            - `order` integer, required — Ordering value within the parent quiz or question bank.
            - `question_type` string, required — Question type code. One of MULTIPLE_CHOICE, MULTIPLE_ANSWER, FILL_IN_THE_BLANK, FREEFORM, CONTENT_UPLOAD, LINEAR_SCALE.
            - `case_sensitive` boolean, required — If true, student responses must match the answer's case to be correct.
            - `requires_manual_grading` boolean, required — If true, responses to this question must be graded by a dashboard user.
            - `is_graded` boolean, required — Whether the question counts toward the quiz score.
            - `is_optional` boolean, required — Whether the question may be skipped.
            - `correct_answer_feedback_html` string — HTML feedback shown when the student answers correctly.
            - `incorrect_answer_feedback_html` string — HTML feedback shown when the student answers incorrectly.
            - `answer_feedback_html` string — HTML feedback shown regardless of correctness.
            - `quiz_id` string, nullable — Obfuscated ID of the parent quiz, when the question is homed under a quiz.
            - `question_bank_id` string, nullable — Obfuscated ID of the parent question bank, when the question is homed under a bank.
            - `external_id` string, required — Stable UUID5 for cross-system correlation.
            - `created_at` string, date-time, required — Timestamp when the question was created.
            - `modified_at` string, date-time, required — Timestamp when the question was last modified.
            - `answers` AnswerObject[] — The question's answer choices, ordered by ``order`` ascending. Excludes soft-deleted answers.
              - …
          - `links` object, nullable — Self and related links.
      - BatchFailedItem — Wrapper for a failed item in a 207 batch response.
        - `status` 'failed'
        - `id` string, nullable — Echoed resource ID if the input identified a row.
        - `error` BatchItemError, required — Per-item error inside a 207 batch response. Aligned with JSON:API ``ErrorObject`` field naming (``code`` rather than ``reason``) so consumers can reuse error-handling logic across document-level errors (``ErrorObject`` in ``ErrorEnvelope``) and per-item errors (here). Differences from ``ErrorObject``: ``status``/``title`` are omitted because they're redundant for the 207-batch context (HTTP status is on the envelope, and the title is derivable from ``code``).
          - `code` 'duplicate_email' | 'duplicate_in_batch' | 'duplicate_name' | 'validation_error' | 'not_found' | 'internal_error' | 'not_in_domain' | 'already_enrolled' | 'already_published', required — Machine-readable error code.
          - `source` object, nullable — Pointer to the offending input slot, e.g. {"pointer": "/data/0/attributes/email"}.
          - `detail` string, nullable — Human-readable explanation of the error.
  - `summary` BatchSummary, required — Aggregate counts for a 207 batch response. Invariant: ``succeeded + failed == total``. Enforced by ``@model_validator``.
    - `total` integer, required — Total number of items submitted.
    - `succeeded` integer, required — Number of items that succeeded.
    - `failed` integer, required — Number of items that failed.

## Other responses

- `207` — Multi-Status
- `400` — Bad Request
- `401` — Unauthorized
- `403` — Forbidden
- `422` — Unprocessable Entity
- `500` — Internal Server Error

---

[API](https://skmtc.net/skilljar/apis/skilljar-api-v2.md) · [All operations](https://skmtc.net/skilljar/apis/skilljar-api-v2/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/skilljar/skilljar-api-v2/revisions/f8025de20169/schema)
