Гарын авлага
Games over the Content API

Games over the Content API

Create, plan, build, revise, restore, and share classroom games from an external system. Same endpoints as the admin UI uses, and the same credit cost.

Games 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}/games and take a tf_content_ key. Discover the classroom id with GET /v1/content/classrooms first.

Resource operations

MethodPathSuccess response
POST/games201 with the created game
GET/games200 with a page of games
GET/games/{gameId}200 with the game, including its built HTML
PATCH/games/{gameId}200 with the updated game
DELETE/games/{gameId}200 with the deleted game
POST/games/{gameId}/versions/{version}/restore200 with the restored game

None of these consume AI Credits. POST /games 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 game object

Every read, create, update, and restore returns the same shape. content appears only on GET /games/{gameId}.

FieldNotes
idUse as {gameId} on later calls.
classroomIdThe classroom the game belongs to.
title, descriptionDisplay text. description may be null.
visibilityPRIVATE or PUBLIC. A public game opens at its play link without an account.
slugA short label TutorFlow assigns. It is not part of any link.
metadataAn object you own. The keys listed under Metadata that shapes the plan feed the brief; anything else is stored untouched.
briefThe plan the build is generated from. null until a brief has been generated or supplied.
contentKeyTutorFlow's internal storage reference for the served version. Opaque; null until the first build.
contentVersionThe version currently served. 0 until the first build, then increments on every build, revision, and restore.
buildHistoryOne entry per version: version, contentKey, createdAt, source (build, revision, or restore), an optional note holding the feedback a revision was asked for, and restoredFromVersion on a restore.
thumbnailA storage reference for a capture of the opening screen, or null. Treat it as opaque, like contentKey.
contentThe built game as one complete HTML document, or null before the first build. Only on GET /games/{gameId}.
createdAt, updatedAtTimestamps for incremental sync.

A game is a self-contained page, so content is the whole thing and can be large. GET /games never includes it. List responses carry metadata only, so paging through a classroom stays cheap. Fetch the content for the one game you actually need.

The brief

brief is what the build reads, and the most useful thing to inspect before spending credits. A generated brief has these fields:

FieldNotes
titleShort, concrete title for the game.
pitchOne sentence on what playing it is like.
coreLoopThe action the player repeats.
mechanicsTwo to five rules or devices the game is built from.
levelsTwo to six entries of name, goal, and twist.
winCondition, failConditionWhat ends a run either way.
controlsHow it is played, by keyboard and by touch.

You can replace the brief with PATCH /games/{gameId} and a brief object, so an integration that already knows the plan it wants can skip the priced brief call.

Creating a game

title and visibility are required. Keep new games PRIVATE until a person has played the build.

curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/games" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: game:fractions-run:v1" \
  -d '{
    "title": "Fractions run",
    "description": "Practise comparing fractions under time pressure.",
    "visibility": "PRIVATE",
    "metadata": {
      "topic": "Comparing fractions with unlike denominators",
      "audience": "Grade 5",
      "objectiveFocus": "Decide which of two fractions is larger",
      "playLength": "short",
      "language": "en",
      "externalId": "game:fractions-run"
    }
  }'

Metadata that shapes the plan

The admin UI's create wizard stores its answers on metadata, and the brief reads the same keys. Set them on create or with a later PATCH. All are optional.

KeyValuesNotes
topicFree textWhat the game teaches. Falls back to title.
audienceFree textWho plays it.
objectiveFocusFree textThe skill a round should exercise.
mechanicFree textA mechanic to build around, if you have one in mind.
dimension2d or 3dDefaults to 2d.
persistencesession or localWhether progress survives a reload. Defaults to session.
playLengthshort, standard, or deepDefaults to standard.
visualStyleFree textArt direction for the page.
languageLanguage name or codeLanguage of on-screen text. Set it; the model does not infer it from your key.
referenceMarkdownMarkdownYour 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 games

GET /v1/content/classrooms/{classroomId}/games?page=1&limit=20&order=DESC&field=updatedAt&search=fraction
ParameterDefaultNotes
page11-based.
limit10Up to 5000.
orderDESCASC or DESC.
fieldtitleSort column: title, createdAt, or updatedAt. Anything else returns 422.
searchCase-insensitive match against title and description.

Response:

{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000501",
      "title": "Fractions run",
      "visibility": "PRIVATE",
      "contentVersion": 2,
      "updatedAt": "2026-09-09T00:00:00.000Z"
    }
  ],
  "totalCount": 1
}

Updating a game

PATCH /games/{gameId} is partial. Fields omitted keep their values. It accepts title, description, visibility, brief, thumbnail, and metadata. It cannot set contentKey or contentVersion; those belong to the build pipeline.

Generation

Building a game 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.

MethodPathCreditsRequires
POST/games/{gameId}/brief1A game.
POST/games/{gameId}/build20A brief.
POST/games/{gameId}/revise20A 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.

POST /games/{gameId}/brief accepts an optional referenceMarkdown body field to ground the plan in your own material. POST /games/{gameId}/build takes no body. POST /games/{gameId}/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"}
EventSent bydata
progressbuild, revise{ "stage": "writing" | "checking" | "fixing" | "publishing" }. fixing means a version failed the checks and is being rebuilt.
brief-donebrief{ "brief": { ... } }, the plan that was stored on the game.
build-donebuild, revise{ "contentKey", "contentVersion", "thumbnail" } for the version that is now served. Call GET /games/{gameId} for the HTML.
errorany{ "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 game, 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:

Statuserror.codeWhen
400content_invalid_requestBuilding without a brief, or revising a game that has not been built.
402content_payment_requiredThe organization does not have enough available AI Credits for this step.
402content_payment_failedThe organization has a failed payment.
404content_not_foundThe classroom or game 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}/games/{gameId}/versions/{version}/restore
Idempotency-Key: game:{gameId}: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 game 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 game is ready to share. It then opens at:

https://tutorflow.io/games/play/{gameId}

That link needs no account. While the game is PRIVATE the link returns 404 to everyone, including its own classroom.

DELETE /games/{gameId} soft-deletes the game and returns it. Later reads return 404 with content_not_found, and the play link stops working.

A typical run

  1. POST /games with a title, visibility: "PRIVATE", and the metadata keys above. Keep the returned id.
  2. POST /games/{gameId}/brief. Read the plan from the brief-done event.
  3. Inspect the plan. If it is wrong, PATCH the game's metadata or brief, or ask for another brief. This is the cheap point to change your mind.
  4. POST /games/{gameId}/build. Wait for build-done.
  5. GET /games/{gameId} to read the built game and its version history.
  6. POST /games/{gameId}/revise with feedback if something needs changing, or restore an earlier version if the revision made it worse.
  7. PATCH /games/{gameId} with visibility: "PUBLIC" once a person has played it, and hand out its link.

What the build guarantees

Every generated version is checked twice before it is stored: an automated validation of the page, then a playthrough that opens the game and works the controls. A version that will not start, or that freezes partway, is rebuilt rather than returned. If the rebuild fails too, the stream ends with an error event that lists the violations, and nothing is charged.

Those checks confirm the game runs. They judge nothing about whether it suits a particular class, so an integration that publishes automatically is publishing something no person has played. See Games Overview for what the checks do and do not cover.