---
title: "Create questions"
method: POST
path: "/v2/questions/"
tags: ["Questions"]
---

# Create questions

`POST /v2/questions/`

Create up to BATCH_MAX_ITEMS questions (each with its answers) in one request.

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 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.

A malformed / missing / cross-org / soft-deleted parent surfaces as a
per-item ``not_found`` (like ``create_lessons``' bad-course path). Per-type,
XOR, and shape validation happen up-front in Pydantic and return a
document-level 422.

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

- QuestionCreateRequestEnvelope — JSON:API envelope for POST /v2/questions/. The ``data`` field is always an array — single-create is a list of one. Cap enforcement (<=100 items) lives in the endpoint, NOT here — that gives HTTP 400 via ``ValidationError`` instead of Pydantic's 422 (matches the quizzes/lessons create envelopes).
  - `data` QuestionCreateData[], required — List of question items to create.
    - `type` 'questions', required — Must be "questions".
    - `attributes` QuestionCreateRequest, required — Request schema for creating a question with its answers. A question is homed under exactly one parent — ``quiz_id`` XOR ``question_bank_id`` (both -> 422, neither -> 422). ``question_type`` is a Literal of the API-enabled subset so CONTENT_UPLOAD / LINEAR_SCALE are rejected with 422. Per-type rules are ported EXACTLY from ``quiz/serializers.py::BaseQuestionSerializer`` (the ``_validate_*`` methods); the dispatch mirrors ``BaseQuestionSerializer.validate``. Neither the question ``order`` nor per-answer ``order`` is accepted — the service auto-assigns question order (``get_order_for_new_question`` logic) and answer order (idx*10 by array position). ``is_graded``, ``is_optional``, and ``answer_feedback_html`` are NOT writable on create — v1's create serializer does not expose them either, so they take their ``Question`` model defaults. ``extra="forbid"`` (the module create-schema convention) turns an attempt to set them into a 422 rather than silently dropping the value.
      - `quiz_id` string, nullable — Obfuscated parent quiz ID. Provide exactly one of quiz_id / question_bank_id.
      - `question_bank_id` string, nullable — Obfuscated parent question bank ID. Provide exactly one of quiz_id / question_bank_id.
      - `question_html` string, required — HTML body of the question prompt. Must be non-empty.
      - `question_type` 'MULTIPLE_CHOICE' | 'MULTIPLE_ANSWER' | 'FILL_IN_THE_BLANK' | 'FREEFORM', required — Question type. One of MULTIPLE_CHOICE, MULTIPLE_ANSWER, FILL_IN_THE_BLANK, FREEFORM.
      - `case_sensitive` boolean — If true, student responses must match the answer's case. Only valid for FILL_IN_THE_BLANK.
      - `requires_manual_grading` boolean — If true, responses must be graded by a dashboard user. Not allowed on MC/MA/FITB.
      - `correct_answer_feedback_html` string — HTML feedback shown when the student answers correctly. Not allowed for FREEFORM.
      - `incorrect_answer_feedback_html` string — HTML feedback shown when the student answers incorrectly. Not allowed for FREEFORM.
      - `answers` AnswerCreateRequest[] — The question's answer choices. Must be empty for FREEFORM; >= 1 for the other types.
        - `answer_text` string, required — The answer choice text.
        - `correct` boolean — Whether this choice is a correct answer.

## 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)
