quizzes
39 endpoints.
/api/quiz-banks/{bank_id}/questions/order Bearer tokenReorder a bank's questions
Sets the authored order of the bank's questions — the order fixed quizzes present them in — from a list of every question id, archived ones included, first to last. The list must name the bank's questions exactly, so a bank that changed since it was listed answers 400 and nothing moves; its 1000-id ceiling is the bank's own size cap, so every bank fits. Papers already started keep the order they were dealt in. Requires quizzes:write over the bank's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| question_ids | array of string (uuid) | yes | Every question of the bank — archived ones included, since they keep their slot — in the order to present them; first is position 0. 1–1000 items |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The bank's questions in their new order, archived ones included. | QuizQuestionList |
| 400 | The request body failed validation, or question_ids is not exactly the bank's questions. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this bank but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-banks/{bank_id}/questions Bearer tokenList a bank's questions
The bank's questions in authored order, correct answers included — this is the authoring read, so it requires quizzes:read over the bank's owner. Takers never see this shape; their attempts serve questions with the answers stripped.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| include_archived | query | string | no | Include archived rows. Defaults to false. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The bank's questions, oldest position first. | QuizQuestionList |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quiz-banks/{bank_id}/questions Bearer tokenAdd a question
Adds one question to a bank — after its live questions, ahead of any archived ones — configured per kind: true_false with its correct_boolean; numeric with correct_number and tolerance; the rest with up to 10 options — multiple_choice (exactly one correct), multi_select (at least one, scored all-or-nothing), ordering (items in their correct order), short_answer (the accepted spellings, one or more) and matching (label/match_label pairs); fill_in_blank marks its blanks in the prompt with runs of three or more underscores and lists the accepted answers of each blank as options tagged with blank_index; long_answer takes no answer at all and is always graded by hand, as is drawing, whose taker draws on a canvas — over drawing_starting_image, a data URL, when one is attached. Every question carries points (default 1), may require manual grading — a marker then awards its points from the grading queue — and may carry a markdown rubric for that marker, shown to takers only when rubric_visible_to_takers. The prompt is plaintext by default; prompt_format markdown renders it as Markdown, and pdf attaches a previously uploaded PDF (see the question-pdfs endpoint). A bank holds at most 1000 questions, archived ones included — the most its reorder write can name — and answers 409 once full. Requires quizzes:write over the bank's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| kind | string | yes | true_false: answered true or false. multiple_choice: exactly one correct option. multi_select: several options may be correct, scored all-or-nothing. ordering: the options are items whose authored order is the answer. short_answer: the options are the accepted spellings of a typed answer. numeric: a number within a tolerance of correct_number. matching: each option is a label/match_label pair the taker reunites. fill_in_blank: the prompt's runs of three or more underscores are blanks, and the options are the accepted answers, each naming its blank_index. long_answer: a free-text essay answer with no accepted spellings, always graded by hand. drawing: the taker draws on a canvas, over drawing_starting_image when one is attached, always graded by hand. one of "true_false" | "multiple_choice" | "multi_select" | "ordering" | "short_answer" | "numeric" | "matching" | "fill_in_blank" | "long_answer" | "drawing" |
| prompt | string | yes | On fill_in_blank questions, every run of three or more underscores (___) is one blank, up to 10 per prompt. 1–2000 characters |
| prompt_format | string | no | Defaults to plaintext. one of "plaintext" | "markdown" | "pdf" |
| prompt_pdf | QuestionPromptPdf | no | — |
| prompt_pdf_prefer_inline_display | boolean | no | Omit to draw the PDF beside the question. Send false for a long reference the taker downloads and works from instead, so it does not bury the answers. Remembered across replacements of the file. |
| explanation | string | no | Omit or send empty for no explanation. at most 2000 characters |
| points | integer | no | Defaults to 1. minimum 1 · maximum 1000 |
| requires_manual_grading | boolean | no | Defaults to false, except for long_answer and drawing questions, which are always manual (sending false for one is refused). |
| rubric_markdown | string | no | Omit or send empty for no rubric. at most 50000 characters |
| rubric_visible_to_takers | boolean | no | Defaults to false: the rubric reaches markers only. |
| correct_boolean | boolean | no | Required for true_false questions; omit for every other kind. |
| correct_number | number | no | numeric questions only. |
| tolerance | number | no | numeric questions only; 0 demands the exact value. minimum 0 |
| options | array of object | no | Required for every kind with options; omit for true_false, numeric, long_answer and drawing. 1–50 items |
| drawing_starting_image | string | null | no | drawing questions only: the image the taker draws over, as a base64 data URL. Omit (or send null) for a blank canvas. at most 3000000 characters · matches ^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/]+=*$ |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The created question, options included. | QuizQuestion |
| 400 | The request body failed validation, does not fit the question kind, or names a PDF upload that is missing or not this bank's. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this bank but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 409 | The bank is full, or that PDF upload is already attached to another question. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |
/api/quiz-banks/{bank_id}/question-pdfs Bearer tokenRequest an upload URL for a question PDF
Issues a presigned URL to PUT one PDF straight to private blob storage, scoped to this bank, application/pdf and a 25 MB ceiling. Create (or update) the question with the returned pathname as prompt_pdf.pathname afterwards. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| file_name | string | yes | 1–300 characters |
| size_bytes | integer | yes | maximum 26214400 |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Where to PUT the file, and the pathname the question will point at. | QuestionPdfUploadTicket |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this bank but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 500 | The upload URL could not be signed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |
/api/quiz-questions/{question_id}/pdf Bearer tokenGet a download URL for a question's PDF
Issues a short-lived presigned GET for a pdf question's private blob — the authoring read. Requires quizzes:read over the bank's owner; takers fetch it through their attempt's question pdf endpoint instead.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| question_id | path | string (uuid) | yes | UUID id of the question. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Where to fetch the PDF from, for the next five minutes. | DownloadTicket |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible question has that id, or it has no PDF attached. | ErrorResponse |
| 500 | The download URL could not be signed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |
/api/quiz-banks/{bank_id}/permanent-deletion Bearer tokenPreview permanently deleting a question bank
Counts everything DELETE on this path would remove or alter: attempts still in progress on its questions, answers to them in finished attempts, the quizzes drawing from the bank (and how many draw from nothing else), and its questions with their PDF prompts. Nothing is changed. Requires quizzes:delete over the bank's owner — a grant separate from quizzes:write, which only archives.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | What the deletion would take with it. | DeletionImpact |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this bank but does not hold quizzes:delete. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quiz-banks/{bank_id}/permanent-deletion Bearer tokenPermanently delete a question bank
Hard-deletes the bank — unlike archiving, which only stops it feeding new attempts. Its questions, options and PDF prompts go, and every quiz stops drawing from it. Finished attempts keep their scores, pass/fail results and learning records but lose their answers to its questions; attempts still in progress on them are discarded so their takers start afresh (expired ones are auto-submitted first). Irreversible. Preview the cost with GET first. Requires quizzes:delete over the bank's owner — a grant separate from quizzes:write, which only archives.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | The bank, its questions and their answers are gone. | — |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this bank but does not hold quizzes:delete. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-banks Bearer tokenList question banks
Lists the question banks the caller can read, newest first, with cursor pagination. Requires quizzes:read in scope; rows outside the caller's scope are simply absent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| department | query | string | no | Department UUID id or slug, resolved within org (which is then required). |
| include_archived | query | string | no | Include archived rows. Defaults to false. |
| limit | query | integer | no | Page size, 1-200. Defaults to 50. |
| cursor | query | string | no | Opaque cursor from a previous page's next_cursor. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | One page of question banks, and the cursor for the next. | QuizQuestionBankList |
| 400 | A filter did not resolve, or the cursor is malformed. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quiz-banks Bearer tokenCreate a question bank
Creates a reusable question bank owned by an organization, or by one department within it. Banks are a shared library: any quiz of the organization may draw from them. Requires quizzes:write over the owner.
| Field | Type | Required | Description |
|---|---|---|---|
| owner_organization_id | string (uuid) | yes | Organization the bank belongs to. Always required. |
| owner_department_id | string (uuid) | no | Omit for a bank owned by the organization directly. |
| slug | string | yes | Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like fire-safety-basics. 1–100 characters · matches ^[a-z0-9]+(-[a-z0-9]+)*$ |
| name | string | yes | 1–200 characters |
| description | string | no | Omit or send empty for no description. at most 2000 characters |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The created question bank. | QuizQuestionBank |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller does not hold quizzes:write over the owner. | ErrorResponse |
| 404 | The organization, or the department within it, does not exist. | ErrorResponse |
| 409 | The organization already has a question bank with that slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-banks/{bank_id} Bearer tokenRead a question bank
One question bank. Requires quizzes:read over the owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The question bank. | QuizQuestionBank |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quiz-banks/{bank_id} Bearer tokenUpdate a question bank
Changes a bank's slug, name or description, or archives or restores it with `archived`. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| slug | string | no | 1–100 characters · matches ^[a-z0-9]+(-[a-z0-9]+)*$ |
| name | string | no | 1–200 characters |
| description | string | null | no | Send null to clear the description. at most 2000 characters |
| archived | boolean | no | True archives the bank — it stops feeding new attempts through any quiz link — and false restores it. History is untouched either way. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The updated question bank. | QuizQuestionBank |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this bank but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 409 | The organization already has a question bank with the new slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-banks/{bank_id} Bearer tokenArchive a question bank
Soft-deletes a question bank by setting archived_at. Its questions stop being drawn into new attempts through any quiz link; history is untouched. PATCH with `archived: false` restores it; permanent deletion is DELETE on /quiz-banks/{bank_id}/permanent-deletion. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| bank_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The archived question bank. | QuizQuestionBank |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this bank but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question bank has that id or slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-questions/{question_id}/copy Bearer tokenCopy a question into a bank
Creates a copy of the question — prompt, answers, points, rubric and explanation — in the target bank, which may be the question's own, as a live question after the bank's live ones. A prompt PDF is copied in the store, so each question keeps a file of its own. The original is untouched. Requires quizzes:read over the question's bank and quizzes:write over the target, both in the same organization.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| question_id | path | string (uuid) | yes | UUID id of the question. |
| Field | Type | Required | Description |
|---|---|---|---|
| question_bank_id | string (uuid) | yes | The bank to copy or move the question into — one of the question's own organization. A copy may land in the question's own bank. |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The copy, options included. | QuizQuestion |
| 400 | The request body failed validation, the target bank is in another organization, or (for a move) the question is already in it. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller may see the question or the target bank but not write it. | ErrorResponse |
| 404 | No visible question has that id, or no visible bank has the target id. | ErrorResponse |
| 409 | The target bank is full. | ErrorResponse |
| 500 | The database could not be reached, or the write or the PDF copy failed. | ErrorResponse |
| 503 | The question has a prompt PDF and file storage is not configured here. | ErrorResponse |
/api/quiz-questions/{question_id}/move Bearer tokenMove a question to another bank
Moves the question into the target bank — how a full bank is relieved into a new one. It keeps its id, so attempts that presented it still review and grade against it; quizzes drawing on the old bank stop presenting it, those drawing on the new one start. A live question lands after the target's live questions, an archived one at the end; a prompt PDF moves into the target bank's files. Requires quizzes:write over both banks, which must share an organization.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| question_id | path | string (uuid) | yes | UUID id of the question. |
| Field | Type | Required | Description |
|---|---|---|---|
| question_bank_id | string (uuid) | yes | The bank to copy or move the question into — one of the question's own organization. A copy may land in the question's own bank. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The moved question, options included. | QuizQuestion |
| 400 | The request body failed validation, the target bank is in another organization, or (for a move) the question is already in it. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller may see the question or the target bank but not write it. | ErrorResponse |
| 404 | No visible question has that id, or no visible bank has the target id. | ErrorResponse |
| 409 | The target bank is full. | ErrorResponse |
| 500 | The database could not be reached, or the write or the PDF copy failed. | ErrorResponse |
| 503 | The question has a prompt PDF and file storage is not configured here. | ErrorResponse |
/api/quiz-questions/{question_id} Bearer tokenUpdate a question
Changes a question's prompt, its format, explanation or answers; sending options replaces the whole option set, and sending prompt_pdf attaches or replaces the PDF (switching prompt_format away from pdf discards it). prompt_pdf_prefer_inline_display switches that PDF between drawing beside the question and downloading only, without re-sending the file. drawing_starting_image replaces (null: clears) a drawing question's canvas image. Points, the manual-grading flag and the rubric may change too; points and the flag reach papers built from now on only, since every attempt snapshots them at start. The kind is immutable. Submitted attempts keep the grades they were given — history is never re-graded — but their answer review renders the questions as they are now. Requires quizzes:write over the bank's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| question_id | path | string (uuid) | yes | UUID id of the question. |
| Field | Type | Required | Description |
|---|---|---|---|
| prompt | string | no | On fill_in_blank questions the new prompt's blanks must still be covered by the options — send both when adding or removing a blank. 1–2000 characters |
| prompt_format | string | no | Switching between plaintext and markdown re-renders the same prompt text. Switching to pdf needs prompt_pdf (unless one is already attached); switching away discards the attachment. one of "plaintext" | "markdown" | "pdf" |
| prompt_pdf | any | no | — |
| prompt_pdf_prefer_inline_display | boolean | no | Omit to draw the PDF beside the question. Send false for a long reference the taker downloads and works from instead, so it does not bury the answers. Remembered across replacements of the file. |
| explanation | string | null | no | Send null to clear the explanation. at most 2000 characters |
| points | integer | no | Applies to papers built from now on; attempts already started keep the points they were built with. minimum 1 · maximum 1000 |
| requires_manual_grading | boolean | no | Applies to papers built from now on. Cannot be switched off on a long_answer or drawing question. |
| rubric_markdown | string | null | no | Send null or empty to clear the rubric. at most 50000 characters |
| rubric_visible_to_takers | boolean | no | Whether takers see the rubric too — beneath the prompt while answering, and again in answer review. Off, the rubric reaches markers only. |
| correct_boolean | boolean | no | For true_false questions only. |
| correct_number | number | no | numeric questions only. |
| tolerance | number | no | numeric questions only; 0 demands the exact value. minimum 0 |
| options | array of object | no | For questions with options only. Replaces the whole option set; answer review of attempts that saw the old options degrades accordingly. Grading of submitted attempts never changes. 1–50 items |
| drawing_starting_image | string | null | no | drawing questions only: replaces the starting image; send null to clear it. Papers already started keep the canvas they opened with. at most 3000000 characters · matches ^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/]+=*$ |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The updated question, options included. | QuizQuestion |
| 400 | The request body failed validation, does not fit the question's kind or format, or names a PDF upload that is missing or not this bank's. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this question but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question has that id. | ErrorResponse |
| 409 | That PDF upload is already attached to another question. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |
/api/quiz-questions/{question_id} Bearer tokenDelete a question
Hard-deletes a question nobody has attempted, along with its attached PDF if any. A question presented in any attempt is kept for history and answers 409 — archive it instead. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| question_id | path | string (uuid) | yes | UUID id of the question. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The deleted question, as it was (options omitted). | QuizQuestion |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this question but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question has that id. | ErrorResponse |
| 409 | The question has attempt history; archive it instead. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-questions/{question_id}/archive Bearer tokenArchive a question
Sets archived_at: the question stops being drawn into new papers but stays readable in past attempts. It also moves to the end of the bank's order, so the live questions stay a contiguous run at the front; archiving again changes nothing. Requires quizzes:write over the bank's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| question_id | path | string (uuid) | yes | UUID id of the question. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The archived question, options included. | QuizQuestion |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this question but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible question has that id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id}/attempts Bearer tokenList a quiz's attempts
Every attempt of the quiz, newest first, scores included — the results view for holders of quizzes:read over the quiz's owner. Expired in-progress attempts are finalized (auto-submitted as-is) before listing, so no background job is needed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| limit | query | integer | no | Page size, 1-200. Defaults to 50. |
| cursor | query | string | no | Opaque cursor from a previous page's next_cursor. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | One page of attempts, and the cursor for the next. | QuizAttemptList |
| 400 | The cursor is malformed. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quizzes/{quiz_id}/attempts/export.csv Bearer tokenDownload a quiz's answers as CSV
Every attempt of the quiz, newest first, flattened to one CSV row per presented question with the columns `attempt_id`, `quiz_id`, `quiz_name`, `user_id`, `user_display_name`, `attempt_number`, `status`, `started_at`, `submitted_at`, `auto_submitted`, `score_percentage`, `passed`, `position`, `question_id`, `kind`, `prompt`, `response`, `correct_answer`, `is_correct`, `points_awarded`, `points_possible`, `feedback`, `answered_at` — the attempt's columns repeated on each of its rows, so the file filters by taker, attempt or question in a spreadsheet. Lists in a cell are `; `-separated, alternative accepted spellings `|`-separated, matched pairs read `left → right`, and the file opens with a UTF-8 byte order mark. For holders of quizzes:read over the quiz's owner. Expired in-progress attempts are finalized first, as the results listing does. Pass `user` to export one taker's attempts only. At most 500 attempts travel; when more match, the export is refused outright rather than truncated — filter by taker.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| user | query | string (uuid) | no | Only attempts by this taker (`users.id`). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The CSV file, served as an attachment named `<quiz slug>-answers-<date>.csv`. | — |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 422 | More attempts match than one export may carry; nothing is exported. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quizzes/{quiz_id}/attempts/export.json Bearer tokenDownload a quiz's answers as JSON
Every attempt of the quiz, newest first, each with its graded paper in the review endpoint's shape, as one JSON attachment. For holders of quizzes:read over the quiz's owner. Expired in-progress attempts are finalized first, as the results listing does. Pass `user` to export one taker's attempts only. At most 500 attempts travel; when more match, the export is refused outright rather than truncated — filter by taker.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| user | query | string (uuid) | no | Only attempts by this taker (`users.id`). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The JSON file, served as an attachment named `<quiz slug>-answers-<date>.json`. | QuizAnswersExport |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 422 | More attempts match than one export may carry; nothing is exported. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quizzes/{quiz_id}/schedules Bearer tokenSchedule a sitting
Adds one scheduled sitting of the quiz — live quizzes are takeable only inside an open sitting, and max_attempts counts per sitting, so recurring runs (say, yearly recertification) are just more sittings. Sittings of one quiz must not overlap. Sittings of async quizzes are accepted but ignored. The audience says who may sit it: every assignee, or only the roster kept at /quiz-schedules/{schedule_id}/participants. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | no | Omit or send empty for an unlabelled sitting. 1–200 characters |
| opens_at | string (date-time) | yes | — |
| closes_at | string (date-time) | yes | — |
| audience | string | no | Defaults to assignees. A roster sitting starts with an empty roster. one of "assignees" | "roster" |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The scheduled sitting. | ScheduledQuiz |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 409 | The sitting would overlap another sitting of the quiz. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-schedules/{schedule_id} Bearer tokenUpdate a sitting
Changes a sitting's label, window or audience. Attempts already sat in it keep their recorded deadlines and grades; moving the close changes when their held-back results release. Requires quizzes:write over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| schedule_id | path | string (uuid) | yes | UUID id of the scheduled sitting. |
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | null | no | Send null to clear the label. 1–200 characters |
| opens_at | string (date-time) | no | — |
| closes_at | string (date-time) | no | — |
| audience | string | no | Switching to assignees keeps the roster for a later switch back; switching to roster admits only whoever is on it. one of "assignees" | "roster" |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The updated sitting. | ScheduledQuiz |
| 400 | The request body failed validation, or the merged window is inverted. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this sitting but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible sitting has that id. | ErrorResponse |
| 409 | The sitting would overlap another sitting of the quiz. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-schedules/{schedule_id} Bearer tokenDelete a sitting
Removes a sitting nobody has sat. A sitting with attempts is history and answers 409 — move its window instead. Requires quizzes:write over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| schedule_id | path | string (uuid) | yes | UUID id of the scheduled sitting. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The deleted sitting, as it was. | ScheduledQuiz |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this sitting but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible sitting has that id. | ErrorResponse |
| 409 | The sitting has attempts; it is history and cannot be deleted. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-schedules/{schedule_id}/participants Bearer tokenList a sitting's roster
The trainees on the sitting's roster, oldest entry first. Decides who may sit a roster sitting; ignored while the sitting's audience is assignees. Requires quizzes:read over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| schedule_id | path | string (uuid) | yes | UUID id of the scheduled sitting. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The roster. | ScheduledQuizParticipantList |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible sitting has that id. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quiz-schedules/{schedule_id}/participants Bearer tokenAdd a trainee to a sitting's roster
Puts one assignee of the quiz on the sitting's roster. The member must be assigned the quiz already — individually, or through a role they hold — since a roster only narrows who may sit this sitting. Requires quizzes:write over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| schedule_id | path | string (uuid) | yes | UUID id of the scheduled sitting. |
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | string (uuid) | yes | A member of the quiz's organization who is assigned the quiz, individually or through a role they hold. |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The roster entry. | ScheduledQuizParticipant |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this sitting but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible sitting has that id, or the member is not in the quiz's organization. | ErrorResponse |
| 409 | The member is not assigned the quiz, or is already on the roster. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-schedule-participants/{participant_id} Bearer tokenRemove a trainee from a sitting's roster
Takes one trainee off the sitting's roster. Attempts they already sat in it are history and stay. Requires quizzes:write over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| participant_id | path | string (uuid) | yes | UUID id of the roster entry. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The removed entry, as it was (display fields omitted). | ScheduledQuizParticipant |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this sitting but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible roster entry has that id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id}/assignments Bearer tokenList a quiz's assignments
Who may see and take the quiz: assigned department roles (every holder) and individually assigned members. Requires quizzes:read over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The quiz's assignments, oldest first. | QuizAssignmentList |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quizzes/{quiz_id}/assignments Bearer tokenAssign a quiz
Assigns the quiz to a department role (every holder can take it) or to one directory member — exactly one of role_id / user_id. Both must belong to the quiz's own organization. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| role_id | string (uuid) | no | Assign to every holder of this department role. |
| user_id | string (uuid) | no | Assign to one member of the quiz's organization, by user id. |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The created assignment. | QuizAssignment |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | The quiz, role, or member does not exist in the quiz's organization. | ErrorResponse |
| 409 | The quiz is already assigned to that role or member. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quiz-assignments/{assignment_id} Bearer tokenRemove an assignment
Unassigns a role or member from the quiz. Attempts already made are history and stay. Requires quizzes:write over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| assignment_id | path | string (uuid) | yes | UUID id of the assignment. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The removed assignment, as it was (display names omitted). | QuizAssignment |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible assignment has that id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id}/banks Bearer tokenSet a quiz's bank links
Replaces the quiz's bank links; array order is presentation order, and draw_count bounds what a random_draw quiz pulls from each bank (omit for all). Banks must belong to the quiz's own organization. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| links | array of object | yes | Replaces the quiz's bank links; array order is presentation order. at most 50 items |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The quiz with its new bank links. | QuizDetail |
| 400 | The body failed validation, or a bank is not in the quiz's organization. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id}/subjects Bearer tokenSet a quiz's subject matters
Replaces the quiz's subject-matter tags — the subjects each taker's learning record is filed under. Subjects must belong to the quiz's own organization. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| subject_matter_ids | array of string (uuid) | yes | Replaces the quiz's subject-matter tags. at most 50 items |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The quiz with its new subject tags. | QuizDetail |
| 400 | The body failed validation, or a subject is not in the quiz's organization. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id}/publish Bearer tokenPublish a quiz
Draft → published: assignees can now see the quiz, and sit it inside one of its scheduled sittings (live) or at any time (async). Refused while the linked banks hold no live questions, or while a live quiz has no sitting scheduled. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The published quiz. | Quiz |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 409 | The quiz would present no questions, or a live quiz has no sitting. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id}/release-results Bearer tokenRelease held-back results
Stamps results_released_at, making every taker's results visible — the manual release for quizzes that neither show results immediately nor have a closing window. Every scored, unreleased attempt is released per attempt at the same moment, manually graded ones included (their learning-record evidence is written now); attempts finalized later need a further release. Expired in-progress attempts are finalized first. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The quiz, with results released. | Quiz |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id} Bearer tokenRead a quiz
One quiz with its bank links, subject tags and scheduled sittings — the admin view. Requires quizzes:read over the owner. Takers read their assigned view at /quizzes/assigned.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The quiz, bank links and subjects included. | QuizDetail |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quizzes/{quiz_id} Bearer tokenUpdate a quiz
Changes a quiz's configuration. Published quizzes stay editable — attempts are graded at submit time, so history never re-grades — but selection and limit changes apply to future attempts immediately. Sittings are managed at /quizzes/{quiz_id}/schedules. Requires quizzes:write over the owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| slug | string | no | 1–100 characters · matches ^[a-z0-9]+(-[a-z0-9]+)*$ |
| name | string | no | 1–200 characters |
| description | string | null | no | Send null to clear the description. at most 2000 characters |
| footer_markdown | string | null | no | Send null or empty to clear the closing note. at most 50000 characters |
| delivery_kind | string | no | live: takeable only while one of the quiz's scheduled sittings (scheduled_quizzes) is open — the same quiz can be scheduled year after year. async: self-scheduled, takeable whenever the quiz is published; sittings do not apply. one of "live" | "async" |
| time_limit_seconds | integer | null | no | Send null to remove the time limit. |
| selection_mode | string | no | fixed: every question in authored order, the same for everyone. shuffle: every question, order and options shuffled per attempt. random_draw: each bank link contributes draw_count randomly chosen questions (all when null), shuffled. one of "fixed" | "shuffle" | "random_draw" |
| pass_percentage | number | no | Score required to pass, as a percentage of the questions presented. minimum 0 · maximum 100 |
| max_attempts | integer | null | no | Send null for unlimited attempts. |
| show_results_immediately | boolean | no | — |
| show_correct_answers | boolean | no | — |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The updated quiz, bank links and subjects included. | QuizDetail |
| 400 | The request body failed validation, or the window rules were broken. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 409 | The organization already has a quiz with the new slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id} Bearer tokenArchive a quiz
Soft-deletes a quiz by setting archived_at: it disappears from assignees and cannot be sat; attempts and their learning records stay readable. Requires quizzes:write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The archived quiz. | Quiz |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes/{quiz_id}/duplicate Bearer tokenDuplicate a quiz
Creates a new draft quiz with the original's configuration, bank links (with their draw counts) and subject tags. Assignments, sittings, attempts and any results release stay with the original; the banks are shared, not copied. The copy is named after the original with " (copy)" appended and takes the first free -copy slug unless name and slug are given. Requires quizzes:write over the quiz's owner.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| quiz_id | path | string | yes | UUID id, or a slug resolved within the organization given by the org query parameter. |
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | no | The copy's name; omit for the original's with " (copy)" appended. 1–200 characters |
| slug | string | no | The copy's slug, unique within the organization; omit for the first free `-copy`, `-copy-2`… of the original's. Slugs are lowercase letters and digits in words separated by single hyphens, like fire-safety-basics. 1–100 characters · matches ^[a-z0-9]+(-[a-z0-9]+)*$ |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The copy, in draft, with its bank links and subjects. | QuizDetail |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this quiz but does not hold quizzes:write. | ErrorResponse |
| 404 | No visible quiz has that id or slug. | ErrorResponse |
| 409 | The organization already has a quiz with the requested slug, or another copy took the generated one first — retry. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/quizzes Bearer tokenList quizzes
Lists the quizzes the caller can administer or read, newest first, with cursor pagination. Requires quizzes:read in scope; rows outside the caller's scope are simply absent. Takers list what is assigned to them at /quizzes/assigned instead.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| department | query | string | no | Department UUID id or slug, resolved within org (which is then required). |
| include_archived | query | string | no | Include archived rows. Defaults to false. |
| limit | query | integer | no | Page size, 1-200. Defaults to 50. |
| cursor | query | string | no | Opaque cursor from a previous page's next_cursor. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | One page of quizzes, and the cursor for the next. | QuizList |
| 400 | A filter did not resolve, or the cursor is malformed. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/quizzes Bearer tokenCreate a quiz
Creates a quiz in draft, owned by an organization or one department within it. Question banks of the owning organization can be linked from the start via bank_links (or later via PUT /quizzes/{quiz_id}/banks); tag subjects and assign takers next, then publish. live quizzes are sat inside scheduled sittings (POST /quizzes/{quiz_id}/schedules) — the same quiz can be scheduled again and again; async ones are takeable whenever published. Requires quizzes:write over the owner.
| Field | Type | Required | Description |
|---|---|---|---|
| owner_organization_id | string (uuid) | yes | Organization the quiz belongs to. Always required. |
| owner_department_id | string (uuid) | no | Omit for a quiz owned by the organization directly. |
| slug | string | yes | Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like fire-safety-basics. 1–100 characters · matches ^[a-z0-9]+(-[a-z0-9]+)*$ |
| name | string | yes | 1–200 characters |
| description | string | no | Omit or send empty for no description. at most 2000 characters |
| footer_markdown | string | no | Omit or send empty for no closing note. at most 50000 characters |
| delivery_kind | string | yes | live: takeable only while one of the quiz's scheduled sittings (scheduled_quizzes) is open — the same quiz can be scheduled year after year. async: self-scheduled, takeable whenever the quiz is published; sittings do not apply. one of "live" | "async" |
| time_limit_seconds | integer | no | Per-attempt time limit in seconds; a live sitting's close still caps it. |
| selection_mode | string | yes | fixed: every question in authored order, the same for everyone. shuffle: every question, order and options shuffled per attempt. random_draw: each bank link contributes draw_count randomly chosen questions (all when null), shuffled. one of "fixed" | "shuffle" | "random_draw" |
| pass_percentage | number | yes | Score required to pass, as a percentage of the questions presented. minimum 0 · maximum 100 |
| max_attempts | integer | no | Omit for unlimited attempts. |
| show_results_immediately | boolean | no | Defaults to true. |
| show_correct_answers | boolean | no | Defaults to false. |
| bank_links | array of object | no | Question banks to link from the start, in presentation order; each must belong to the owning organization. Omit to link banks later via PUT /quizzes/{quiz_id}/banks. at most 50 items |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The created quiz, in draft, with any bank links it was created with. | QuizDetail |
| 400 | The request body failed validation, or a requested bank is not in the owning organization. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller does not hold quizzes:write over the owner. | ErrorResponse |
| 404 | The organization, or the department within it, does not exist. | ErrorResponse |
| 409 | The organization already has a quiz with that slug. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |