Every change to /v1/content/** that an integration can observe is listed here, newest first. How additive and breaking changes are handled, and when deprecated fields stop being sent, is described in Versioning and Deprecation.
2026-10-01 (later)
A second release on the same day: resuming failed course runs, more exact generation charges, and safer expansion manifests. Read Check your integration if you read manifest.raw from expansion results.
Check your integration
- Expansion
manifest.rawis a short summary. It used to be the whole created entity, including internal columns and dates serialized as{}. It now holds only whichever ofid,title,name,description,type,status,slug,isPublic,visibility,classroomId,createdAt, andupdatedAtthe resource has, with dates as ISO 8601 strings. Read any other field from the resource route. The manifesttitleof anexpanded_quizoutput now comes from the test'sname(it wasnull). See Read the result. creditsChargedcomes from the credit history. On generation runs and*.generation.*webhooks it is now the run's charges minus its refunds, as recorded, so after a crash it can be lower than the step prices add up to. Every credit row a run writes has a note starting withContent API generation run {runId}. Runs started before this release keep the old count.- A build can be corrected from failed to completed. A game or simulation build reported failed after a restart that then finished saving in its last seconds is recorded as
done. If*.build.failedwas already sent,*.build.completedfollows with the samerunId; treat the later event as the outcome. See Interrupted runs.
Added
- Resume a failed course run.
POST .../runs/{runId}/resumecontinues a failed course run on the same course and charges only the lessons left (3 credits each). Runs carryresumedFromRunId. New error codescontent_run_not_resumableandcontent_run_already_resumed(409). See Resume a failed course run. - Course single read fields.
GET .../courses/{courseId}now also returnscontentTypeandisPublic, as the list does.
Changed
- Time limits are enforced by the server doing the work. A generation run at its limit aborts the step in flight and fails once it has unwound; a lesson saved in that moment stays and is counted. If that server hangs, another fails the run up to five minutes after the limit.
- Deploys no longer fail saved builds. A build that saved its new version before a clean shutdown ends
completed, once. - Simulation builds charge before
build-done, as games do. A failed charge ends the stream witherroralone (402). - TutorFlow's editors send
If-Match. The course, module, video, slide, test, game, and simulation editors stop on412and ask the educator before overwriting, so a change made through the API is no longer silently overwritten by an editor that had an older version open. See Concurrency and ETags.
2026-10-01
Adds test mode, generation of tests, modules, courses, and slide decks, asset uploads, a learner API, ETags, per-key monthly credit limits, a credit history and audit log, and webhook endpoint health. Most integrations keep working unchanged; read Check your integration first, because seven changes can affect an existing one.
Check your integration
- Resource events from any source.
resource.*events are now sent for changes made anywhere, including the TutorFlow app's editors, and carry newsource,sequence,resourceUpdatedAt,changeCount, andsubresourcefields. Changes within about 2 seconds of each other arrive as one event, a few seconds after the change, so a create followed by quick edits is oneresource.created. Receivers that ignore unknown fields keep working; deduplicate byevent.idand order bysequence. See Resource events from any source. - Storage keys are checked against the classroom. Fields that hold a storage key (course, slide, test, and game
thumbnail, videothumbnailKeyandbgmAudioKey, scenevisualKeyand overlay keys, testitems[].questionAudioKey, and modulepdfKey,videoUrl, andlectureKey) now answer400content_invalid_requestfor a new key outside the calling classroom, for example "pdfKey must reference an asset stored in this classroom". Keys under the classroom or the organization, the shared music library, and externalhttpsURLs are accepted, and a value the resource already holds saves unchanged. On reads,pdfUrlandquestionAudioUrlarenullwhen a stored private key belongs to another classroom. See Keys in resource fields are checked. - Test item ids from another test answer
404. A testPATCHwhoseitems[].idbelongs to another test now answers404content_not_foundand saves nothing. Ids of this test's items, and new ids no item uses, work as before. - Webhooks are delivered in the background. Events now leave about a second after they are created, not during the request that caused them, one delivery at a time per endpoint, oldest first. After a failed attempt, an endpoint rests about 30 seconds and newer events wait behind the failed one. Signatures, headers, bodies, and the retry schedule are unchanged. See Delivery.
- Failing endpoints are turned off. An endpoint with at least 20 failed attempts in a row over at least 3 days is set to
DISABLEDwithdisabledReason: "repeated_failures", its waiting deliveries are markedFAILED, and the organization's admins get an email that links to the Webhooks tab of Settings > Content API. Turn it back on withPATCHstatus: "ACTIVE", and resend what failed withreplayFailedSince. See Endpoint health. content.failedis sent only after an expansion job's final attempt. Before, every failed attempt sent it, even when a later attempt succeeded. A job being tried again now staysprocessing. See Poll the job.ETagheaders carry the resource version. Single-resourceGETandPATCHroutes on courses (and their chapters and lessons), modules, videos (and their scenes), slides, tests, games, and simulations now answer with a strongETagsuch as"v12", which changes whenever the resource changes, in the API or in TutorFlow. Before, the web server sent a weak tag computed from the response body. Nothing changes unless you sendIf-MatchorIf-None-Match, but an HTTP client or proxy that caches onETagwill see the new tags. See Concurrency and ETags.
Added
- Test mode. Test keys (
tf_content_test_..., created with"mode": "test") work only in the organization's "API sandbox" classroom, never spend AI Credits, and answer generation, builds, narration, and renders with canned content, including a sample mp4. Key objects carrymodeandsandboxClassroomId, classrooms carryisSandbox, webhook endpoints carrymode, and test endpoints get only sandbox events. The learner API answers403content_sandbox_unsupportedto test keys. Sandbox uploads have their own allowance of 50 uploads and 500 MB a day. New error codescontent_sandbox_unsupportedandcontent_sandbox_spend_blocked. See Test Mode. livemodeon every webhook payload.truefor real content,falsefor sandbox events, next toevent. Ignore it until you use test mode; deliveries queued before this release do not carry it.- Test, module, course, and slide deck generation.
POST .../tests/generate,.../modules/generate,.../courses/generate, and.../slides/generatetake the subject intopicand answer202with a run, andGET .../runsandGET .../runs/{runId}follow it. Runs survive server restarts, are charged at the editor's prices (3 credits for a test or module, 3 plus 3 per lesson for a course, and the slide editor's page price for a deck, for example 16 credits for 10 pages, charged per batch of four pages), and end withtest.generation.*,module.generation.*,course.generation.*, orslide.generation.*webhooks. See Generate Tests, Modules, Courses, and Slides. - Asset uploads.
POST .../assetsreturns a presignedPUTURL and anassetKeyfor a purpose (thumbnail,scene-visual,overlay,bgm,test-audio,lesson-pdf,slide-file), andPOST .../assets/{assetId}/completeconfirms the file. An upload key goes only into the fields of its purpose. Up to 1,000 uploads and 20 GB per organization in 24 hours. New error codes:content_asset_type_not_allowed,content_asset_too_large,content_asset_extension_mismatch,content_asset_not_found,content_asset_not_uploaded,content_asset_mismatch, andcontent_asset_daily_limit_reached. See Asset Uploads. - Learner API. List a classroom's learners, read progress, enrollments, and test results, invite learners, enroll and unenroll them, and remove them. New opt-in scope
learners:write;learners:readnow covers the learner reads. New error codecontent_learner_limit_reached(402). New webhookslearner.enrolled,learner.course.completed, andlearner.test.submitted. See Learners. - ETags and conditional requests.
If-None-Matchanswers304when nothing changed, andIf-MatchonPATCHandDELETEanswers412content_precondition_failed, witherror.currentETag, when the resource changed, including changes an educator made in TutorFlow. Withfetch, sendCache-Control: max-age=0next toIf-None-Match. See Concurrency and ETags. - Per-key monthly credit limits. Keys accept
monthlyCreditLimit(1 to 10,000,000, ornull) on create and change, and the key list showsspentThisMonth. A priced call that would pass the limit answers402content_key_budget_exceededwithlimit,spent,requested, andresetsAt. See Monthly credit limit. - Credit history and audit log.
GET /v1/content/credits/historyandGET /v1/content/audit-log, with cursor paging, and the same under/v1/content/organizations/{organizationId}/for an admin session, where audit rows of admin changes also carryactorName. Credit rows made with a key name the key. See Credit History and Audit Log. - Endpoint health fields. Every webhook endpoint object carries
consecutiveFailures,failingSince,lastSuccessAt,disabledReason, anddisabledAt.PATCHacceptsreplayFailedSince(at most 7 days back, up to 10,000 events) and answers withreplayedDeliveries. - Durable game and simulation runs. Run status is stored, so
GET .../runanswers the same on every server. A run whose server stops ends as failed with a message that says so, and still sends its outcome webhook. See Interrupted runs.
Changed
- Learner scopes are opt-in for every key. A key with no scope list (
isLegacyFullAccess: true) holds every scope exceptlearners:readandlearners:write. It still receives learner names in course stats, as before. See Learner data. - A failed async run releases its
Idempotency-Key. For async game and simulation steps and the new generation routes, a retry with the same key after a*.failedwebhook starts a new run with a newrunId. Before, the failed run's202was replayed for 24 hours. See Idempotency in async mode. 409for a running build holds across servers. A second build of the same game or simulation is refused whichever server receives it.- Rotation copies the monthly credit limit. The new key starts the month with nothing spent.
- Webhook
status: "DISABLED"recordsdisabledReason: "manual"anddisabledAt. Endpoints disabled before this release showdisabledReason: "manual".
2026-09-30 (later)
A second release on the same day. Existing integrations keep working, with two exceptions to check: course PATCH no longer publishes a course, and lists now return 20 rows by default in createdAt DESC order.
Check your integration
- Course visibility. Before this release, a course
PATCHwithoutvisibilityset the course toPUBLIC. If your integration updated courses, check that their visibility is what you intend. - List defaults. Every resource list now defaults to 20 rows (was 10), sorted by
createdAt DESC(modules used to default totitle ASC, and slides, games, and simulations totitle DESC). Sendfieldandorderif you relied on the old order. - List search.
searchnow matches the title only (nameon tests), case-insensitively, on every list. Courses used to match title or slug case-sensitively; tests, name or slug; modules, slides, games, and simulations, title or description. - Malformed ids. An id path parameter that is not a UUID answers
422content_invalid_request(was500, or400on the key webhook routes).
Added
- Request ids. Every
/v1/content/**response carries aRequest-Idheader, and every error body carrieserror.requestId. This includes a body refused as too large (413content_payload_too_large) or as malformed JSON (400content_invalid_request), which now use the Content API error envelope too. SendX-Request-Idto choose the id. Quote it to support. See Request ids. Idempotent-Replayed: trueon every response that replays a stored answer for a repeatedIdempotency-Key. See Idempotency.- List parameters on every resource list.
limit(default 20; values above 100 are clamped to 100),fieldwith a per-list allowlist (422witherror.allowedValuesotherwise),orderwith anidtie-break, case-insensitive titlesearch, andupdatedSincefor incremental sync. Videos now accept all of them. See List pages and Syncing changes. - Key editing.
PATCH /v1/content/organizations/{organizationId}/api-keys/{keyId}changes a key's name, scopes, classrooms, rate limit, or expiry without changing its secret. A revoked or expired key answers409content_api_key_not_active. Settings > Content API can edit keys too, and shows the Request ID of a failed request. See Change a key. - Key status fields. The key list and the
PATCHanswer carryisLegacyFullAccessandissuerStatus(active,not_admin,unknown). See Key status notices. - Expiry warnings. 7 days before a key expires, TutorFlow emails the organization's admins and sends the new
api_key.expiringwebhook, once per expiry. See Expiry warnings and Key events. learners:readscope. Opt-in. Keys with an explicit scope list need it to receivestats.topLearnersandstats.mostBehindLearneron a course read withisIncludeStats=true; without it those two keys are left out and the aggregates stay. Keys without a scope list keep full access. It can be chosen in Settings and on key create and change. See Learner data.- Deprecation headers. Responses that carry deprecated aliases (lists, deletes, and module, slide, and test updates) send
Deprecation,Sunset, andLinkheaders. TutorFlow records which keys still receive deprecated fields and will contact those integrations before the sunset. See Deprecation headers. - Browser-readable headers.
Access-Control-Expose-HeaderslistsRequest-Id,X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After,Idempotent-Replayed,Deprecation,Sunset, andLink. - Failed authentication limit. An address that fails authentication 300 times in a minute gets
429content_rate_limit_exceededon everytf_content_route for the rest of that minute, before its key is looked up. Valid traffic is never counted. See Failed authentication limit. - Async briefs are tracked as runs. The
202of an async brief carries arunId(wasnull),GET .../runreports briefs withphase: "brief", and*.brief.completedand*.brief.failedwebhooks carry therunId. See Run status. - Audit log. Key and webhook changes made by an admin in Settings or with an admin session are recorded with the admin's user id, and every audit entry stores the
Request-Id. - Delete old keys.
DELETE /v1/content/organizations/{organizationId}/api-keys/{keyId}removes a revoked or expired key from the key list, and Settings > Content API has a Delete button for them. A key that still works answers409content_api_key_active. See Delete a key. - Rotation re-issues the key. A rotated key is issued by the admin who rotates it, not the original creator, so rotating a key whose
issuerStatusisnot_adminties it to an active admin again. - Session error code. A
401on an admin session route (key and webhook management, and the legacy export) now sayscontent_session_requiredinstead ofcontent_invalid_api_key, because those routes take a signed-in admin, not an API key. Routes that take a key are unchanged.
Changed
- Idempotency keys are scoped per API key. Two integrations in one organization can use the same key string without replaying each other's answers or getting
409. Keys first used before this release still match any key of the organization until they expire. - Expansion idempotency follows the shared rules. Same key and body within 24 hours:
202with the first job in its current state andIdempotent-Replayed: true. Same key with a different body:409content_conflict. Concurrent requests with one key: one202, one409. Keys are limited to 255 characters and scoped to the API key andclassroomId. Jobs created earlier under anidempotencyKeyare still returned for it. See Idempotency for expansion jobs. - Module, slide, and test
PATCHreturn the updated resource as the single read returns it, plussuccess: true(was only{ "success": true }). - Course
PATCHchanges only the fields sent.visibilityis validated (422otherwise),nullclearslevelorthumbnail, andsettings.aiTutorDefaultsandsettings.celebrationTriggerare saved. See Update a course. - Course stats
daysother than7or30answers422(was silently7).dailyLearningTimehas one bucket per UTC day. video.render.completedsignsvideoUrlagain for every delivery attempt and redelivery, withvideoUrlExpiresAt6 hours after that attempt, so late retries carry a working link.event.idis unchanged across attempts.- Error envelope on admin routes. The admin session routes for keys, webhooks, and the legacy export answer errors in the Content API envelope. The organization list keeps the standard web shape.
- Unparseable values anywhere in a request answer
422content_invalid_requestinstead of500.
Deprecated
Sunset 2027-04-30, as the aliases in the previous release. See Current deprecations.
successon module, slide, and test update responses. Read the resource fields.- The
takequery parameter on resource lists. Sendlimit.
Removed from the OpenAPI description
qandidswere documented on some lists but never applied. They are no longer described, and are still ignored if sent.
2026-09-30
Existing integrations keep working. This release adds capabilities and deprecates four response keys.
Added
- Narration for API videos.
POST .../videos/{videoId}/narrationnarrates every scene that has none from itsscript, andPOST .../videos/{videoId}/scenes/{sceneId}/narrationnarrates one scene (1 AI Credit per scene, the editor's price). The render request acceptsgenerateMissingNarration: trueto do this first. Scenes reporthasNarration. A render with scenes missing narration now returns400content_video_narration_missingwith theirsceneIds. Videos built through the Content API no longer need the TutorFlow editor before they can render. See Video Rendering. - Expansion jobs are free. Expansions create editable drafts without calling a model, so they no longer check or charge AI Credits (previously 3, 1, and 3 credits per output), and they no longer return
402.outputs[].creditAmountis always0. See Pricing. - Key scopes, classroom allowlists, and expiry. New keys can be limited to
content:read,content:write,content:generate, andwebhooks:manage, to a list of classrooms, and to an expiry time. Keys created earlier hold every scope. See Keys and Authentication. - Key rotation with a grace period.
POST .../api-keys/{keyId}/rotateacceptsgracePeriodHours(0 to 168) so the old key keeps working while a deployment switches over. - Settings page. Keys and webhooks can be created and managed in TutorFlow under Settings > Content API.
- Per-key rate limits with headers. Each key's
rateLimitPerMinuteis one budget per 60-second window across every route. Responses carryX-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset, and a429carriesRetry-Afteranderror.retryAfterSeconds. See Limits. - Audit log and ownership. Every change made with a key is recorded with the key id. Resources a key creates are owned by the admin who created the key while they remain an active admin, otherwise by the longest-standing active admin.
- Webhook events.
resource.created,resource.updated,resource.deleted,game.brief.*,game.build.*,simulation.brief.*,simulation.build.*,video.render.completed, andvideo.render.failed. - Webhook signatures and delivery. A timestamped
X-Content-Integration-Signature-V2header and anX-Content-Integration-Event-Idheader; the legacy signature is still sent. Up to 9 attempts over about 23 hours.307and308redirects are followed;301,302, and303are not. Webhook URLs must behttpson a public address. - Webhook management. Endpoints can be updated, paused, tested, and inspected through a delivery log, and deliveries can be sent again, with an admin session or a
webhooks:managekey. - Webhook secret rotation with a grace period.
rotate-secretacceptsgracePeriodHours(0 to 168). During the grace period the V2 header carries onev1=signature per secret. See Rotate the signing secret. - Async generation. Game and simulation brief, build, and revise accept
Prefer: respond-async(or?async=true) and answer202, withIdempotency-Keysupport and aGET .../runstatus route. Streaming is unchanged. See Generation. - Video rendering.
POST .../videos/{videoId}/render, a render status route with a signedvideoUrl, and cancel. See Video Rendering. - Course chapters and lessons. Chapters and text lessons can be listed, added, edited, moved, reordered, and deleted.
- Idempotency. Records replay for 24 hours, and a request stuck in progress for 5 minutes can be taken over by a retry.
- OpenAPI 3.1 description at
https://tutorflow.io/resources/integrations/openapi.json. See API Reference.
Changed
- Response contracts. Every response is an explicit list of public fields. Authoring tokens, video
videoKeyandmetadata.remotionLambda, scenettsAudioKeyandvideoClipKey, slidecontentKey, courseauthorId,chatSession, andsettings, moduleauthorId, and learner contact details in course stats are no longer returned. See Response fields. - Deletes. Every resource delete returns
200with{ "id", "deleted": true, "success": true }. Video delete used to return204with no body. - Lists. Resource lists return
{ "data", "meta" }. - Course read. A course that does not exist returns
404instead of200withnull. - Expansion scope. Creating an expansion job needs
content:generate. - Errors. A render already in progress returns
409content_video_render_in_progressinstead of restarting and charging again. Unexpected server errors always carry the message "An unexpected error occurred". Failed renders report a fixed public message instead of engine output.
Deprecated
These keys are still sent. They stop being sent on 2027-04-30, the final sunset date in Versioning and Deprecation.
itemsandtotalCounton module, course, slide, test, game, and simulation lists. Readdataandmeta.itemCount.totalon the video list. Readmeta.itemCount.successon delete responses. Readdeleted.