POST /v1/platform/tests
Creates a test request. The AI model generates a structured test with questions of mixed types (multiple choice, true/false, fill-in-the-blank, open-ended), creates the test in TutorFlow, and returns preview and public URLs.
Pricing
The Test API is billed per generated item (per question). One unit on the catalog = one item.
| Unit | Price |
|---|---|
| Per item | $0.015 |
A test request generates itemCount items (default 10, hard cap 30). The
billable amount on the request equals unitPrice × actual generated items, not
the requested itemCount. The AI may produce slightly fewer, and billing
follows what was actually delivered.
Legacy
tierfield: Thetierfield is still accepted for backwards compatibility but has no effect on price. Omit it in new integrations. If you send it, use one of the legacy request values:basic,standard, oradvanced. Thedefaulttier appears only in responsepriceSnapshotand the pricing catalog.
The priceSnapshot returned with the test response carries the full billing
record:
{
"category": "test_creation",
"tier": "default",
"unit": "item",
"unitPrice": 0.015,
"units": 10,
"amountUsd": 0.15,
"currency": "USD",
"source": "platform_pricing_catalog_v2"
}Item Types
The AI generates a mix of the following item types based on the prompt:
| Type | Description | Auto-graded |
|---|---|---|
select | Multiple choice (one correct answer from options) | Yes |
true-false | True/false question | Yes |
blank | Fill-in-the-blank (exact-match grading) | Yes |
open-ended | Free-form written response | No (manual review) |
select items always include an options array. true-false, blank, and open-ended
items have options: null.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes | Prompt describing the test to generate |
title | string | No | Override AI-generated title |
description | string | No | Override AI-generated description |
language | string | No | Test language (default: en) |
level | string | No | Difficulty hint (e.g. easy, medium, hard) |
subject | string | No | Subject area (e.g. mathematics, physics, programming) |
itemCount | number | No | Target number of items for AI to generate (1–30, default: 10) |
timeLimit | number | No | Time limit in minutes (default: 60, min: 1) |
classroomId | string | No | Classroom ID to create the test in. Uses the default classroom if omitted |
tier | string | No | Legacy request value only: basic, standard, or advanced. Omit for new integrations. Do not send default; default is a response/catalog tier. |
idempotencyKey | string | No | Prevents duplicate processing. Can also be sent via Idempotency-Key HTTP header |
mode | string | No | sync (default) or async |
Example Request
curl -X POST https://api.tutorflow.io/v1/platform/tests \
-H "Authorization: Bearer tf_platform_..." \
-H "Content-Type: application/json" \
-d '{
"prompt": "Create a 10-question quiz on basic algebra covering linear equations and inequalities",
"language": "en",
"level": "medium",
"itemCount": 10,
"timeLimit": 30
}'Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Platform test request ID |
status | string | PENDING, PROCESSING, COMPLETED, or FAILED |
classroomTestId | string | null | Actual test ID in TutorFlow (null until generation completes) |
title | string | null | Test title |
description | string | null | Test description |
level | string | null | Difficulty level (easy, medium, or hard) |
timeLimit | number | null | Time limit in minutes |
totalScore | number | null | Sum of all item scores |
itemCount | number | null | Number of items in the generated test |
tier | string | Pricing tier used |
priceSnapshot | object | null | Pricing details captured at request time |
items | array | null | Item summaries with sequence, title, type, question, options, score |
shareToken | string | null | Token used to construct preview and public URLs |
previewUrl | string | null | Editor URL, /{locale}/platform/tests/{shareToken} |
publicUrl | string | null | Public test-taking URL, /{locale}/platform/tests/take/{shareToken} |
isTerminal | boolean | Whether the request has reached a final state |
pollAfterMs | number | null | Recommended polling interval for async requests |
idempotencyKey | string | null | Echoed idempotency key |
idempotentReplay | boolean | null | true if this response reused an existing request, false for a fresh request, null when no idempotencyKey was provided |
createdAt | string | ISO 8601 timestamp |
completedAt | string | null | ISO 8601 timestamp when generation completed |
Example Response
{
"id": "a1c2d3e4-f5a6-4789-8b0c-d1e2f3a4b5c6",
"status": "COMPLETED",
"classroomTestId": "c7d8e9f0-1a2b-43c4-9e6f-7a8b9c0d1e2f",
"title": "Basic Algebra Quiz",
"description": "A 10-question quiz covering linear equations and inequalities.",
"level": "medium",
"timeLimit": 30,
"totalScore": 100,
"itemCount": 10,
"tier": "default",
"priceSnapshot": {
"category": "test_creation",
"catalogKey": "test_creation.default",
"tier": "default",
"unit": "item",
"unitPrice": 0.015,
"units": 10,
"amountUsd": 0.15,
"currency": "USD",
"source": "platform_pricing_catalog_v2"
},
"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
},
{
"sequence": 3,
"title": "Solve for y",
"type": "blank",
"question": "Solve for y: 3y = 21. y = ___",
"options": null,
"score": 10
},
{
"sequence": 4,
"title": "Equation vs inequality",
"type": "open-ended",
"question": "Explain the difference between an equation and an inequality.",
"options": null,
"score": 10
}
],
"shareToken": "b018172542f9a3c4d5e6f7890abcdef12345678",
"previewUrl": "https://tutorflow.io/en/platform/tests/b018172542f9a3c4d5e6f7890abcdef12345678",
"publicUrl": "https://tutorflow.io/en/platform/tests/take/b018172542f9a3c4d5e6f7890abcdef12345678",
"idempotencyKey": null,
"idempotentReplay": null,
"isTerminal": true,
"createdAt": "2026-03-24T10:32:15.108Z",
"completedAt": "2026-03-24T10:32:31.554Z"
}Preview URL vs Public URL vs Edit URL
The response includes two URL fields plus an implicit edit URL pattern:
| URL | Purpose | Auth | Notes |
|---|---|---|---|
publicUrl | Public test-taking page for learners | Public, no login | Lives at /{locale}/platform/tests/take/{shareToken}, the link you share with test-takers |
previewUrl | Reserved for a future server-rendered editor preview | Public, no login | Currently returned in API responses but not all deployments expose a frontend page at this URL. For reliable editor access, prefer minting an edit token (below). |
| Editor URL | Authoring editor (title, description, items, answers) | Public, no login | Build from editToken: /{locale}/platform/tests/edit/{editToken} |
To get an editor URL, mint a fresh edit token via POST /v1/platform/tests/:id/edit-token.
Edit tokens expire after 1 hour; minting a new one is cheap and idempotent.
The publicUrl is the read-only link you share with learners. They land on the test
intro page and start a submission by submitting their email (and optional name).
See Submissions for the full taking flow.
Refreshing an Edit Token
curl -X POST https://api.tutorflow.io/v1/platform/tests/a1c2d3e4-5678-4abc-9def-0123456789ab/edit-token \
-H "Authorization: Bearer tf_platform_..."{
"editToken": "new-token-hex...",
"editTokenExpiresAt": "2026-03-24T13:30:00.000Z"
}Edit Token API
The edit URL provides access to these public endpoints (no API key needed). Two addressing modes coexist for backwards compatibility:
- By UUID (
/items/by-id/:itemId), preferred for editor surfaces. Stable even if items are reordered. - By sequence (
/items/:sequence), legacy / read paths. Re-numbers when items are inserted, deleted, or reordered.
| Method | Path | Description |
|---|---|---|
GET | /v1/platform/tests/edit/:editToken | Get test summary |
GET | /v1/platform/tests/edit/:editToken/full | Get full test with all items, correct answers, and explanations |
GET | /v1/platform/tests/edit/:editToken/items/:sequence | Get a single item (with answer) |
PATCH | /v1/platform/tests/edit/:editToken | Update test title or description |
PATCH | /v1/platform/tests/edit/:editToken/full | Bulk update, accepts the full items[] array; performs upsert + delete |
POST | /v1/platform/tests/edit/:editToken/items | Create a single item |
PATCH | /v1/platform/tests/edit/:editToken/items/by-id/:itemId | Update one item by UUID |
DELETE | /v1/platform/tests/edit/:editToken/items/by-id/:itemId | Delete one item by UUID |
PATCH | /v1/platform/tests/edit/:editToken/items/reorder | Reorder items, accepts [{ id, sequence }] |
POST | /v1/platform/tests/edit/:editToken/items/generate | Generate additional items via AI (synchronous) |
PATCH | /v1/platform/tests/edit/:editToken/items/:sequence | Update one item by sequence (legacy path) |
Update Test
curl -X PATCH https://api.tutorflow.io/v1/platform/tests/edit/{editToken} \
-H "Content-Type: application/json" \
-d '{ "title": "Algebra Mid-Term", "description": "Updated copy" }'| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Updated test title |
description | string | No | Updated test description |
Use
PATCH /v1/platform/tests/edit/:editToken/fullwhen you also need to changelevel,timeLimit, or the items array. The title-only endpoint above does not accept those fields.
Bulk Update (full)
Used by the editor's bulk-save / drag-reorder paths. Accepts the same payload shape the classroom admin PATCH accepts. The server diffs items[] against the persisted set: items with an existing id are updated, items without id are created, and any persisted item not present in the payload is deleted.
curl -X PATCH https://api.tutorflow.io/v1/platform/tests/edit/{editToken}/full \
-H "Content-Type: application/json" \
-d '{
"title": "Algebra Mid-Term",
"description": "Updated copy",
"level": "medium",
"timeLimit": 45,
"items": [
{ "id": "...", "sequence": 1, "type": "select", "title": "...", "question": "...", "options": ["..."], "correctAnswers": ["..."], "explanation": "...", "score": 10 }
]
}'Returns { "success": true } on completion.
Create Item
curl -X POST https://api.tutorflow.io/v1/platform/tests/edit/{editToken}/items \
-H "Content-Type: application/json" \
-d '{
"type": "select",
"title": "Solve 2x + 3 = 11",
"question": "What is x?",
"options": ["3", "4", "5", "6"],
"correctAnswers": ["4"],
"explanation": "Subtract 3, divide by 2",
"score": 10
}'Returns the created item including its assigned id and sequence (appended to the end of the items list).
Update Item (by UUID)
Preferred for editor surfaces, survives reorders. Returns { "success": true }.
curl -X PATCH https://api.tutorflow.io/v1/platform/tests/edit/{editToken}/items/by-id/{itemId} \
-H "Content-Type: application/json" \
-d '{
"title": "Solve for y",
"question": "Solve for y: 3y = 21",
"correctAnswers": ["7"],
"score": 15
}'| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Updated short item label |
question | string | No | Updated question text |
options | array | No | Updated options (for select items) |
correctAnswers | array | No | Updated correct answer(s) |
explanation | string | No | Updated explanation shown after submission |
score | number | No | Updated point value for the item |
Update Item (by sequence)
Same body as the by-UUID variant. Use for legacy integrations only, sequence numbers shift when other items are inserted, deleted, or reordered.
curl -X PATCH https://api.tutorflow.io/v1/platform/tests/edit/{editToken}/items/3 \
-H "Content-Type: application/json" \
-d '{ "question": "Solve for y: 3y = 21", "correctAnswers": ["7"], "score": 15 }'Delete Item (by UUID)
curl -X DELETE https://api.tutorflow.io/v1/platform/tests/edit/{editToken}/items/by-id/{itemId}Returns { "success": true }. Remaining items are NOT renumbered server-side; if
the caller needs contiguous sequences, follow up with /items/reorder.
Reorder Items
curl -X PATCH https://api.tutorflow.io/v1/platform/tests/edit/{editToken}/items/reorder \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "id": "abc...", "sequence": 1 },
{ "id": "def...", "sequence": 2 },
{ "id": "ghi...", "sequence": 3 }
]
}'Persists new sequence values atomically. Returns { "success": true }.
Generate Items (AI)
Synchronous (non-streaming) item generation. The new items are persisted to the test before the response returns; the response contains the final array.
curl -X POST https://api.tutorflow.io/v1/platform/tests/edit/{editToken}/items/generate \
-H "Content-Type: application/json" \
-d '{
"itemCount": 5,
"itemTypes": ["select", "select", "true-false", "blank", "open-ended"],
"language": "en"
}'| Field | Type | Required | Description |
|---|---|---|---|
itemCount | number | Yes | Number of new items to generate |
itemTypes | string[] | Yes | Per-slot type list. Length should equal itemCount |
language | string | No | Generation language (default: test language) |
Async Mode
Set mode: "async" to queue test generation as a background job. The response
returns immediately with status: "PENDING" and a pollAfterMs value. Poll
GET /v1/platform/tests/:id until isTerminal is true.
Idempotency
Pass an idempotencyKey in the request body or Idempotency-Key header to prevent
duplicate test generation. Reusing the same key returns the original response
with idempotentReplay: true.