Submissions

Take a test as a learner, start a submission, save answers, and retrieve graded results.

The submission flow lets learners take a test via the public shareToken without needing an API key. Submissions are uniquely identified per (test, email) pair, so returning to the test with the same email resumes any in-progress submission.

Flow Overview

  1. Load the test for taking, GET /v1/platform/tests/public/:shareToken/take
  2. Start a submission, POST /v1/platform/tests/public/:shareToken/submissions
  3. Save answers (optionally repeatedly), PATCH /v1/platform/tests/submissions/:submissionToken
  4. Submit final answers, same PATCH endpoint with isDone: true
  5. Retrieve the graded result, GET /v1/platform/tests/submissions/:submissionToken/result
  6. (Workspace) List all submissions, GET /v1/platform/tests/:id/submissions

GET /v1/platform/tests/public/:shareToken/take

Loads the test in taking mode. Item correctAnswers and explanation are not included on this endpoint. They are only revealed after the submission is finalized.

Response

FieldTypeDescription
titlestring | nullTest title
descriptionstring | nullTest description
levelstring | nullDifficulty level
timeLimitnumberTime limit in minutes
itemCountnumberNumber of items
totalScorenumberSum of all item scores
items[].sequencenumber1-based item position. Use as the addressing key when saving answers
items[].titlestringShort item label, useful for sidebar/progress UIs
items[].typestringOne of select, true-false, blank, open-ended
items[].questionstringQuestion prompt
items[].optionsstring[] | nullChoice options for select items; null for other types
items[].scorenumberMaximum score awardable for the item

Example Response

{
  "title": "Basic Algebra Quiz",
  "description": "A 10-question quiz covering linear equations and inequalities.",
  "level": "medium",
  "timeLimit": 30,
  "itemCount": 10,
  "totalScore": 100,
  "items": [
    {
      "sequence": 1,
      "title": "Solve 2x + 3 = 11",
      "type": "select",
      "question": "What is the solution to 2x + 3 = 11?",
      "options": ["x = 3", "x = 4", "x = 5", "x = 6"],
      "score": 10
    },
    {
      "sequence": 2,
      "title": "Inequality boundary",
      "type": "true-false",
      "question": "The inequality x > 5 includes the value 5.",
      "options": null,
      "score": 10
    }
  ]
}

POST /v1/platform/tests/public/:shareToken/submissions

Starts a new submission, or resumes an existing in-progress submission for the same email address.

Request Body

FieldTypeRequiredDescription
emailstringYesLearner email, used to identify and resume submissions
namestringNoOptional learner display name (≤ 200 chars)

Example Request

curl -X POST https://api.tutorflow.io/v1/platform/tests/public/{shareToken}/submissions \
  -H "Content-Type: application/json" \
  -d '{ "email": "alice@example.com", "name": "Alice" }'

Response

FieldTypeDescription
submissionIdstringInternal submission ID
submissionTokenstring32-character hex token used to submit answers and fetch the result
startedAtstringISO 8601 timestamp when the submission was first started
resumedtrue (omitted for fresh)Present and set to true only when an existing in-progress submission was resumed for the same email
savedAnswersarray (omitted for fresh)Previously saved { sequence, answers } rows. Only present when resumed: true
testobjectSame shape as GET /v1/platform/tests/public/:shareToken/take

Idempotent per email: re-calling this endpoint with an email that already has an unfinalized submission returns the EXISTING submission token (with resumed: true), not a new one. Use this to support "close tab, come back later" UX without inventing client-side persistence.

Example Response, fresh submission

{
  "submissionId": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
  "submissionToken": "b3f1a2c4d5e6f7890abcdef1234567890",
  "startedAt": "2026-03-24T11:00:00.000Z",
  "test": {
    "title": "Basic Algebra Quiz",
    "description": "A 10-question quiz...",
    "level": "medium",
    "timeLimit": 30,
    "itemCount": 10,
    "totalScore": 100,
    "items": [
      { "sequence": 1, "title": "Solve 2x + 3 = 11", "type": "select", "question": "...", "options": ["..."], "score": 10 }
    ]
  }
}

Example Response, resumed submission

{
  "submissionId": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
  "submissionToken": "b3f1a2c4d5e6f7890abcdef1234567890",
  "startedAt": "2026-03-24T10:50:00.000Z",
  "resumed": true,
  "savedAnswers": [
    { "sequence": 1, "answers": ["x = 4"] },
    { "sequence": 2, "answers": ["false"] }
  ],
  "test": { "title": "Basic Algebra Quiz", "items": [/* ... */] }
}

PATCH /v1/platform/tests/submissions/:submissionToken

Saves answers for an in-progress submission. Call multiple times to save progress incrementally; call once with isDone: true to finalize and trigger grading.

Request Body

FieldTypeRequiredDescription
itemsarrayYesArray of { sequence, answers } objects
items[].sequencenumberYesItem sequence number
items[].answersstring[]YesLearner's answer(s) for the item
isDonebooleanNoSet to true to finalize the submission. Defaults to false

Example Request

curl -X PATCH https://api.tutorflow.io/v1/platform/tests/submissions/{submissionToken} \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "sequence": 1, "answers": ["x = 4"] },
      { "sequence": 2, "answers": ["false"] },
      { "sequence": 3, "answers": ["7"] },
      { "sequence": 4, "answers": ["An equation states two expressions are equal..."] }
    ],
    "isDone": true
  }'

Auto-Grading

When isDone: true, items are graded as follows:

Item TypeGradingResulting status
selectAuto-graded against correctAnswers after trimming and lowercasing. Answers may be option text or a 0-based option index string such as "0"CORRECT or INCORRECT
true-falseAuto-graded against correctAnswers after trimming and lowercasingCORRECT or INCORRECT
blankAuto-graded against correctAnswers after trimming and lowercasingCORRECT or INCORRECT
open-endedNot auto-gradeable, requires manual reviewPENDING

Tests containing open-ended items will return a totalScore that reflects only the auto-graded portion until the open-ended items are reviewed.

Response, when isDone: true (finalized)

FieldTypeDescription
submissionIdstringSubmission ID
isDonetrueAlways true for this branch
totalScorenumberSum of score across all auto-graded items (open-ended items contribute 0 until manual review)
maxScorenumberMaximum possible total score
finishedAtstringISO 8601 timestamp
items[].sequencenumber1-based item position
items[].answersstring[] | nullWhat the learner submitted
items[].statusstringCORRECT, INCORRECT, or PENDING
items[].scorenumberScore earned for this item (0 if not yet graded or wrong)
items[].maxScorenumberMaximum possible score for the item
items[].correctAnswersstring[] | nullExpected answer(s); null for open-ended
items[].explanationstring | nullPer-item explanation

Note: the finalize response intentionally omits type, question, and options. Fetch them from the take payload (still in scope client-side) or call GET /v1/platform/tests/submissions/:submissionToken/result for the full enriched view.

{
  "submissionId": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
  "isDone": true,
  "totalScore": 10,
  "maxScore": 40,
  "finishedAt": "2026-03-24T11:25:42.000Z",
  "items": [
    {
      "sequence": 1,
      "answers": ["x = 4"],
      "status": "CORRECT",
      "score": 10,
      "maxScore": 10,
      "correctAnswers": ["x = 4"],
      "explanation": "Subtract 3 from both sides, then divide by 2."
    },
    {
      "sequence": 4,
      "answers": ["An equation states two expressions are equal..."],
      "status": "PENDING",
      "score": 0,
      "maxScore": 10,
      "correctAnswers": null,
      "explanation": "An equation uses =, an inequality uses <, >, ≤, or ≥."
    }
  ]
}

Response, when isDone: false (autosave)

Use this shape to confirm partial saves between question changes. Only sequence and the echoed answers are returned. No grading info is leaked while the submission is open.

{
  "submissionId": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
  "isDone": false,
  "items": [
    { "sequence": 1, "answers": ["x = 4"] },
    { "sequence": 2, "answers": ["false"] }
  ]
}

GET /v1/platform/tests/submissions/:submissionToken/result

Fetches the full state of a submission. Public, no API key required. Works while the submission is in progress AND after it has been finalized; the response shape varies by isDone.

Example Request

curl https://api.tutorflow.io/v1/platform/tests/submissions/{submissionToken}/result

Response Fields

Always-present fields (both isDone branches):

FieldTypeDescription
submissionIdstringSubmission ID
emailstringLearner email
namestring | nullLearner display name
isDonebooleanWhether the submission has been finalized
startedAtstringISO 8601
finishedAtstring | nullISO 8601 if isDone, else null
totalScorenumberEarned score (auto-graded portion only)
maxScorenumberMaximum possible total score
itemsarrayPer-item state. Shape depends on isDone

When isDone: false, each item is just { sequence, answers }. No grading info is returned.

When isDone: true, each item is enriched with the question content + correct answers + grading:

FieldTypeDescription
items[].sequencenumber1-based item position
items[].typestring | nullselect, true-false, blank, or open-ended
items[].questionstring | nullQuestion prompt
items[].optionsstring[] | nullChoices (for select)
items[].answersstring[] | nullLearner-submitted answers
items[].correctAnswersstring[] | nullExpected answers (null for open-ended)
items[].explanationstring | nullPer-item explanation
items[].statusstringCORRECT, INCORRECT, or PENDING
items[].scorenumberScore earned
items[].maxScorenumberMaximum possible score for the item

Example Response (finalized)

{
  "submissionId": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
  "email": "alice@example.com",
  "name": "Alice",
  "isDone": true,
  "startedAt": "2026-03-24T11:00:00.000Z",
  "finishedAt": "2026-03-24T11:25:42.000Z",
  "totalScore": 30,
  "maxScore": 40,
  "items": [
    {
      "sequence": 1,
      "type": "select",
      "question": "What is the solution to 2x + 3 = 11?",
      "options": ["x = 3", "x = 4", "x = 5", "x = 6"],
      "answers": ["x = 4"],
      "correctAnswers": ["x = 4"],
      "explanation": "Subtract 3 from both sides, then divide by 2.",
      "status": "CORRECT",
      "score": 10,
      "maxScore": 10
    }
  ]
}

GET /v1/platform/tests/:id/submissions

Lists all submissions for a given test request. Requires Platform API key auth.

Example Request

curl https://api.tutorflow.io/v1/platform/tests/a1c2d3e4-5678-4abc-9def-0123456789ab/submissions \
  -H "Authorization: Bearer tf_platform_..."

Response

FieldTypeDescription
items[].submissionIdstringSubmission ID
items[].submissionTokenstring32-character hex submission token (for fetching result)
items[].emailstringLearner email
items[].namestring | nullLearner display name
items[].isDonebooleanWhether the submission has been finalized
items[].startedAtstringISO 8601 start timestamp
items[].finishedAtstring | nullISO 8601 finalize timestamp
items[].totalScorenumberEarned score (auto-graded portion)
items[].maxScorenumberMaximum possible score
items[].createdAtstringISO 8601 submission row creation timestamp
totalnumberTotal number of submissions for this test

Example Response

{
  "items": [
    {
      "submissionId": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
      "submissionToken": "b3f1a2c4d5e6f7890abcdef1234567890",
      "email": "alice@example.com",
      "name": "Alice",
      "isDone": true,
      "startedAt": "2026-03-24T11:00:00.000Z",
      "finishedAt": "2026-03-24T11:25:42.000Z",
      "totalScore": 30,
      "maxScore": 40,
      "createdAt": "2026-03-24T11:00:00.000Z"
    }
  ],
  "total": 1
}

Public Read-Only Endpoints

These additional endpoints serve the test editor preview (previewUrl):

MethodPathDescription
GET/v1/platform/tests/public/:shareTokenGet test summary (no items)
GET/v1/platform/tests/public/:shareToken/fullGet full test with items, correct answers, and explanations
GET/v1/platform/tests/public/:shareToken/items/:sequenceGet a single item with answer

/full automatically refreshes the underlying edit token if it has expired or is missing. /items/:sequence returns a single item by share token and does not refresh or return an edit token.

Error Conditions

HTTPWhenWhat to do
404 on POST /v1/platform/tests/public/:shareToken/submissionsshareToken is unknown or test was deletedVerify the share token from the original POST /v1/platform/tests response
400 on PATCH /v1/platform/tests/submissions/:submissionTokenThe submission was already finalized (isDone: true)Final answers are immutable; create a new submission with a different email if a retake is needed
404 on PATCH /v1/platform/tests/submissions/:submissionTokensubmissionToken is unknownThe token was wrong, never created, or belongs to another test
404 on GET /v1/platform/tests/submissions/:submissionToken/resultsubmissionToken is unknownSame as above
404 on GET /v1/platform/tests/:id/submissionsid is unknown OR caller's API key does not own the workspaceVerify the test request id and that your API key matches the workspace that created the test