The exam must be open before it accepts submissions (
POST /exams/{exam_id}/open), and its mode decides the body: a handwritten exam takes upload_id, a typed exam takes answers[]. Sending the wrong one is refused with 422 mode_mismatch. See Exams.Setup for the code samples
Setup for the code samples
The Python and Node samples on this page assume this setup. The Node samples use top-level
await, so run them as ES modules (a .mjs file, or "type": "module" in package.json).Identify the candidate
Every submission names its candidate in one of two ways. Send exactly one of them.object
Your own reference for the candidate. The candidate is created or updated by
external_id and registered for the exam automatically if needed. Fields you send replace stored ones; fields you leave out keep their stored value.string
The Evalezy id of a candidate you already created.
409 submission_exists, and details.submission_id gives you the existing one. To send a new copy, replace it.
object
Optional. Any JSON object up to 2 KB, such as your barcode or booklet number. It is returned on the submission as is.
Handwritten copies
Send theupload_id of a PDF you uploaded. The upload must belong to your institute, hold a readable PDF and not be used by another submission.
202 Accepted: the submission object plus the quote and any warnings.
Page policy
Handwritten copies are priced and handled by their page count, which comes from the upload.Every page of the PDF counts, including blank ones. Remove cover sheets and blank pages before you upload to keep the cost down.
Typed answers
For atyped exam, send answers[]. Each answer names its question by question_id or by question_label (labels are matched without regard to case) and uses the field that fits the question type.
Option labels are the labels you gave the options when you created the exam.
- Send at most one answer per question. Questions you leave out, and answers that are empty, count as not answered: they score 0 and are not charged.
- Objective answers (
mcq_*,true_false,numeric,one_word) are marked automatically against the answer key, free of charge. - Each non-blank
long_answercosts 1 credit at the standard rate. A submission with no non-blank long answer is marked at once withstatus: "graded"and a quote of 0 credits. - Send plain text. Markup and angle brackets are kept as plain text, so
x < 5reaches the grader as typed. Text containing NUL characters (\u0000) is refused with422 validation_failed, with the field error codeinvalid_characters. - An answer whose letters are more than 20% Devanagari is refused with
422 language_not_supported(details.question_label). Only English answers are graded today. - An option label the question does not have is refused with
422 unknown_option_label.
Quotes and credits
Every submission is priced before grading starts, and the price does not change afterwards.object
credits_charged on the submission shows what was actually charged. If your balance cannot cover the quote, the submission is refused with 402 insufficient_credits, and details gives required, available, balance, credit_limit and committed. Buy credits in the Vacademy dashboard; see evalezy.com/pricing.
The submission object
Every submission endpoint (create, get, the lists and the feed) returns this same shape.string
The submission id.
string
The exam it belongs to.
object
{ "id", "external_id" } of the candidate.string
live, replaced or deleted. Only a live submission can be reviewed, finalized or changed.string
Present only when
state is replaced: the id of the submission that replaced it.string
copy for handwritten copies, typed for typed answers.integer | null
Pages of the handwritten copy;
null for typed answers.boolean
true when a teacher should look at the result. See Needs review.string[]
Submission-level reasons, such as
pages_beyond_vision_limit. Cleared when the submission is approved.string | null
When it was finalized.
object | null
{ "step", "questions_done", "questions_total" }, plus estimated_ready_at while grading runs. null when no AI grading was needed. step is informational; do not branch on it.object | null
{ "position", "estimated_ready_at" } while the submission is queued; null otherwise. See Queue position and ETA.integer
How many grading runs this submission has had (re-evaluations add one).
integer | null
The rubric version the latest run graded with.
number | null
What was charged: the quote once grading completes,
0 when no AI grading was needed, and null while grading has not completed (and for failed or cancelled runs, which are free).object | null
object | null
The metadata you sent.
string
ISO 8601, UTC.
string
Moves whenever anything you can see about the submission changes.
Statuses
needs_review and finalized are separate flags, not statuses: a graded submission can still need review.
Failure codes
Follow a submission
Read one submission withGET /submissions/{submission_id}.
Queue position and ETA
While a submission isqueued, queue tells you where it stands:
positionis the number of your institute’s submissions in the same lane that are ahead of it.0means it is next. Other institutes’ work is not counted in your position.estimated_ready_atis when the result should be ready, estimated from recent grading times and current load. It is a single estimate, never earlier than 30 seconds from now.
queue becomes null and progress.estimated_ready_at carries the estimate, refined as questions are marked. Handwritten copies and typed answers wait in separate lanes, so a large batch of copies does not hold up typed answers.
List an exam’s submissions
GET /exams/{exam_id}/submissions lists the exam’s live submissions, ordered by (updated_at, id).
The submission feed
GET /submissions?updated_since=… returns submissions from all your exams that changed after updated_since, ordered by (updated_at, id). Use it to keep your system in sync without polling each submission. It also returns replaced and deleted submissions, so your sync loop learns about them (check state).
Follow these rules when you read the feed:
- Keep
updated_sincethe same for every page of one pass, and follownext_cursoruntilhas_moreisfalse. Don’t moveupdated_sinceforward in the middle of a pass: rows written together share the sameupdated_at, and you would skip some of them. - Remember the newest
updated_atyou processed (your watermark). Start the next pass from the watermark minus about 2 minutes, so rows committed a moment late are not missed. - Upsert by
idand skip a row whoseupdated_atis not newer than what you already stored. The overlap means you see some rows twice; this check makes that harmless.
Replace a submission
To send a corrected or rescanned copy for a candidate who already has a submission, create a new submission with"replace": true.
state becomes replaced and its replaced_by points to the new one. The new submission is quoted and charged as a new copy; a cancelled grading run of the old one is not charged. A finalized submission cannot be replaced (409 submission_finalized): unfinalize it first.
Cancel grading
POST /submissions/{submission_id}/cancel stops grading. A queued submission is cancelled at once; a running one is stopped. Cancelled submissions are not charged.
409 already_completed. To grade a cancelled submission later, re-evaluate it.
Delete a submission
DELETE /submissions/{submission_id} removes a submission that is not finalized, for example one made with the wrong upload. Any grading in progress is stopped.
state: "deleted", but its result is no longer available. An upload can be used only once, even if its submission is deleted: upload the file again for the new submission.
Re-evaluate
POST /submissions/{submission_id}/re-evaluate grades the whole copy again, for example after you changed the rubric or after a timed_out failure. It returns 202 Accepted with the submission object.
boolean
default:"true"
Keep the marks of questions a teacher already overrode. Set
false to have every question graded again.409 evaluation_in_progress while a run is still active (cancel it or wait), and with 409 submission_finalized once the result is finalized. Re-evaluating single questions (question_ids) is not available yet and is refused with 422 feature_not_available.
Retries and idempotency
EveryPOST above accepts an optional Idempotency-Key header. A retry with the same key and the same body returns the stored response with Idempotent-Replayed: true, so a network timeout never creates or charges a submission twice. The one-live-submission rule protects you too: a duplicate submission for the same candidate and exam is refused with 409 submission_exists.