Quiz creation API
Create quizzes programmatically with the same schema-version 3 JSON package produced by QuizMachine export.
Create quizzes programmatically with the QuizMachine API
1. Get a short-lived bearer token
POST /api/v1/auth/token
Content-Type: application/json
{"email":"trainer@example.com","password":"your-password","scopes":["quiz:read","quiz:write","topic:read","topic:write"]}
# With image upload and image placement
POST /api/v1/auth/token
Content-Type: application/json
{"email":"trainer@example.com","password":"your-password","scopes":["quiz:read","quiz:write","topic:read","topic:write","media:write"]}
PowerShell
$email=Read-Host 'Email'; $securePassword=Read-Host 'Password' -AsSecureString; $password=[pscredential]::new('user',$securePassword).GetNetworkCredential().Password; (Invoke-RestMethod -Method Post -Uri 'https://dev-quizmachine.covecode.net/api/v1/auth/token' -ContentType 'application/json' -Body (@{email=$email;password=$password;scopes=@('quiz:read','quiz:write','topic:read','topic:write')}|ConvertTo-Json)).access_token
# With images
$mediaBody = @{email=$email;password=$password;scopes=@('quiz:read','quiz:write','topic:read','topic:write','media:write')} | ConvertTo-Json
$mediaToken = (Invoke-RestMethod -Method Post -Uri 'https://dev-quizmachine.covecode.net/api/v1/auth/token' -ContentType 'application/json' -Body $mediaBody).access_token
HTTP 200 OK
Cache-Control: no-store
{
"access_token": "<token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "quiz:read quiz:write topic:read topic:write"
}
# A token requested with image access returns:
{ "scope": "quiz:read quiz:write topic:read topic:write media:write" }
Keep the returned access token secret. API tokens expire after 60 minutes, are invalidated by password changes or email-confirmation revocation, and should only be sent over HTTPS.
2. Create a quiz
POST /api/v1/quizzes
Authorization: Bearer <access_token>
Content-Type: application/json
{
"schemaVersion": "3",
"exportedAtUtc": "2026-01-01T00:00:00Z",
"quiz": {
"title": "API-created quiz",
"kind": "Quiz",
"passingScorePercent": 60,
"questions": []
}
}
Set "kind" to "Quiz" (the default) or "Article". An article is read at /Article/Read/ as one continuous scroll with a reading-progress bar, and its Text/Media blocks default to the Article presentation. An article can end with optional questions (single choice, multiple choice or free text). Submitting them records and scores a response, which supports certificates and completion analytics; a plain read creates no response.
3. Read the quizzes in your organization
Lists include quizzes owned by members of your organization. Reading or updating a complete quiz definition requires that you own the quiz; RootAdmin recovery is the only exception. A quiz in another organization always answers 404.
GET /api/v1/quizzes?page=1&pageSize=25&search=onboarding&sort=modified-desc
Authorization: Bearer <access_token>
List results are paged. Use page and pageSize (maximum 100), search to filter by title, and sort with modified-desc, modified-asc, created-desc, created-asc, title-asc, or title-desc.
HTTP 200 OK
{
"items": [
{
"id": "8f1c...",
"title": "API-created quiz",
"status": "InDesign",
"ownerId": "3a0b...",
"pageCount": 1,
"responseCount": 0,
"requiresLogin": false,
"slug": null,
"createdAtUtc": "2026-01-01T00:00:00Z",
"modifiedAtUtc": "2026-01-02T09:15:00Z"
}
],
"totalCount": 1,
"page": 1,
"pageSize": 25,
"totalPages": 1
}
GET /api/v1/quizzes/8f1c0000-0000-0000-0000-000000000000
Authorization: Bearer <access_token>
4. Update an existing quiz
An update replaces the quiz definition: settings, pages, canvas blocks, and questions. The quiz keeps its ID, owner, publication state, custom link, AI providers, paywall, folder, and collected responses.
PUT /api/v1/quizzes/8f1c0000-0000-0000-0000-000000000000
Authorization: Bearer <access_token>
Content-Type: application/json
{
"schemaVersion": "3",
"exportedAtUtc": "2026-01-02T00:00:00Z",
"quiz": {
"title": "API-created quiz",
"passingScorePercent": 80,
"questions": []
}
}
Concurrency and stable ids
Every read returns an ETag header and, inside the package, stable ids for each question, option, and image. Granular edits, publishing or closing, and custom-link changes require an If-Match header naming the revision you last read; a stale value is refused with 412 Precondition Failed instead of silently overwriting a concurrent change.
HTTP 200 OK
ETag: "3"
{ "schemaVersion": "3", "quiz": { "questions": [ { "id": "1f9e...", "topicIds": ["7c2a..."] } ] } }
PATCH /api/v1/quizzes/8f1c0000-0000-0000-0000-000000000000/questions/1f9e0000-0000-0000-0000-000000000000
Authorization: Bearer <access_token>
If-Match: "3"
Content-Type: application/merge-patch+json
{"feedbackText": "Nice work!"}
HTTP 412 Precondition Failed
{ "title": "Revision mismatch", "detail": "The quiz has changed since revision 3 was read." }
PUT /api/v1/quizzes/{id}/status · PUT/DELETE …/slug
PUT /api/v1/quizzes/{quizId}/slug
If-Match: "3"
{"slug":"training-basics"}
PUT /api/v1/quizzes/{quizId}/status
If-Match: "4"
{"status":"Open"}
DELETE /api/v1/quizzes/{quizId}/slug
If-Match: "5"
Update one question field
PATCH a single question with a JSON Merge Patch body to change its body, bodyAllowMarkdown, points, isRequired, isDisabled, randomizeOptions, caseSensitiveMatch, matchingImmediateFeedback, immediateFeedback, immediateFeedbackMaxAttempts, allowNegativeScore, showFeedback, feedbackText, feedbackAllowMarkdown, aiEvaluationEnabled, modelAnswer, rubric, aiOnlyCodeEvaluation, codeStarterCode, codeReferenceSolution, codeTestCasesJson, mathWorkingAiFeedbackEnabled, mathExpectedWorking, mathWorkingRubric, childPictureChoiceResponseMode, childPictureChoiceMaxAttempts, tilesGridSize, tilesBlankTargetPosition, tilesShuffleDepth, tilesMaximumMoves, tilesConfigurationVersion, or tilesEngineVersion field without touching anything else — its id, options, images, topics, and page placement all stay exactly as they were.
PATCH /api/v1/quizzes/{quizId}/questions/{questionId}
Content-Type: application/merge-patch+json
{ "aiEvaluationEnabled": true, "modelAnswer": "Expected answer", "rubric": "Key concepts" }
HTTP 200 OK
ETag: "4"
{ "question": { "id": "1f9e...", "aiEvaluationEnabled": true, "modelAnswer": "Expected answer", "rubric": "Key concepts" }, "revision": 4 }
Add media=references to a GET request to omit embedded image bytes and receive only metadata and a mediaAssetId where one exists. Use media=embedded, the default, for the complete portable package.
GET /api/v1/quizzes/8f1c0000-0000-0000-0000-000000000000?media=references
Authorization: Bearer <access_token>
POST /api/v1/quizzes/{id}/questions
This endpoint adds one question without replacing existing questions, options, images, or Topics. pageNumber must identify an existing page unless newPage is true. Embedded media is rejected; upload it first, then attach the returned media id.
POST /api/v1/quizzes/8f1c0000-0000-0000-0000-000000000000/questions
Authorization: Bearer <access_token>
If-Match: "4"
Content-Type: application/json
{"pageNumber":1,"order":2,"type":"FreeText","body":"Explain the result","points":1}
POST · DELETE /api/v1/quizzes/{id}/pages
Create a page at position with type Standard or SectionBreak and an optional timeLimitSeconds. Set newPage on question creation to create and use a new page. Deleting a non-empty page requires deleteContents=true.
POST /api/v1/quizzes/{quizId}/pages
If-Match: "5"
{"position":2,"type":"SectionBreak","timeLimitSeconds":60}
POST /api/v1/quizzes/{quizId}/questions
If-Match: "6"
{"newPage":true,"type":"FreeText","body":"Explain the result","points":1}
DELETE /api/v1/quizzes/{quizId}/pages/2?deleteContents=true
If-Match: "7"
POST · PATCH · DELETE /api/v1/quizzes/{id}/text-media-blocks
Create, patch, move or reorder, and delete Text, Image, YouTube, Mermaid, and LatexArticle canvas blocks by stable id. Use If-Match on every write; Image mediaAssetId changes also need media:write. newPage inserts a Standard page. LaTeX preview returns PDF or Problem Details, and paid-block entitlement follows the quiz owner's effective plan. Batch types are createBlock, patchBlock, and deleteBlock, reorderPage. Mermaid diagrams in questions and feedback
POST /api/v1/quizzes/{quizId}/text-media-blocks
If-Match: "8"
{"pageNumber":1,"order":2,"blockType":"Text","textContent":"## Context","textAllowMarkdown":true,"isHeader":true,"alignment":"Left","layoutColumns":2}
PATCH /api/v1/quizzes/{quizId}/text-media-blocks/{blockId}
If-Match: "9"
Content-Type: application/merge-patch+json
{"pageNumber":2,"order":1,"alignment":"Center","layoutColumns":3}
POST /api/v1/quizzes/{quizId}/text-media-blocks/latex-preview
{"textContent":"\\documentclass{article}..."}
DELETE /api/v1/quizzes/{quizId}/text-media-blocks/{blockId}
Topics
List and search your organization's Topics with topic:read; creating or assigning Topics requires topic:write. Send topicIds when creating a package or question, or topicNames to reuse existing Topics case-insensitively and create missing ones. PUT on a question's topics endpoint still replaces its Topic set atomically.
GET /api/v1/topics?search=heap
POST /api/v1/topics
Authorization: Bearer <access_token>
Content-Type: application/json
{"name": "Heaps"}
POST /api/v1/quizzes/{quizId}/questions
Authorization: Bearer <access_token>
If-Match: "4"
Content-Type: application/json
{"pageNumber":1,"type":"FreeText","body":"What is minimax?","points":1,"topicIds":["7c2a0000-0000-0000-0000-000000000000"],"topicNames":["Game trees"]}
PUT /api/v1/quizzes/8f1c0000-0000-0000-0000-000000000000/questions/1f9e0000-0000-0000-0000-000000000000/topics
Authorization: Bearer <access_token>
If-Match: "4"
Content-Type: application/json
{"topicIds": ["7c2a0000-0000-0000-0000-000000000000"]}
/operations · /validate
Validation is read-only. Mutating batches are atomic and require an Idempotency-Key, which clients should reuse only when retrying the same request body.
POST /api/v1/quizzes/{quizId}/validate
If-Match: "5"
POST /api/v1/quizzes/{quizId}/operations
If-Match: "5"
Idempotency-Key: <client-generated-value>
{"operations":[{"type":"createBlock","blockId":"<stable_id>","block":{"pageNumber":1,"blockType":"Mermaid","textContent":"graph TD; A-->B"}},{"type":"patchBlock","blockId":"<stable_id>","patch":{"order":3}},{"type":"deleteBlock","blockId":"<old_block_id>"}]}
POST /api/v1/media
Upload one PNG, JPEG, GIF, or WebP image, up to 5 MB, in the multipart/form-data field file. Send media:write and an Idempotency-Key; then attach the returned id to a supported image placement.
POST /api/v1/media
Authorization: Bearer <access_token>
Idempotency-Key: <client-generated-value>
Content-Type: multipart/form-data
POST /api/v1/quizzes/{quizId}/questions/{questionId}/images
PATCH /api/v1/quizzes/{quizId}/questions/{questionId}/images/{imageId}
PUT /api/v1/quizzes/{quizId}/questions/{questionId}/images/order
DELETE /api/v1/quizzes/{quizId}/questions/{questionId}/images/{imageId}
# Feedback images: attach the returned media id instead of base64 in feedbackText.
POST /api/v1/quizzes/{quizId}/questions/{questionId}/feedback-images
{"mediaAssetId":"<returned_media_id>","alignment":"Center","altText":"Worked solution"}
PATCH /api/v1/quizzes/{quizId}/questions/{questionId}/feedback-images/{imageId}
PUT /api/v1/quizzes/{quizId}/questions/{questionId}/feedback-images/order
DELETE /api/v1/quizzes/{quizId}/questions/{questionId}/feedback-images/{imageId}
# Option images; side is required for Matching questions.
PUT /api/v1/quizzes/{quizId}/questions/{questionId}/options/{optionId}/image
{"mediaAssetId":"<returned_media_id>","side":"Left"}
DELETE /api/v1/quizzes/{quizId}/questions/{questionId}/options/{optionId}/image
PUT /api/v1/quizzes/{quizId}/questions/{questionId}/diagram-reference-image
DELETE /api/v1/quizzes/{quizId}/questions/{questionId}/diagram-reference-image
PUT /api/v1/quizzes/{quizId}/text-media-blocks/{blockId}/image
Use the interactive API reference at /api/v1/docs, or consume the complete machine-readable contract at /api/v1/openapi.json.
/api/v1/docs
/api/v1/openapi.json
/api/v1/schema.json
Responses and limits
- 200 OK returns the requested quiz package, or the updated quiz ID, title, and editor URL.
- 201 Created returns the quiz ID, title, and editor URL.
- 400 rejects malformed schemas, invalid configuration, base64, or image signatures.
- 400 also rejects question body or feedback text over 65,536 characters and option text over 1,000 characters.
- 401 means the bearer token is missing or invalid.
- 403 means the account, plan, question type, or effective organization subscription is not eligible.
- 404 means no quiz with that ID exists inside your organization.
- 409 means that user already owns a quiz with the requested title.
- 412 Precondition Failed means If-Match is stale. Every granular and batch write can return it; fetch the current quiz, reapply the change, and retry.
- 429 means the edge rate limit or durable account creation quota has been reached. Wait before retrying.
- All API errors use application/problem+json Problem Details with standard title, detail, status, and instance fields.
- Question body and feedback text are limited to 65,536 characters; option text is limited to 1,000. Uploading media or changing question, feedback, option, diagram-reference, or block images requires media:write; without it, media upload returns 403. Prefer feedback-image attachments to base64 data URLs, which count toward the text limit. Attachments appear below feedback in Take, Preview, Embed, results, and result PDFs, and can be managed in the question editor.
- Requests are limited to 32 MB, 20 creations per minute at the edge, and 200 creations per day in durable account state. A reached creation limit returns 429.
- Embedded PNG, JPEG, GIF, and WebP images are verified by content, limited to 2 MB each, and charged to the effective account's storage quota. Exceeding storage returns 403.
Start with a quiz exported from the dashboard for the complete contract, including every question type and embedded image field.