Ressourcen
Content API Changelog

Content API Changelog

Dated list of additions, changes, and deprecations in the TutorFlow Content API.

Auf dieser Seite

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.raw is a short summary. It used to be the whole created entity, including internal columns and dates serialized as {}. It now holds only whichever of id, title, name, description, type, status, slug, isPublic, visibility, classroomId, createdAt, and updatedAt the resource has, with dates as ISO 8601 strings. Read any other field from the resource route. The manifest title of an expanded_quiz output now comes from the test's name (it was null). See Read the result.
  • creditsCharged comes 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 with Content 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.failed was already sent, *.build.completed follows with the same runId; treat the later event as the outcome. See Interrupted runs.

Added

  • Resume a failed course run. POST .../runs/{runId}/resume continues a failed course run on the same course and charges only the lessons left (3 credits each). Runs carry resumedFromRunId. New error codes content_run_not_resumable and content_run_already_resumed (409). See Resume a failed course run.
  • Course single read fields. GET .../courses/{courseId} now also returns contentType and isPublic, 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 with error alone (402).
  • TutorFlow's editors send If-Match. The course, module, video, slide, test, game, and simulation editors stop on 412 and 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 new source, sequence, resourceUpdatedAt, changeCount, and subresource fields. 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 one resource.created. Receivers that ignore unknown fields keep working; deduplicate by event.id and order by sequence. 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, video thumbnailKey and bgmAudioKey, scene visualKey and overlay keys, test items[].questionAudioKey, and module pdfKey, videoUrl, and lectureKey) now answer 400 content_invalid_request for 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 external https URLs are accepted, and a value the resource already holds saves unchanged. On reads, pdfUrl and questionAudioUrl are null when a stored private key belongs to another classroom. See Keys in resource fields are checked.
  • Test item ids from another test answer 404. A test PATCH whose items[].id belongs to another test now answers 404 content_not_found and 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 DISABLED with disabledReason: "repeated_failures", its waiting deliveries are marked FAILED, and the organization's admins get an email that links to the Webhooks tab of Settings > Content API. Turn it back on with PATCH status: "ACTIVE", and resend what failed with replayFailedSince. See Endpoint health.
  • content.failed is 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 stays processing. See Poll the job.
  • ETag headers carry the resource version. Single-resource GET and PATCH routes on courses (and their chapters and lessons), modules, videos (and their scenes), slides, tests, games, and simulations now answer with a strong ETag such 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 send If-Match or If-None-Match, but an HTTP client or proxy that caches on ETag will 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 carry mode and sandboxClassroomId, classrooms carry isSandbox, webhook endpoints carry mode, and test endpoints get only sandbox events. The learner API answers 403 content_sandbox_unsupported to test keys. Sandbox uploads have their own allowance of 50 uploads and 500 MB a day. New error codes content_sandbox_unsupported and content_sandbox_spend_blocked. See Test Mode.
  • livemode on every webhook payload. true for real content, false for sandbox events, next to event. 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/generate take the subject in topic and answer 202 with a run, and GET .../runs and GET .../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 with test.generation.*, module.generation.*, course.generation.*, or slide.generation.* webhooks. See Generate Tests, Modules, Courses, and Slides.
  • Asset uploads. POST .../assets returns a presigned PUT URL and an assetKey for a purpose (thumbnail, scene-visual, overlay, bgm, test-audio, lesson-pdf, slide-file), and POST .../assets/{assetId}/complete confirms 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, and content_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:read now covers the learner reads. New error code content_learner_limit_reached (402). New webhooks learner.enrolled, learner.course.completed, and learner.test.submitted. See Learners.
  • ETags and conditional requests. If-None-Match answers 304 when nothing changed, and If-Match on PATCH and DELETE answers 412 content_precondition_failed, with error.currentETag, when the resource changed, including changes an educator made in TutorFlow. With fetch, send Cache-Control: max-age=0 next to If-None-Match. See Concurrency and ETags.
  • Per-key monthly credit limits. Keys accept monthlyCreditLimit (1 to 10,000,000, or null) on create and change, and the key list shows spentThisMonth. A priced call that would pass the limit answers 402 content_key_budget_exceeded with limit, spent, requested, and resetsAt. See Monthly credit limit.
  • Credit history and audit log. GET /v1/content/credits/history and GET /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 carry actorName. 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, and disabledAt. PATCH accepts replayFailedSince (at most 7 days back, up to 10,000 events) and answers with replayedDeliveries.
  • Durable game and simulation runs. Run status is stored, so GET .../run answers 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 except learners:read and learners: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 *.failed webhook starts a new run with a new runId. Before, the failed run's 202 was replayed for 24 hours. See Idempotency in async mode.
  • 409 for 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" records disabledReason: "manual" and disabledAt. Endpoints disabled before this release show disabledReason: "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 PATCH without visibility set the course to PUBLIC. 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 to title ASC, and slides, games, and simulations to title DESC). Send field and order if you relied on the old order.
  • List search. search now matches the title only (name on 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 422 content_invalid_request (was 500, or 400 on the key webhook routes).

Added

  • Request ids. Every /v1/content/** response carries a Request-Id header, and every error body carries error.requestId. This includes a body refused as too large (413 content_payload_too_large) or as malformed JSON (400 content_invalid_request), which now use the Content API error envelope too. Send X-Request-Id to choose the id. Quote it to support. See Request ids.
  • Idempotent-Replayed: true on every response that replays a stored answer for a repeated Idempotency-Key. See Idempotency.
  • List parameters on every resource list. limit (default 20; values above 100 are clamped to 100), field with a per-list allowlist (422 with error.allowedValues otherwise), order with an id tie-break, case-insensitive title search, and updatedSince for 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 answers 409 content_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 PATCH answer carry isLegacyFullAccess and issuerStatus (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.expiring webhook, once per expiry. See Expiry warnings and Key events.
  • learners:read scope. Opt-in. Keys with an explicit scope list need it to receive stats.topLearners and stats.mostBehindLearner on a course read with isIncludeStats=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, and Link headers. 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-Headers lists Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, Idempotent-Replayed, Deprecation, Sunset, and Link.
  • Failed authentication limit. An address that fails authentication 300 times in a minute gets 429 content_rate_limit_exceeded on every tf_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 202 of an async brief carries a runId (was null), GET .../run reports briefs with phase: "brief", and *.brief.completed and *.brief.failed webhooks carry the runId. 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 answers 409 content_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 issuerStatus is not_admin ties it to an active admin again.
  • Session error code. A 401 on an admin session route (key and webhook management, and the legacy export) now says content_session_required instead of content_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: 202 with the first job in its current state and Idempotent-Replayed: true. Same key with a different body: 409 content_conflict. Concurrent requests with one key: one 202, one 409. Keys are limited to 255 characters and scoped to the API key and classroomId. Jobs created earlier under an idempotencyKey are still returned for it. See Idempotency for expansion jobs.
  • Module, slide, and test PATCH return the updated resource as the single read returns it, plus success: true (was only { "success": true }).
  • Course PATCH changes only the fields sent. visibility is validated (422 otherwise), null clears level or thumbnail, and settings.aiTutorDefaults and settings.celebrationTrigger are saved. See Update a course.
  • Course stats days other than 7 or 30 answers 422 (was silently 7). dailyLearningTime has one bucket per UTC day.
  • video.render.completed signs videoUrl again for every delivery attempt and redelivery, with videoUrlExpiresAt 6 hours after that attempt, so late retries carry a working link. event.id is 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 422 content_invalid_request instead of 500.

Deprecated

Sunset 2027-04-30, as the aliases in the previous release. See Current deprecations.

  • success on module, slide, and test update responses. Read the resource fields.
  • The take query parameter on resource lists. Send limit.

Removed from the OpenAPI description

  • q and ids were 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}/narration narrates every scene that has none from its script, and POST .../videos/{videoId}/scenes/{sceneId}/narration narrates one scene (1 AI Credit per scene, the editor's price). The render request accepts generateMissingNarration: true to do this first. Scenes report hasNarration. A render with scenes missing narration now returns 400 content_video_narration_missing with their sceneIds. 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[].creditAmount is always 0. See Pricing.
  • Key scopes, classroom allowlists, and expiry. New keys can be limited to content:read, content:write, content:generate, and webhooks: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}/rotate accepts gracePeriodHours (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 rateLimitPerMinute is one budget per 60-second window across every route. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset, and a 429 carries Retry-After and error.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, and video.render.failed.
  • Webhook signatures and delivery. A timestamped X-Content-Integration-Signature-V2 header and an X-Content-Integration-Event-Id header; the legacy signature is still sent. Up to 9 attempts over about 23 hours. 307 and 308 redirects are followed; 301, 302, and 303 are not. Webhook URLs must be https on 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:manage key.
  • Webhook secret rotation with a grace period. rotate-secret accepts gracePeriodHours (0 to 168). During the grace period the V2 header carries one v1= 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 answer 202, with Idempotency-Key support and a GET .../run status route. Streaming is unchanged. See Generation.
  • Video rendering. POST .../videos/{videoId}/render, a render status route with a signed videoUrl, 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 videoKey and metadata.remotionLambda, scene ttsAudioKey and videoClipKey, slide contentKey, course authorId, chatSession, and settings, module authorId, and learner contact details in course stats are no longer returned. See Response fields.
  • Deletes. Every resource delete returns 200 with { "id", "deleted": true, "success": true }. Video delete used to return 204 with no body.
  • Lists. Resource lists return { "data", "meta" }.
  • Course read. A course that does not exist returns 404 instead of 200 with null.
  • Expansion scope. Creating an expansion job needs content:generate.
  • Errors. A render already in progress returns 409 content_video_render_in_progress instead 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.

  • items and totalCount on module, course, slide, test, game, and simulation lists. Read data and meta.itemCount.
  • total on the video list. Read meta.itemCount.
  • success on delete responses. Read deleted.

War diese Seite hilfreich?