Flow
- Prepare the media. Images can be written as
httpslinks directly in the package, or uploaded first toPOST /question-assetsto get anasset_id. Audio can only be sent as an upload. - (Optional) Check the package with
POST /question-imports/validate. - Send the package to
POST /question-importswith anIdempotency-Keyheader. The package is checked right away; if it is valid, the response is202and the package is processed in the background. - Follow the result through
GET /question-imports/{id}or thequestion_import.completedandquestion_import.failedwebhooks. - Store
question_bank.idfor the revision andquestion_idfor every question.
Error format
The endpoints on this page returnerrors as a list, not as the per-field object of other endpoints, because one package can hold hundreds of questions. pointer is a JSON Pointer to the part of the body at fault and source_item_id is the source_id of that question or passage; for an error that is not about one question, both are null. One response carries at most 1,000 errors.
Endpoints
POST /question-assets — Upload an image or audio file
POST /question-assets — Upload an image or audio file
Uploads one file as
multipart/form-data. The file type is read from its contents, not from its name or declared type.- Images: JPEG, PNG, WebP or GIF, up to 5 MB and 10,000 pixels per side.
- Audio: MP3, WAV or OGG, up to 10 MB.
- SVG and any other file are rejected.
asset_id with status 200 and a renewed expiry. An asset can be used in any package until expires_at (7 days after the upload); after that it is deleted. An import copies the file into the question package, so questions are not affected when the asset expires.Parameters
Example request
Example response
Status201.POST /question-imports/validate — Check a package
POST /question-imports/validate — Check a package
Takes the same body as
POST /question-imports and checks it without storing anything. No Idempotency-Key is needed. Validation only checks the shape of image links; images are downloaded when the import is processed, so an unreachable link only shows up as image_download_failed on a failed import.Example request
Example response
Status200 when the package is valid. When it is not, status 422 with the error format above.POST /question-imports — Send a package
POST /question-imports — Send a package
Checks the package and then processes it in the background. When there are errors, the response is
422 with every error and nothing is stored. When the package is valid, the response is 202 with a Location header pointing to GET /question-imports/{id}.An Idempotency-Key is required: up to 255 ASCII characters without spaces, for example {package_id}-r{revision}-{uuid}. A key is valid for 7 days:- Same key and same package body: the existing import is returned with status
200and the headerIdempotent-Replayed: true. This is safe to use when the connection drops before the response arrives. - Same key with a different package body: rejected with
422and the codeidempotency_key_mismatch.
source.package_id and source.revision pair can become a question package only once per project. A revision that was already imported is rejected with 409 (revision_already_imported). A failed import does not prevent sending the same revision again.Parameters
Example request
Example response
Status202.GET /question-imports — List imports
GET /question-imports — List imports
GET /question-imports/{id} — Status and ID mapping
GET /question-imports/{id} — Status and ID mapping
Package format
Question types
Each option has an
id (unique within the question, up to 50 characters), content (HTML) or an image, and optional points (default 0). answer_key may give an id as a string or a number. points are used by the Sum of Answer Scores scoring method, so partial credit can be set per option.
multiple_choice example:
categorization example (Fact/Opinion; for True/False just change the categories):
short_answer example with several correct answers:
format: "numeric": the participant’s answer box only accepts numbers, and every correct answer must be a number.format: "text": the participant types free text. Whenformatis not sent, the question usesnumericif every correct answer is a number, andtextotherwise.- Two numbers are compared by value (
0,5equals0.50); anything else is compared as text, ignoring letter case and extra spaces. There is no numeric tolerance; write every form you accept as its own answer. Each answer is up to 255 characters.
matching example:
Content and media
- HTML keeps the same tags as the question editor in the admin panel:
b i em span p u strong table th tr td ul li ol br sup sub pre codefor questions, passages and explanations; options and statements keep onlyb i em span p strong ul li ol br sup sub pre code(no underline and no tables). Other tags are unwrapped (their text stays);script,style,iframe,svgand similar elements are removed with their content; every attribute is removed exceptcolspanandrowspanon table cells. Content that was changed is reported inwarningswith the codecontent_sanitized. - Write
<and>inside text or formulas as<and>, for example\(a < b\). Control characters (other than tabs and line breaks) are rejected. - Formulas are written in LaTeX with the delimiters
\( ... \),\[ ... \],$ ... $or$$ ... $$. - Question, option, statement and explanation text is up to 5,000 characters once tags are removed.
- Images inside text are not supported and are removed. Send question images through
media.imagesand option images throughoptions.*.image. - Images from links must be
https, publicly reachable, and JPEG, PNG, GIF or WebP up to 5 MB. Images are downloaded into the project’s storage; the original link is neither stored nor shown to participants. Links to internal network addresses, more than 3 redirects and files whose contents are not an image are rejected. Up to 300 distinct links per package. - Every image and audio file counts towards the project’s storage quota.
Import status
A failed import carries
error.code, error.message, error.retryable and error.details (a list of per-question errors in the same shape as the error format):
When
retryable is true, send the package again with a new Idempotency-Key. When it is false, fix the data as details describes, then send it again with a new Idempotency-Key.
Webhooks
Subscribe to thequestion_import.completed and question_import.failed events through the Webhooks API. Headers and signatures are the same as for other events.
question_import.failed, question_bank is null and error holds the cause. The full ID mapping is read through GET /question-imports/{id}.
Limits
- 500 questions and 100 passages per package; the body is up to 10 MB.
- 20 imports per hour per project; one import is processed at a time and the others wait in the queue.
- The API rate limit of 120 requests per minute still applies.
- The package body is kept for 30 days after the import finishes and then deleted. The question package and the ID mapping are not deleted.

