Simulations are reachable from the Content API with the same operations the admin UI has, and at the same credit cost. The API routes through the very services the UI calls, so nothing about access, validation, or pricing differs between the two.
All routes live under /v1/content/classrooms/{classroomId}/simulations and take a tf_content_ key. Discover the classroom id with GET /v1/content/classrooms first.
What a simulation is
A model a learner perturbs. There is no winning, no losing, and no score; the learner moves controls and reads what changes. If you want something with a score, you want Games instead.
Resource operations
| Method | Path | Success response |
|---|---|---|
POST | /simulations | 201 with the created simulation |
GET | /simulations | 200 with a page of simulations |
GET | /simulations/{simulationId} | 200 with the simulation, including its built HTML |
PATCH | /simulations/{simulationId} | 200 with the updated simulation |
DELETE | /simulations/{simulationId} | 200 with the deleted simulation |
POST | /simulations/{simulationId}/versions/{version}/restore | 200 with the restored simulation |
None of these consume AI Credits. POST /simulations and the restore action honour the Idempotency-Key header like the other content resources: a retry with the same key and body returns the response made the first time, and the same key with a different body returns 409.
The simulation object
Every read, create, update, and restore returns the same shape. content appears only on GET /simulations/{simulationId}.
| Field | Notes |
|---|---|
id | Use as {simulationId} on later calls. |
classroomId | The classroom the simulation belongs to. |
title, description | Display text. description may be null. |
visibility | PRIVATE or PUBLIC. A public simulation opens at its play link without an account. |
subject | Chosen by the planner, not by you: computer-science, ai-ml, finance, business, math, science, or other. other until a brief exists. |
representation | How the built page draws itself, settled at brief time: 2d-canvas for a plain canvas, or 3d-three when the third dimension is the lesson. A 3D build costs more; see Generation. |
slug | A short label TutorFlow assigns. It is not part of any link. |
metadata | An object you own. The keys listed under Metadata that shapes the plan feed the brief; anything else is stored untouched. |
brief | The plan the build is generated from. null until a brief has been generated or supplied. |
contentKey | TutorFlow's internal storage reference for the served version. Opaque; null until the first build. |
contentVersion | The version currently served. 0 until the first build, then increments on every build, revision, tuning, and restore. |
buildHistory | One entry per version: version, contentKey, createdAt, source, an optional note holding the feedback a revision was asked for, and restoredFromVersion on a restore. source is build, revision, restore, or tuning. |
thumbnail | A storage reference for a capture of the opening frame, or null. Treat it as opaque, like contentKey. |
content | The built simulation as one complete HTML document, or null before the first build. Only on GET /simulations/{simulationId}. |
createdAt, updatedAt | Timestamps for incremental sync. |
A tuning entry records a change made in the TutorFlow editor to a control's range, step, or starting value. The document was patched in place rather than regenerated, so it cost no credits. There is no Content API call that produces one; you will only see it on simulations an educator has also worked on.
content is the whole page and can be large. GET /simulations never includes it. List responses carry metadata only, so paging through a classroom stays cheap.
The brief
brief is the most useful thing to read if you want to know what a simulation actually does without parsing its HTML, and the place to catch a wrong model before paying for a build. A generated brief has these fields:
| Field | Notes |
|---|---|
title, pitch, eyebrow | The title, one sentence for the learner, and a two-to-four-word label shown above the title. |
facts | Two to four label and value pairs about the model: its parts, its rule, its scale. |
status | A short footer line naming what kind of model is running. |
subject | One of the subject values above. |
model | What the simulation computes: the state it tracks and the rules that advance it. |
controls | One to five entries of label, kind (slider, toggle, choice, or number), range, effect, and section. |
observations | One to four things the learner watches to read the result. |
inquiry | Two to four questions the learner is meant to answer by moving the controls. |
You can replace the brief with PATCH /simulations/{simulationId} and a brief object, so an integration that already knows the model it wants can skip the priced brief call.
Creating a simulation
title and visibility are required. Keep new simulations PRIVATE until a person has opened the build.
curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/simulations" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: simulation:compound-interest:v1" \
-d '{
"title": "Compound interest",
"description": "See how rate and time change a balance.",
"visibility": "PRIVATE",
"metadata": {
"topic": "Compound interest on a savings balance",
"audience": "First-year finance",
"objectiveFocus": "Why doubling the time matters more than doubling the rate",
"theme": "auto",
"language": "en",
"externalId": "simulation:compound-interest"
}
}'Metadata that shapes the plan
The admin UI's create screen stores its answers on metadata, and the brief reads the same keys. Set them on create or with a later PATCH. All are optional.
| Key | Values | Notes |
|---|---|---|
topic | Free text | The principle the learner should work out. Falls back to title. |
audience | Free text | Who the simulation is for. |
objectiveFocus | Free text | What the learner should end up understanding, in your words. |
theme | auto, studio, or exhibit | How the page is lit. studio is light, exhibit is a dark stage. auto or anything else lets the plan choose. A learner can flip it in the player without a rebuild. |
language | Language name or code | Language of on-screen text. Set it; the model does not infer it from your key. |
referenceMarkdown | Markdown | Your own material to ground the plan. POST /brief can override it per call. |
PATCH merges metadata: keys you send replace their old values, and keys you leave out stay as they were.
Listing simulations
GET /v1/content/classrooms/{classroomId}/simulations?page=1&limit=20&order=DESC&field=updatedAt&subject=finance| Parameter | Default | Notes |
|---|---|---|
page | 1 | 1-based. |
limit | 10 | Up to 5000. |
order | DESC | ASC or DESC. |
field | title | Sort column: title, createdAt, updatedAt, or subject. Anything else returns 422. |
search | Case-insensitive match against title and description. | |
subject | Filter to one subject value. |
The response is { "items": [...], "totalCount": n }, with the same fields as the object above minus content.
Updating a simulation
PATCH /simulations/{simulationId} is partial. Fields omitted keep their values. It accepts title, description, visibility, brief, and metadata. It cannot set subject, representation, contentKey, or contentVersion; those belong to the planner and the build pipeline.
Generation
Building a simulation is three steps, and each is its own call. They are separate because the plan is cheap and the build is not: reading a plan and rejecting it should not cost what building it costs.
| Method | Path | Credits | Requires |
|---|---|---|---|
POST | /simulations/{simulationId}/brief | 1 | A simulation. |
POST | /simulations/{simulationId}/build | 15 for a 2D plan, 20 for 3D | A brief. |
POST | /simulations/{simulationId}/revise | 15 for a 2D build, 20 for 3D | A built version. |
These are the same prices the admin UI pays, drawn from the organization's AI Credit balance. Check the balance with GET /v1/content/credits before a batch. A credit is charged only after the new version is stored; a build that fails costs nothing.
The planner decides between 2D and 3D when it writes the brief, so the build price is not known until the brief exists. Read representation on the simulation after brief-done: 2d-canvas builds and revises at 15, 3d-three at 20. A 3D page loads a three.js runtime, which is why it costs more to produce.
POST /simulations/{simulationId}/brief accepts an optional referenceMarkdown body field to ground the plan in your own material. The brief also settles subject and representation on the simulation. POST /simulations/{simulationId}/build takes no body. POST /simulations/{simulationId}/revise takes a feedback field of 1 to 2000 characters describing the change in plain language, the same way the chat panel in the editor does. The feedback is kept, truncated to 200 characters, as the note on the resulting history entry.
These three stream
The generation routes respond with text/event-stream, not a JSON body. Read them as Server-Sent Events. Each event has an event name and a JSON data line, and the response ends after the terminal event.
event: progress
data: {"stage":"writing"}
event: progress
data: {"stage":"checking"}
event: build-done
data: {"contentKey":"classrooms/.../content/2.html","contentVersion":2,"thumbnail":"classrooms/.../thumbnail/2.png"}| Event | Sent by | data |
|---|---|---|
progress | build, revise | { "stage": "writing" | "checking" | "fixing" | "publishing" }. fixing means a version failed the checks and is being rebuilt. |
brief-done | brief | { "brief": { ... } }, the plan that was stored on the simulation. |
build-done | build, revise | { "contentKey", "contentVersion", "thumbnail" } for the version that is now served. Call GET /simulations/{simulationId} for the HTML. |
error | any | { "message", "violations"? }. Nothing was stored and nothing was charged. violations lists the checks the last attempt failed. |
Exactly one of brief-done, build-done, or error ends a stream.
Errors before and during the stream
The classroom, the simulation, its readiness, and the credit balance are all checked before the stream opens. Those failures come back as the normal JSON error envelope, not as events:
| Status | error.code | When |
|---|---|---|
400 | content_invalid_request | Building without a brief, or revising a simulation that has not been built. |
402 | content_payment_required | The organization does not have enough available AI Credits for this step. |
402 | content_payment_failed | The organization has a failed payment. |
404 | content_not_found | The classroom or simulation was not found. |
Once the stream has opened, the HTTP status is 200 whatever happens next, and a failure arrives as an error event.
Disconnecting aborts the work. If your client hangs up mid-build, the model call stops rather than running to completion unread, and nothing is charged. That also means a client timeout shorter than a build will cancel the build. Builds take minutes, not seconds, so set the timeout on these three calls with the build duration in mind rather than reusing the default you use for CRUD.
These three routes do not take Idempotency-Key. Retrying a build that already streamed build-done builds and charges again.
Restoring a version
POST /v1/content/classrooms/{classroomId}/simulations/{simulationId}/versions/{version}/restore
Idempotency-Key: simulation:{simulationId}:restore:3:v1{version} is a version number from buildHistory. Restoring runs no model and charges nothing. It copies the chosen version forward as the newest one rather than rewinding, so a restore is itself reversible and nothing an educator made is lost. The response is the simulation with contentVersion incremented and a new history entry whose source is restore and whose restoredFromVersion names the version you chose.
An unknown version returns 404 with content_not_found. Send an Idempotency-Key: without one, a retried restore appends a second identical version.
Sharing and deleting
Set visibility to PUBLIC with a PATCH when the simulation is ready to share. It then opens at:
https://tutorflow.io/simulations/play/{simulationId}That link needs no account. While the simulation is PRIVATE the link returns 404 to everyone, including its own classroom.
DELETE /simulations/{simulationId} soft-deletes the simulation and returns it. Later reads return 404 with content_not_found, and the play link stops working.
A typical run
POST /simulationswith a title,visibility: "PRIVATE", and themetadatakeys above. Keep the returnedid.POST /simulations/{simulationId}/brief. Read the plan from thebrief-doneevent.- Inspect
brief, especiallymodelandcontrols. If the model is wrong,PATCHthemetadataorbrief, or ask for another brief. This is the cheap point to change your mind, and a simulation that computes the wrong thing is the failure this content type is most prone to. POST /simulations/{simulationId}/build. Wait forbuild-done.GET /simulations/{simulationId}to read the built document and its version history.POST /simulations/{simulationId}/revisewith feedback if something needs changing, or restore an earlier version if the revision made it worse.PATCH /simulations/{simulationId}withvisibility: "PUBLIC"once a person has opened it, and hand out its link.
What the build guarantees
Every generated version is checked before it is stored. Beyond validating the page, TutorFlow runs the simulation and drives each declared control from one end of its range to the other, watching whether the drawing actually changes. A version whose sliders move and whose picture does not is rebuilt rather than returned, because that failure passes every static rule and is useless in a classroom. If the rebuild fails too, the stream ends with an error event that lists the violations, and nothing is charged.
That check confirms the simulation responds. It does not check that the arithmetic is right, and it does not judge whether the subject or the difficulty suits a particular class. An integration that publishes automatically is publishing something no person has opened. Read the brief at minimum, and open the built document before it reaches learners.