Resources
Content API Reference

Content API Reference

Every Content API route with its authentication, scope, and success response, plus the OpenAPI 3.1 description for Postman, Insomnia, and Bruno.

On this page

This is the complete list of Content API routes. Other pages link here rather than repeating it. Base URL: https://api.tutorflow.io.

OpenAPI description

An OpenAPI 3.1 description of the Content API is published at:

https://tutorflow.io/resources/integrations/openapi.json

Import it to get every route with its parameters and schemas:

  • Postman: File > Import, paste the URL, and choose to generate a collection.
  • Insomnia: Create > Import, choose From URL, and paste the URL.
  • Bruno: Import Collection, choose OpenAPI V3 Spec, and give it the downloaded file or the URL.

Then set a collection variable for the bearer token to your tf_content_ key and the base URL to https://api.tutorflow.io. It also works with code generators such as openapi-generator and openapi-typescript.

Authentication

AuthHeaderUsed by
Content API keyAuthorization: Bearer tf_content_...Every route below marked with a scope. Test keys (tf_content_test_...) work only in the sandbox classroom; see Test Mode.
Admin sessionCookie: jwt=...Key management, the organization list, admin webhook routes, and the legacy export. See Create a key with the API.

Key routes and the admin session routes for keys, webhooks, and the legacy export answer errors in the Content API envelope. Only the organization list, GET /v1/content/organizations, answers in TutorFlow's standard web shape, { "statusCode", "message", "error" }.

A route whose scope is not named here uses content:read for GET and content:write for other methods. The learner routes need the opt-in learners:read or learners:write instead. A {classroomId} outside the key's allowlist returns 403 content_classroom_not_allowed before the route runs. Every id path parameter ({classroomId}, {moduleId}, {keyId}, and so on) must be a UUID; anything else returns 422 content_invalid_request. Prices are in Pricing.

Every resource list (GET on /modules, /courses, /videos, /slides, /tests, /games, /simulations) takes page, limit, field, order, search, and updatedSince; see List pages.

Response headers

HeaderOnMeaning
Request-IdEvery responseThe id TutorFlow files this request under. Also error.requestId on errors. Send X-Request-Id to choose it. See Request ids.
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-ResetKey requests counted against the keyThe key's budget. See Rate limits.
Retry-After429Seconds to wait.
Idempotent-ReplayedA replayed idempotent responsetrue when the response is the stored answer to an earlier request with the same Idempotency-Key. Absent on first answers.
Deprecation, Sunset, LinkResponses that carry deprecated fieldsSee Deprecation headers.
Location, Preference-AppliedAsync generation 202The run status URL, and respond-async (game and simulation steps only).
ETagSingle-resource reads and writesThe resource's version. See Concurrency and ETags.
Cache-Control: no-storeLearner routesLearner responses must not be cached.

Browser clients can read all of these except Location, Preference-Applied, and Cache-Control: the Content API lists them in Access-Control-Expose-Headers.

Request headers

HeaderOnMeaning
AuthorizationEvery routeBearer tf_content_...
Idempotency-KeyCreates and POST actions marked belowReplays the first answer to a retry. See Idempotency.
X-Request-IdAnyChooses the Request-Id.
If-None-MatchSingle-resource GET304 when the tag still matches. With fetch, also send Cache-Control: max-age=0.
If-MatchSingle-resource PATCH and DELETEWrite only if the tag still matches, otherwise 412.
Prefer: respond-asyncGame and simulation brief, build, reviseAnswer 202 instead of streaming.

Account

MethodPathScopeSuccess
GET/v1/content/creditscontent:read200 with the credit balance
GET/v1/content/classroomscontent:read200 with an array of { "id", "name", "slug", "locale", "isSandbox" }. A test key lists only the sandbox.
GET/v1/content/credits/historycontent:read200 with a cursor page of credit rows. See Credit history.
GET/v1/content/audit-logcontent:read200 with a cursor page of audit rows. See Audit log.

Expansion

MethodPathScopeSuccess
POST/v1/content/integrations/expansionscontent:generate202 with the job. Takes Idempotency-Key or body idempotencyKey; a replay answers 202 with the job's current state.
GET/v1/content/integrations/expansions/{jobId}content:read200 with the job
GET/v1/content/integrations/expansions/{jobId}/resultcontent:read200 with { "job", "outputs", "manifest" }

See Source JSON Expansion.

Modules

Under /v1/content/classrooms/{classroomId}. See Modules.

MethodPathScopeSuccess
POST/modulescontent:write201 with the module. Takes Idempotency-Key.
GET/modulescontent:read200 with a list
GET/modules/{moduleId}content:read200 with the module, plus pdfUrl, lecture, quizzes
PATCH/modules/{moduleId}content:write200 with the module, as the single read returns it, plus deprecated success: true
DELETE/modules/{moduleId}content:write200 with { "id", "deleted": true, "success": true }
POST/modules/{moduleId}/copy-to-coursecontent:write200 with { "lessonId", "courseLessonId", "message" }. Takes Idempotency-Key.
POST/modules/generatecontent:generate202 with a generation run. Takes Idempotency-Key. See Generation runs.

Courses

Under /v1/content/classrooms/{classroomId}. See Courses.

MethodPathScopeSuccess
POST/coursescontent:write201 with the course. Takes Idempotency-Key.
GET/coursescontent:read200 with a list
GET/courses/{courseId}content:read200 with the course; 404 if it does not exist. With isIncludeStats=true, learner names need learners:read; days other than 7 or 30 returns 422.
PATCH/courses/{courseId}content:write200 with the course. Only the fields sent change.
DELETE/courses/{courseId}content:write200 with { "id", "deleted": true, "success": true }, and chatSessionId when there is one
POST/courses/generatecontent:generate202 with a generation run. Takes Idempotency-Key.
GET/courses/{courseId}/chapterscontent:read200 with { "data": [chapter], "unassignedLessons": [lesson] }
POST/courses/{courseId}/chapterscontent:write201 with the chapter. Takes Idempotency-Key.
PATCH/courses/{courseId}/chapters/reordercontent:write200 with the whole curriculum
PATCH/courses/{courseId}/chapters/{chapterId}content:write200 with the chapter
DELETE/courses/{courseId}/chapters/{chapterId}content:write200 with { "id", "deleted": true, "success": true }. Deletes its lessons.
POST/courses/{courseId}/chapters/{chapterId}/lessonscontent:write201 with the lesson. Takes Idempotency-Key.
PATCH/courses/{courseId}/chapters/{chapterId}/lessons/reordercontent:write200 with the chapter
GET/courses/{courseId}/lessons/{lessonId}content:read200 with the lesson
PATCH/courses/{courseId}/lessons/{lessonId}content:write200 with the lesson
DELETE/courses/{courseId}/lessons/{lessonId}content:write200 with { "id", "deleted": true, "success": true }

Videos

Under /v1/content/classrooms/{classroomId}. See Videos and Video Rendering.

MethodPathScopeSuccess
POST/videoscontent:write201 with the video, with no scenes. Takes Idempotency-Key. The TutorFlow workspace lists it once it has a scene.
GET/videoscontent:read200 with a list
GET/videos/{videoId}content:read200 with the video
PATCH/videos/{videoId}content:write200 with the video
DELETE/videos/{videoId}content:write200 with { "id", "deleted": true, "success": true }
POST/videos/{videoId}/scenescontent:write201 with the video. Takes Idempotency-Key.
PATCH/videos/{videoId}/scenes/reordercontent:write200 with the video
PATCH/videos/{videoId}/scenes/{sceneId}content:write200 with the video
DELETE/videos/{videoId}/scenes/{sceneId}content:write200 with the video
POST/videos/{videoId}/narrationcontent:generate200 with { "videoId", "narratedSceneIds", "creditsCharged" }. Narrates scenes that have none. Takes Idempotency-Key.
POST/videos/{videoId}/scenes/{sceneId}/narrationcontent:generate200 with the same shape. Replaces one scene's narration. Takes Idempotency-Key.
POST/videos/{videoId}/rendercontent:generate202 with { "status", "renderStatus", "estimatedSeconds", "totalDurationSeconds", "narratedSceneIds", "narrationCreditsCharged" }. Body { "generateMissingNarration"?: boolean }. Takes Idempotency-Key.
GET/videos/{videoId}/rendercontent:read200 with the render status
POST/videos/{videoId}/render/cancelcontent:generate200 with { "status": "ok" }

Slides

Under /v1/content/classrooms/{classroomId}. See Slides.

MethodPathScopeSuccess
POST/slidescontent:write201 with the deck. Takes Idempotency-Key.
GET/slidescontent:read200 with a list
GET/slides/{slideId}content:read200 with the deck, including content
PATCH/slides/{slideId}content:write200 with the deck, including content, plus deprecated success: true
DELETE/slides/{slideId}content:write200 with { "id", "deleted": true, "success": true }
POST/slides/generatecontent:generate202 with a generation run. Takes Idempotency-Key.

Tests

Under /v1/content/classrooms/{classroomId}. See Tests.

MethodPathScopeSuccess
POST/testscontent:write201 with the test and its items. Takes Idempotency-Key.
GET/testscontent:read200 with a list
GET/tests/{testId}content:read200 with the test and its items
PATCH/tests/{testId}content:write200 with the test and its items, plus deprecated success: true
DELETE/tests/{testId}content:write200 with { "id", "deleted": true, "success": true }
POST/tests/generatecontent:generate202 with a generation run. Takes Idempotency-Key.

Generation runs

Under /v1/content/classrooms/{classroomId}. See Generate Tests, Modules, Courses, and Slides.

MethodPathScopeSuccess
POST/tests/generate, /modules/generate, /courses/generate, /slides/generatecontent:generate202 with { "runId", "status", "kind", "estimatedCredits", "statusUrl" } and a Location header. Takes Idempotency-Key.
GET/runscontent:read200 with a cursor page of runs. Filters kind and status.
GET/runs/{runId}content:read200 with the run
POST/runs/{runId}/resumecontent:generate202 with a new run that continues a failed course run. Takes Idempotency-Key. See Resume a failed course run.

Assets

Under /v1/content/classrooms/{classroomId}. See Asset Uploads.

MethodPathScopeSuccess
POST/assetscontent:write201 with { "id", "purpose", "status", "assetKey", "uploadUrl", "method", "headers", "expiresAt" }. Takes Idempotency-Key.
POST/assets/{assetId}/completecontent:write200 with the upload, status: "ready"

Learners

Under /v1/content/classrooms/{classroomId}. See Learners. Every response carries Cache-Control: no-store.

MethodPathScopeSuccess
GET/learnerslearners:read200 with a cursor page of learners. Filters status and q.
GET/learners/{learnerId}learners:read200 with the learner
GET/learners/{learnerId}/progresslearners:read200 with { "learnerId", "data" }. courseId adds lessons.
POST/learners/invitationslearners:write201 with { "status", "learnerId", "invitation" }. Takes Idempotency-Key.
DELETE/learners/{learnerId}learners:write200 with { "id", "deleted": true }
GET/courses/{courseId}/enrollmentslearners:read200 with a cursor page of enrollments
POST/courses/{courseId}/enrollmentslearners:write201 with { "data": [enrollment] }. Takes Idempotency-Key.
DELETE/courses/{courseId}/enrollments/{learnerId}learners:write200 with { "learnerId", "courseId", "deleted": true }
GET/tests/{testId}/resultslearners:read200 with a cursor page of results. Filter learnerId.
GET/tests/{testId}/results/{resultId}learners:read200 with the result and items. include=answers adds answers.

Games

Under /v1/content/classrooms/{classroomId}. See Games and Generation.

MethodPathScopeSuccess
POST/gamescontent:write201 with the game. Takes Idempotency-Key.
GET/gamescontent:read200 with a list
GET/games/{gameId}content:read200 with the game, including content
PATCH/games/{gameId}content:write200 with the game
DELETE/games/{gameId}content:write200 with { "id", "deleted": true, "success": true }
POST/games/{gameId}/versions/{version}/restorecontent:write200 with the game. Takes Idempotency-Key.
POST/games/{gameId}/briefcontent:generateStream, or 202 in async mode
POST/games/{gameId}/buildcontent:generateStream, or 202 in async mode
POST/games/{gameId}/revisecontent:generateStream, or 202 in async mode
GET/games/{gameId}/runcontent:read200 with { "active", "last" }, for briefs, builds, and revisions

Simulations

Under /v1/content/classrooms/{classroomId}. See Simulations and Generation.

MethodPathScopeSuccess
POST/simulationscontent:write200 with the simulation. Takes Idempotency-Key.
GET/simulationscontent:read200 with a list
GET/simulations/{simulationId}content:read200 with the simulation, including content
PATCH/simulations/{simulationId}content:write200 with the simulation
DELETE/simulations/{simulationId}content:write200 with { "id", "deleted": true, "success": true }
POST/simulations/{simulationId}/versions/{version}/restorecontent:write200 with the simulation. Takes Idempotency-Key.
POST/simulations/{simulationId}/briefcontent:generateStream, or 202 in async mode
POST/simulations/{simulationId}/buildcontent:generateStream, or 202 in async mode
POST/simulations/{simulationId}/revisecontent:generateStream, or 202 in async mode
GET/simulations/{simulationId}/runcontent:read200 with { "active", "last" }, for briefs, builds, and revisions

Webhooks

With a key, under /v1/content/webhooks, every route needs webhooks:manage. With an admin session, the same routes are under /v1/content/organizations/{organizationId}/webhooks. See Webhooks.

MethodPathSuccess
GET/200 with an array of endpoints
POST/201 with the endpoint and its secret
GET/{webhookId}200 with the endpoint
PATCH/{webhookId}200 with the endpoint and replayedDeliveries. Takes replayFailedSince with status: "ACTIVE".
POST/{webhookId}/rotate-secret200 with the endpoint and its new secret
POST/{webhookId}/test200 with the delivery
GET/{webhookId}/deliveries200 with an array of deliveries
POST/{webhookId}/deliveries/{deliveryId}/redeliver200 with the new delivery
DELETE/{webhookId}200 with { "id", "deleted": true }

Keys and organizations (admin session)

See Keys and Authentication.

MethodPathSuccess
GET/v1/content/organizations200 with an array of organizations you can manage
GET/v1/content/organizations/{organizationId}/api-keys200 with an array of key metadata, including monthlyCreditLimit and spentThisMonth
POST/v1/content/organizations/{organizationId}/api-keys201 with the key, including the one-time apiKey. mode: "test" makes a test key.
PATCH/v1/content/organizations/{organizationId}/api-keys/{keyId}200 with the key metadata. Changes name, scopes, classroomIds, rateLimitPerMinute, expiresAt, or monthlyCreditLimit in place; 409 content_api_key_not_active for a revoked or expired key.
POST/v1/content/organizations/{organizationId}/api-keys/{keyId}/rotate200 with the new key and previousKeyExpiresAt
POST/v1/content/organizations/{organizationId}/api-keys/{keyId}/revoke200 with no body
DELETE/v1/content/organizations/{organizationId}/api-keys/{keyId}200 with { "id", "deleted": true }. Removes a revoked or expired key from the list; 409 content_api_key_active for a key that still works.
GET/v1/content/organizations/{organizationId}/credits/history200 with the organization's credit rows, as the key route returns them
GET/v1/content/organizations/{organizationId}/audit-log200 with the organization's audit rows, as the key route returns them, plus actorName on admin rows

Legacy standalone lesson export (admin session)

HTTP
POST /v1/content/classrooms/{classroomId}/integrations/exports
Cookie: jwt=...
JSON
{ "lessonId": "00000000-0000-4000-8000-000000000101", "idempotencyKey": "export:00000000-0000-4000-8000-000000000101:v1" }

This route takes an admin session, not a tf_content_ key, and answers errors in the Content API envelope. It records an existing standalone lesson as a completed job and returns 200 with { "job", "outputs", "manifest" }. New integrations do not need it: read the same lesson with GET /modules/{moduleId} and a key.

Status codes

CodeMeaning
200Read, update, delete, action (including a module copied to a course and a restored game or simulation version), or an opened generation stream (whatever event ends it).
201Created: a resource, chapter, lesson, scene, upload URL, learner invitation, or enrollments.
202Accepted: an expansion job, a video render, async game or simulation generation, or a test, module, course, or slide deck generation run.
304Not modified: the If-None-Match tag still matches. No body.
400The request is well formed but not allowed in the current state.
401Missing or invalid key, or no admin session on a session route.
402Not enough AI Credits, a failed payment, the key's monthly credit limit reached, or the plan's learner limit reached.
403Missing scope, classroom not allowed, or not permitted.
404Not found, or deleted.
409Idempotency or state conflict, content_api_key_not_active when changing a revoked or expired key, or content_asset_not_uploaded.
412If-Match did not match the current version (content_precondition_failed).
413Request body over 100 KB.
422Path, body, or query validation failed, including an id that is not a UUID; error.details names each field.
429Rate limit exceeded, too many failed authentications from the address, or the organization's daily upload limit reached; wait for Retry-After.
500Unexpected server error.

Every code, error.code, and the action to take is in Errors.

Was this page helpful?