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
- Load the test for taking,
GET /v1/platform/tests/public/:shareToken/take - Start a submission,
POST /v1/platform/tests/public/:shareToken/submissions - Save answers (optionally repeatedly),
PATCH /v1/platform/tests/submissions/:submissionToken - Submit final answers, same
PATCHendpoint withisDone: true - Retrieve the graded result,
GET /v1/platform/tests/submissions/:submissionToken/result - (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
| Field | Type | Description |
|---|---|---|
title | string | null | Test title |
description | string | null | Test description |
level | string | null | Difficulty level |
timeLimit | number | Time limit in minutes |
itemCount | number | Number of items |
totalScore | number | Sum of all item scores |
items[].sequence | number | 1-based item position. Use as the addressing key when saving answers |
items[].title | string | Short item label, useful for sidebar/progress UIs |
items[].type | string | One of select, true-false, blank, open-ended |
items[].question | string | Question prompt |
items[].options | string[] | null | Choice options for select items; null for other types |
items[].score | number | Maximum 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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Learner email, used to identify and resume submissions |
name | string | No | Optional 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
| Field | Type | Description |
|---|---|---|
submissionId | string | Internal submission ID |
submissionToken | string | 32-character hex token used to submit answers and fetch the result |
startedAt | string | ISO 8601 timestamp when the submission was first started |
resumed | true (omitted for fresh) | Present and set to true only when an existing in-progress submission was resumed for the same email |
savedAnswers | array (omitted for fresh) | Previously saved { sequence, answers } rows. Only present when resumed: true |
test | object | Same 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
| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Array of { sequence, answers } objects |
items[].sequence | number | Yes | Item sequence number |
items[].answers | string[] | Yes | Learner's answer(s) for the item |
isDone | boolean | No | Set 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 Type | Grading | Resulting status |
|---|---|---|
select | Auto-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-false | Auto-graded against correctAnswers after trimming and lowercasing | CORRECT or INCORRECT |
blank | Auto-graded against correctAnswers after trimming and lowercasing | CORRECT or INCORRECT |
open-ended | Not auto-gradeable, requires manual review | PENDING |
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)
| Field | Type | Description |
|---|---|---|
submissionId | string | Submission ID |
isDone | true | Always true for this branch |
totalScore | number | Sum of score across all auto-graded items (open-ended items contribute 0 until manual review) |
maxScore | number | Maximum possible total score |
finishedAt | string | ISO 8601 timestamp |
items[].sequence | number | 1-based item position |
items[].answers | string[] | null | What the learner submitted |
items[].status | string | CORRECT, INCORRECT, or PENDING |
items[].score | number | Score earned for this item (0 if not yet graded or wrong) |
items[].maxScore | number | Maximum possible score for the item |
items[].correctAnswers | string[] | null | Expected answer(s); null for open-ended |
items[].explanation | string | null | Per-item explanation |
Note: the finalize response intentionally omits
type,question, andoptions. Fetch them from the take payload (still in scope client-side) or callGET /v1/platform/tests/submissions/:submissionToken/resultfor 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}/resultResponse Fields
Always-present fields (both isDone branches):
| Field | Type | Description |
|---|---|---|
submissionId | string | Submission ID |
email | string | Learner email |
name | string | null | Learner display name |
isDone | boolean | Whether the submission has been finalized |
startedAt | string | ISO 8601 |
finishedAt | string | null | ISO 8601 if isDone, else null |
totalScore | number | Earned score (auto-graded portion only) |
maxScore | number | Maximum possible total score |
items | array | Per-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:
| Field | Type | Description |
|---|---|---|
items[].sequence | number | 1-based item position |
items[].type | string | null | select, true-false, blank, or open-ended |
items[].question | string | null | Question prompt |
items[].options | string[] | null | Choices (for select) |
items[].answers | string[] | null | Learner-submitted answers |
items[].correctAnswers | string[] | null | Expected answers (null for open-ended) |
items[].explanation | string | null | Per-item explanation |
items[].status | string | CORRECT, INCORRECT, or PENDING |
items[].score | number | Score earned |
items[].maxScore | number | Maximum 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
| Field | Type | Description |
|---|---|---|
items[].submissionId | string | Submission ID |
items[].submissionToken | string | 32-character hex submission token (for fetching result) |
items[].email | string | Learner email |
items[].name | string | null | Learner display name |
items[].isDone | boolean | Whether the submission has been finalized |
items[].startedAt | string | ISO 8601 start timestamp |
items[].finishedAt | string | null | ISO 8601 finalize timestamp |
items[].totalScore | number | Earned score (auto-graded portion) |
items[].maxScore | number | Maximum possible score |
items[].createdAt | string | ISO 8601 submission row creation timestamp |
total | number | Total 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):
| Method | Path | Description |
|---|---|---|
GET | /v1/platform/tests/public/:shareToken | Get test summary (no items) |
GET | /v1/platform/tests/public/:shareToken/full | Get full test with items, correct answers, and explanations |
GET | /v1/platform/tests/public/:shareToken/items/:sequence | Get 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
| HTTP | When | What to do |
|---|---|---|
404 on POST /v1/platform/tests/public/:shareToken/submissions | shareToken is unknown or test was deleted | Verify the share token from the original POST /v1/platform/tests response |
400 on PATCH /v1/platform/tests/submissions/:submissionToken | The 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/:submissionToken | submissionToken is unknown | The token was wrong, never created, or belongs to another test |
404 on GET /v1/platform/tests/submissions/:submissionToken/result | submissionToken is unknown | Same as above |
404 on GET /v1/platform/tests/:id/submissions | id is unknown OR caller's API key does not own the workspace | Verify the test request id and that your API key matches the workspace that created the test |