quizzes

39 endpoints.

PUT/api/quiz-banks/{bank_id}/questions/order Bearer token

Reorder 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

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredReorderBankQuestionsRequest
FieldTypeRequiredDescription
question_idsarray 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

StatusDescriptionBody
200The bank's questions in their new order, archived ones included.QuizQuestionList
400The request body failed validation, or question_ids is not exactly the bank's questions.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this bank but does not hold quizzes:write.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quiz-banks/{bank_id}/questions Bearer token

List 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

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
include_archivedquerystringnoInclude archived rows. Defaults to false.

Responses

StatusDescriptionBody
200The bank's questions, oldest position first.QuizQuestionList
401The access token is missing or invalid.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/quiz-banks/{bank_id}/questions Bearer token

Add 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

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredCreateQuizQuestionRequest
FieldTypeRequiredDescription
kindstringyes

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"

promptstringyes

On fill_in_blank questions, every run of three or more underscores (___) is one blank, up to 10 per prompt.

1–2000 characters

prompt_formatstringno

Defaults to plaintext.

one of "plaintext" | "markdown" | "pdf"

prompt_pdfQuestionPromptPdfno—
prompt_pdf_prefer_inline_displaybooleanno

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.

explanationstringno

Omit or send empty for no explanation.

at most 2000 characters

pointsintegerno

Defaults to 1.

minimum 1 · maximum 1000

requires_manual_gradingbooleanno

Defaults to false, except for long_answer and drawing questions, which are always manual (sending false for one is refused).

rubric_markdownstringno

Omit or send empty for no rubric.

at most 50000 characters

rubric_visible_to_takersbooleanno

Defaults to false: the rubric reaches markers only.

correct_booleanbooleanno

Required for true_false questions; omit for every other kind.

correct_numbernumberno

numeric questions only.

tolerancenumberno

numeric questions only; 0 demands the exact value.

minimum 0

optionsarray of objectno

Required for every kind with options; omit for true_false, numeric, long_answer and drawing.

1–50 items

drawing_starting_imagestring | nullno

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

StatusDescriptionBody
201The created question, options included.QuizQuestion
400The request body failed validation, does not fit the question kind, or names a PDF upload that is missing or not this bank's.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this bank but does not hold quizzes:write.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
409The bank is full, or that PDF upload is already attached to another question.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse
POST/api/quiz-banks/{bank_id}/question-pdfs Bearer token

Request 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

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredCreateQuestionPdfUploadRequest
FieldTypeRequiredDescription
file_namestringyes

1–300 characters

size_bytesintegeryes

maximum 26214400

Responses

StatusDescriptionBody
201Where to PUT the file, and the pathname the question will point at.QuestionPdfUploadTicket
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this bank but does not hold quizzes:write.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
500The upload URL could not be signed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse
GET/api/quiz-questions/{question_id}/pdf Bearer token

Get 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

NameInTypeRequiredDescription
question_idpathstring (uuid)yesUUID id of the question.

Responses

StatusDescriptionBody
200Where to fetch the PDF from, for the next five minutes.DownloadTicket
401The access token is missing or invalid.ErrorResponse
404No visible question has that id, or it has no PDF attached.ErrorResponse
500The download URL could not be signed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse
GET/api/quiz-banks/{bank_id}/permanent-deletion Bearer token

Preview 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

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200What the deletion would take with it.DeletionImpact
401The access token is missing or invalid.ErrorResponse
403The caller can read this bank but does not hold quizzes:delete.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
500The database could not be read.ErrorResponse
DELETE/api/quiz-banks/{bank_id}/permanent-deletion Bearer token

Permanently 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

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
204The bank, its questions and their answers are gone.—
401The access token is missing or invalid.ErrorResponse
403The caller can read this bank but does not hold quizzes:delete.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quiz-banks Bearer token

List 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

NameInTypeRequiredDescription
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
departmentquerystringnoDepartment UUID id or slug, resolved within org (which is then required).
include_archivedquerystringnoInclude archived rows. Defaults to false.
limitqueryintegernoPage size, 1-200. Defaults to 50.
cursorquerystringnoOpaque cursor from a previous page's next_cursor.

Responses

StatusDescriptionBody
200One page of question banks, and the cursor for the next.QuizQuestionBankList
400A filter did not resolve, or the cursor is malformed.ErrorResponse
401The access token is missing or invalid.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/quiz-banks Bearer token

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

Request body

application/jsonrequiredCreateQuizQuestionBankRequest
FieldTypeRequiredDescription
owner_organization_idstring (uuid)yes

Organization the bank belongs to. Always required.

owner_department_idstring (uuid)no

Omit for a bank owned by the organization directly.

slugstringyes

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]+)*$

namestringyes

1–200 characters

descriptionstringno

Omit or send empty for no description.

at most 2000 characters

Responses

StatusDescriptionBody
201The created question bank.QuizQuestionBank
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller does not hold quizzes:write over the owner.ErrorResponse
404The organization, or the department within it, does not exist.ErrorResponse
409The organization already has a question bank with that slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quiz-banks/{bank_id} Bearer token

Read a question bank

One question bank. Requires quizzes:read over the owner.

Parameters

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200The question bank.QuizQuestionBank
401The access token is missing or invalid.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
500The database could not be read.ErrorResponse
PATCH/api/quiz-banks/{bank_id} Bearer token

Update a question bank

Changes a bank's slug, name or description, or archives or restores it with `archived`. Requires quizzes:write.

Parameters

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredUpdateQuizQuestionBankRequest
FieldTypeRequiredDescription
slugstringno

1–100 characters · matches ^[a-z0-9]+(-[a-z0-9]+)*$

namestringno

1–200 characters

descriptionstring | nullno

Send null to clear the description.

at most 2000 characters

archivedbooleanno

True archives the bank — it stops feeding new attempts through any quiz link — and false restores it. History is untouched either way.

Responses

StatusDescriptionBody
200The updated question bank.QuizQuestionBank
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this bank but does not hold quizzes:write.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
409The organization already has a question bank with the new slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
DELETE/api/quiz-banks/{bank_id} Bearer token

Archive 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

NameInTypeRequiredDescription
bank_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200The archived question bank.QuizQuestionBank
401The access token is missing or invalid.ErrorResponse
403The caller can read this bank but does not hold quizzes:write.ErrorResponse
404No visible question bank has that id or slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
POST/api/quiz-questions/{question_id}/copy Bearer token

Copy 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

NameInTypeRequiredDescription
question_idpathstring (uuid)yesUUID id of the question.

Request body

application/jsonrequiredQuestionTargetBankRequest
FieldTypeRequiredDescription
question_bank_idstring (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

StatusDescriptionBody
201The copy, options included.QuizQuestion
400The request body failed validation, the target bank is in another organization, or (for a move) the question is already in it.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller may see the question or the target bank but not write it.ErrorResponse
404No visible question has that id, or no visible bank has the target id.ErrorResponse
409The target bank is full.ErrorResponse
500The database could not be reached, or the write or the PDF copy failed.ErrorResponse
503The question has a prompt PDF and file storage is not configured here.ErrorResponse
POST/api/quiz-questions/{question_id}/move Bearer token

Move 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

NameInTypeRequiredDescription
question_idpathstring (uuid)yesUUID id of the question.

Request body

application/jsonrequiredQuestionTargetBankRequest
FieldTypeRequiredDescription
question_bank_idstring (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

StatusDescriptionBody
200The moved question, options included.QuizQuestion
400The request body failed validation, the target bank is in another organization, or (for a move) the question is already in it.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller may see the question or the target bank but not write it.ErrorResponse
404No visible question has that id, or no visible bank has the target id.ErrorResponse
409The target bank is full.ErrorResponse
500The database could not be reached, or the write or the PDF copy failed.ErrorResponse
503The question has a prompt PDF and file storage is not configured here.ErrorResponse
PATCH/api/quiz-questions/{question_id} Bearer token

Update 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

NameInTypeRequiredDescription
question_idpathstring (uuid)yesUUID id of the question.

Request body

application/jsonrequiredUpdateQuizQuestionRequest
FieldTypeRequiredDescription
promptstringno

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_formatstringno

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_pdfanyno—
prompt_pdf_prefer_inline_displaybooleanno

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.

explanationstring | nullno

Send null to clear the explanation.

at most 2000 characters

pointsintegerno

Applies to papers built from now on; attempts already started keep the points they were built with.

minimum 1 · maximum 1000

requires_manual_gradingbooleanno

Applies to papers built from now on. Cannot be switched off on a long_answer or drawing question.

rubric_markdownstring | nullno

Send null or empty to clear the rubric.

at most 50000 characters

rubric_visible_to_takersbooleanno

Whether takers see the rubric too — beneath the prompt while answering, and again in answer review. Off, the rubric reaches markers only.

correct_booleanbooleanno

For true_false questions only.

correct_numbernumberno

numeric questions only.

tolerancenumberno

numeric questions only; 0 demands the exact value.

minimum 0

optionsarray of objectno

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_imagestring | nullno

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

StatusDescriptionBody
200The updated question, options included.QuizQuestion
400The 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
401The access token is missing or invalid.ErrorResponse
403The caller can read this question but does not hold quizzes:write.ErrorResponse
404No visible question has that id.ErrorResponse
409That PDF upload is already attached to another question.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse
DELETE/api/quiz-questions/{question_id} Bearer token

Delete 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

NameInTypeRequiredDescription
question_idpathstring (uuid)yesUUID id of the question.

Responses

StatusDescriptionBody
200The deleted question, as it was (options omitted).QuizQuestion
401The access token is missing or invalid.ErrorResponse
403The caller can read this question but does not hold quizzes:write.ErrorResponse
404No visible question has that id.ErrorResponse
409The question has attempt history; archive it instead.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
POST/api/quiz-questions/{question_id}/archive Bearer token

Archive 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

NameInTypeRequiredDescription
question_idpathstring (uuid)yesUUID id of the question.

Responses

StatusDescriptionBody
200The archived question, options included.QuizQuestion
401The access token is missing or invalid.ErrorResponse
403The caller can read this question but does not hold quizzes:write.ErrorResponse
404No visible question has that id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quizzes/{quiz_id}/attempts Bearer token

List 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
limitqueryintegernoPage size, 1-200. Defaults to 50.
cursorquerystringnoOpaque cursor from a previous page's next_cursor.

Responses

StatusDescriptionBody
200One page of attempts, and the cursor for the next.QuizAttemptList
400The cursor is malformed.ErrorResponse
401The access token is missing or invalid.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
500The database could not be read.ErrorResponse
GET/api/quizzes/{quiz_id}/attempts/export.csv Bearer token

Download 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
userquerystring (uuid)noOnly attempts by this taker (`users.id`).

Responses

StatusDescriptionBody
200The CSV file, served as an attachment named `<quiz slug>-answers-<date>.csv`.—
401The access token is missing or invalid.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
422More attempts match than one export may carry; nothing is exported.ErrorResponse
500The database could not be read.ErrorResponse
GET/api/quizzes/{quiz_id}/attempts/export.json Bearer token

Download 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
userquerystring (uuid)noOnly attempts by this taker (`users.id`).

Responses

StatusDescriptionBody
200The JSON file, served as an attachment named `<quiz slug>-answers-<date>.json`.QuizAnswersExport
401The access token is missing or invalid.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
422More attempts match than one export may carry; nothing is exported.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/quizzes/{quiz_id}/schedules Bearer token

Schedule 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredCreateScheduledQuizRequest
FieldTypeRequiredDescription
labelstringno

Omit or send empty for an unlabelled sitting.

1–200 characters

opens_atstring (date-time)yes—
closes_atstring (date-time)yes—
audiencestringno

Defaults to assignees. A roster sitting starts with an empty roster.

one of "assignees" | "roster"

Responses

StatusDescriptionBody
201The scheduled sitting.ScheduledQuiz
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
409The sitting would overlap another sitting of the quiz.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
PATCH/api/quiz-schedules/{schedule_id} Bearer token

Update 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

NameInTypeRequiredDescription
schedule_idpathstring (uuid)yesUUID id of the scheduled sitting.

Request body

application/jsonrequiredUpdateScheduledQuizRequest
FieldTypeRequiredDescription
labelstring | nullno

Send null to clear the label.

1–200 characters

opens_atstring (date-time)no—
closes_atstring (date-time)no—
audiencestringno

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

StatusDescriptionBody
200The updated sitting.ScheduledQuiz
400The request body failed validation, or the merged window is inverted.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this sitting but does not hold quizzes:write.ErrorResponse
404No visible sitting has that id.ErrorResponse
409The sitting would overlap another sitting of the quiz.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
DELETE/api/quiz-schedules/{schedule_id} Bearer token

Delete 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

NameInTypeRequiredDescription
schedule_idpathstring (uuid)yesUUID id of the scheduled sitting.

Responses

StatusDescriptionBody
200The deleted sitting, as it was.ScheduledQuiz
401The access token is missing or invalid.ErrorResponse
403The caller can read this sitting but does not hold quizzes:write.ErrorResponse
404No visible sitting has that id.ErrorResponse
409The sitting has attempts; it is history and cannot be deleted.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quiz-schedules/{schedule_id}/participants Bearer token

List 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

NameInTypeRequiredDescription
schedule_idpathstring (uuid)yesUUID id of the scheduled sitting.

Responses

StatusDescriptionBody
200The roster.ScheduledQuizParticipantList
401The access token is missing or invalid.ErrorResponse
404No visible sitting has that id.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/quiz-schedules/{schedule_id}/participants Bearer token

Add 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

NameInTypeRequiredDescription
schedule_idpathstring (uuid)yesUUID id of the scheduled sitting.

Request body

application/jsonrequiredAddScheduledQuizParticipantRequest
FieldTypeRequiredDescription
user_idstring (uuid)yes

A member of the quiz's organization who is assigned the quiz, individually or through a role they hold.

Responses

StatusDescriptionBody
201The roster entry.ScheduledQuizParticipant
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this sitting but does not hold quizzes:write.ErrorResponse
404No visible sitting has that id, or the member is not in the quiz's organization.ErrorResponse
409The member is not assigned the quiz, or is already on the roster.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
DELETE/api/quiz-schedule-participants/{participant_id} Bearer token

Remove 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

NameInTypeRequiredDescription
participant_idpathstring (uuid)yesUUID id of the roster entry.

Responses

StatusDescriptionBody
200The removed entry, as it was (display fields omitted).ScheduledQuizParticipant
401The access token is missing or invalid.ErrorResponse
403The caller can read this sitting but does not hold quizzes:write.ErrorResponse
404No visible roster entry has that id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quizzes/{quiz_id}/assignments Bearer token

List 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200The quiz's assignments, oldest first.QuizAssignmentList
401The access token is missing or invalid.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/quizzes/{quiz_id}/assignments Bearer token

Assign 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredCreateQuizAssignmentRequest
FieldTypeRequiredDescription
role_idstring (uuid)no

Assign to every holder of this department role.

user_idstring (uuid)no

Assign to one member of the quiz's organization, by user id.

Responses

StatusDescriptionBody
201The created assignment.QuizAssignment
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404The quiz, role, or member does not exist in the quiz's organization.ErrorResponse
409The quiz is already assigned to that role or member.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
DELETE/api/quiz-assignments/{assignment_id} Bearer token

Remove 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

NameInTypeRequiredDescription
assignment_idpathstring (uuid)yesUUID id of the assignment.

Responses

StatusDescriptionBody
200The removed assignment, as it was (display names omitted).QuizAssignment
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible assignment has that id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
PUT/api/quizzes/{quiz_id}/banks Bearer token

Set 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredSetQuizBankLinksRequest
FieldTypeRequiredDescription
linksarray of objectyes

Replaces the quiz's bank links; array order is presentation order.

at most 50 items

Responses

StatusDescriptionBody
200The quiz with its new bank links.QuizDetail
400The body failed validation, or a bank is not in the quiz's organization.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
PUT/api/quizzes/{quiz_id}/subjects Bearer token

Set 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredSetQuizSubjectsRequest
FieldTypeRequiredDescription
subject_matter_idsarray of string (uuid)yes

Replaces the quiz's subject-matter tags.

at most 50 items

Responses

StatusDescriptionBody
200The quiz with its new subject tags.QuizDetail
400The body failed validation, or a subject is not in the quiz's organization.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
POST/api/quizzes/{quiz_id}/publish Bearer token

Publish 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200The published quiz.Quiz
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
409The quiz would present no questions, or a live quiz has no sitting.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
POST/api/quizzes/{quiz_id}/release-results Bearer token

Release 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200The quiz, with results released.Quiz
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quizzes/{quiz_id} Bearer token

Read 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200The quiz, bank links and subjects included.QuizDetail
401The access token is missing or invalid.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
500The database could not be read.ErrorResponse
PATCH/api/quizzes/{quiz_id} Bearer token

Update 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonrequiredUpdateQuizRequest
FieldTypeRequiredDescription
slugstringno

1–100 characters · matches ^[a-z0-9]+(-[a-z0-9]+)*$

namestringno

1–200 characters

descriptionstring | nullno

Send null to clear the description.

at most 2000 characters

footer_markdownstring | nullno

Send null or empty to clear the closing note.

at most 50000 characters

delivery_kindstringno

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_secondsinteger | nullno

Send null to remove the time limit.

selection_modestringno

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_percentagenumberno

Score required to pass, as a percentage of the questions presented.

minimum 0 · maximum 100

max_attemptsinteger | nullno

Send null for unlimited attempts.

show_results_immediatelybooleanno—
show_correct_answersbooleanno—

Responses

StatusDescriptionBody
200The updated quiz, bank links and subjects included.QuizDetail
400The request body failed validation, or the window rules were broken.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
409The organization already has a quiz with the new slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
DELETE/api/quizzes/{quiz_id} Bearer token

Archive 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Responses

StatusDescriptionBody
200The archived quiz.Quiz
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
POST/api/quizzes/{quiz_id}/duplicate Bearer token

Duplicate 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

NameInTypeRequiredDescription
quiz_idpathstringyesUUID id, or a slug resolved within the organization given by the org query parameter.
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).

Request body

application/jsonoptionalDuplicateQuizRequest
FieldTypeRequiredDescription
namestringno

The copy's name; omit for the original's with " (copy)" appended.

1–200 characters

slugstringno

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

StatusDescriptionBody
201The copy, in draft, with its bank links and subjects.QuizDetail
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this quiz but does not hold quizzes:write.ErrorResponse
404No visible quiz has that id or slug.ErrorResponse
409The organization already has a quiz with the requested slug, or another copy took the generated one first — retry.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/quizzes Bearer token

List 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

NameInTypeRequiredDescription
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
departmentquerystringnoDepartment UUID id or slug, resolved within org (which is then required).
include_archivedquerystringnoInclude archived rows. Defaults to false.
limitqueryintegernoPage size, 1-200. Defaults to 50.
cursorquerystringnoOpaque cursor from a previous page's next_cursor.

Responses

StatusDescriptionBody
200One page of quizzes, and the cursor for the next.QuizList
400A filter did not resolve, or the cursor is malformed.ErrorResponse
401The access token is missing or invalid.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/quizzes Bearer token

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

Request body

application/jsonrequiredCreateQuizRequest
FieldTypeRequiredDescription
owner_organization_idstring (uuid)yes

Organization the quiz belongs to. Always required.

owner_department_idstring (uuid)no

Omit for a quiz owned by the organization directly.

slugstringyes

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]+)*$

namestringyes

1–200 characters

descriptionstringno

Omit or send empty for no description.

at most 2000 characters

footer_markdownstringno

Omit or send empty for no closing note.

at most 50000 characters

delivery_kindstringyes

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_secondsintegerno

Per-attempt time limit in seconds; a live sitting's close still caps it.

selection_modestringyes

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_percentagenumberyes

Score required to pass, as a percentage of the questions presented.

minimum 0 · maximum 100

max_attemptsintegerno

Omit for unlimited attempts.

show_results_immediatelybooleanno

Defaults to true.

show_correct_answersbooleanno

Defaults to false.

bank_linksarray of objectno

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

StatusDescriptionBody
201The created quiz, in draft, with any bank links it was created with.QuizDetail
400The request body failed validation, or a requested bank is not in the owning organization.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller does not hold quizzes:write over the owner.ErrorResponse
404The organization, or the department within it, does not exist.ErrorResponse
409The organization already has a quiz with that slug.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse