---
title: "Bind question banks to a quiz"
method: POST
path: "/v2/quizzes/{quiz_id}/question-banks/"
tags: ["Quizzes"]
---

# Bind question banks to a quiz

`POST /v2/quizzes/{quiz_id}/question-banks/`

Bind up to BATCH_MAX_ITEMS question banks to a quiz in a single request.

The ``quiz_id`` path resource is resolved once up front — a malformed /
cross-org / soft-deleted quiz is a document-level 404. Within-batch duplicate
detection is first-wins on ``question_bank_id`` (mirrors ``unbind`` /
``delete_groups``); subsequent duplicates are pre-marked ``duplicate_in_batch``
without reaching the service, so a logical bind fires its
``QuestionBankAssignmentBound`` event exactly once. Each surviving item is
processed independently in its own atomic block: a bad ``question_bank_id``
is a per-item ``not_found``; re-binding an already-bound bank UPDATES the
row (``update_or_create`` on the natural key, no IntegrityError). Response is
200 if all succeed, 207 if any fail, 400 if the envelope is empty or over the
cap.

See ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention.

**Required OAuth scope:** `quizzes:write`

## Path parameters

- `quiz_id` string, required

## Request body

- QuestionBankAssignmentBindRequestEnvelope — JSON:API envelope for POST /v2/quizzes/{quiz_id}/question-banks/. The ``data`` field is always an array — single-bind is a list of one. Cap enforcement (<=100 items) lives in the endpoint so violations produce HTTP 400 via ``ValidationError`` instead of Pydantic's 422.
  - `data` QuestionBankAssignmentBindData[], required — List of bind items.
    - `type` 'question-bank-assignments', required — Must be "question-bank-assignments".
    - `attributes` QuestionBankAssignmentBindRequest, required — Request schema for binding a question bank to a quiz. ``question_bank_id`` is required (the natural key alongside the path ``quiz_id``). ``order`` is optional: when omitted (``None``) on a first bind the service derives ``Max(order) + 10`` within the quiz's assignments (mirrors the question-order derivation). ``randomize_questions`` / ``limit_question_count`` default to the model column defaults on a first bind. Binding is an idempotent partial update: re-binding an already-bound bank writes only the fields the caller actually supplies, so any writable field OMITTED on the re-bind is PRESERVED at its stored value (it is NOT reset to a schema default, and an omitted ``order`` is NOT re-derived). ``extra="forbid"`` rejects unknown attribute keys with a 422 (mirrors ``QuestionBankAssignmentUpdateRequest`` and the other api_v2 write schemas) so a typo'd field is a hard error rather than a silently-dropped no-op.
      - `question_bank_id` string, required — Obfuscated ID of the question bank to bind.
      - `order` integer, nullable — Ordering value within the quiz. Omit to append after the current max order.
      - `randomize_questions` boolean — If true, the bank's questions are presented in a random order.
      - `limit_question_count` integer — Number of questions drawn from this bank. 0 means all questions.

## Response `200`

OK

- BatchResultEnvelopeQuestionBankAssignmentResource
  - `data` union[], required — Per-item results in request order.
    - union
      - BatchSucceededItemQuestionBankAssignmentResource
        - `status` 'succeeded'
        - `id` string, nullable — Resource ID of the created or updated resource.
        - `result` QuestionBankAssignmentResource, required — JSON:API resource object for a quiz↔question-bank assignment.
          - `type` 'question-bank-assignments' — Always "question-bank-assignments".
          - `id` string, required — Opaque assignment ID.
          - `attributes` QuestionBankAssignmentAttributes, required — Attributes of a question-bank-assignment resource object. Fields mirror the writable/observable ``quiz.models.QuestionBankAssignment`` columns. ``question_bank_id`` (obfuscated) identifies the bound bank; the assignment's own obfuscated PK is the resource ``id``. No ``external_id`` — the model is not an ``ExternallyAuditableModel``.
            - `question_bank_id` string, required — Obfuscated ID of the bound question bank.
            - `order` integer, required — Ordering value of this bank within the quiz.
            - `randomize_questions` boolean, required — If true, the bank's questions are presented in a random order.
            - `limit_question_count` integer, required — Number of questions drawn from this bank. 0 means all questions.
            - `created_at` string, date-time, required — Timestamp when the assignment was created.
            - `modified_at` string, date-time, required — Timestamp when the assignment was last modified.
          - `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
- `404` — Not Found
- `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)
