Use this runbook when you, or an AI coding agent working for you, write integration code, test scripts, SDK wrappers, or support diagnostics for the Content API. For a machine-readable description of every route, import the OpenAPI description.
The boundary
| Task | API |
|---|---|
| Create, read, update, delete, or sync modules, courses, videos, slides, tests, games, or simulations in a classroom | /v1/content/classrooms/{classroomId}/... |
| Plan, build, or revise a game or simulation | /v1/content/classrooms/{classroomId}/{games,simulations}/{id}/{brief,build,revise} |
| Render a video to mp4 | /v1/content/classrooms/{classroomId}/videos/{videoId}/render |
| Expand your own source JSON into TutorFlow content | /v1/content/integrations/expansions |
| Generate a test, module, or course from a topic | /v1/content/classrooms/{classroomId}/{tests,modules,courses}/generate, then /runs/{runId} |
| Upload a file for a resource field | /v1/content/classrooms/{classroomId}/assets |
| Read or manage a classroom's learners | /v1/content/classrooms/{classroomId}/learners/..., with learners:read or learners:write |
| Read what a key spent or changed | /v1/content/credits/history, /v1/content/audit-log |
| Create or rotate a key | Settings > Content API, or /v1/content/organizations/{organizationId}/api-keys with an admin session |
| Autonomous AI agent workflows built for the Agent Platform | /v1/platform/**, a separate API |
A Content API client never needs a platform key, a workspace id, an edit token, or a platform URL. If generated code reaches for one, it is on the wrong API.
Discovery
Never hard-code ids copied from screenshots, examples, or a database.
- The key comes from Settings > Content API, as an environment variable.
GET /v1/content/creditsconfirms the key works.GET /v1/content/classroomslists the classrooms the key can use. Select one by name or slug from configuration, not by array position.- Resource ids come from create responses, lists, or expansion results.
Resource calls do not need an organization id. It is needed only for admin session routes.
Environment variables
Use these names, as the rest of these docs do:
| Variable | Holds |
|---|---|
TUTORFLOW_API_BASE_URL | https://api.tutorflow.io |
TUTORFLOW_CONTENT_API_KEY | The tf_content_ key. From a secret manager, never from source. |
CLASSROOM_ID | The target classroom id. |
JOB_ID, MODULE_ID, COURSE_ID, VIDEO_ID, SLIDE_ID, TEST_ID, GAME_ID, SIMULATION_ID, WEBHOOK_ID | Ids returned by the API. |
TUTORFLOW_WEBHOOK_SECRET | The webhook signing secret. |
Safe defaults
| Concern | Default |
|---|---|
| Headers | Authorization: Bearer tf_content_... and Content-Type: application/json. |
| Idempotency | An Idempotency-Key on every create and every POST action, built from your own ids and a version: module:{externalId}:v{version}, video:{videoId}:render:v{n}, expansion:{externalLevelId}:v{sourceVersion}. The full pattern list is in Idempotency. Never use the current time. |
| Retries | Follow What to do for each status: retry 429 after Retry-After, retry 5xx and network errors with backoff only when the request is safe to repeat, stop on 402, read before deciding on 409, and never retry 400, 401, 403, 404, or 422 unchanged. The request wrapper implements this. |
| Timeouts | 30 seconds for ordinary requests. For game and simulation generation, use async mode (Prefer: respond-async); a streamed build runs for minutes and hanging up aborts it. |
| Polling | Expansion jobs every 3 to 5 seconds, generation runs every 10 to 30 seconds, renders every 10 to 15 seconds, each with a deadline. Prefer webhooks in production. |
| Test mode | Build and test with a tf_content_test_ key in the sandbox classroom first; it never spends credits. Check livemode in webhook receivers. See Test Mode. |
| Scopes | Ask for the narrowest key. On 403 content_insufficient_scope, report error.requiredScope; do not retry. |
| Credits | Read Pricing and check availableCredit before a batch of priced calls. |
| Visibility | Create games and simulations PRIVATE. |
| Logging | Log keyPrefix, route, status, error.code, Request-Id, and ids. Never the key, the webhook secret, or a signed videoUrl. |
| Lists | Page with limit (at most 100) and page until meta.hasNextPage is false. Sync with updatedSince. |
What to store
| Value | Why |
|---|---|
TutorFlow resource id and classroomId | Needed for every later call, and proves the classroom. |
The Idempotency-Key used | Explains replays, and lets you retry safely. |
updatedAt last seen, and the start time of the last sync run | For syncing changes with updatedSince. |
Request-Id of failed calls | The first thing TutorFlow support asks for. |
Expansion job.id, outputs[].resourceId, outputs[].manifest | Links your source to the created resources. |
keyPrefix | Safe to share with support. |
Smoke test
set -euo pipefail
curl -sSf "$TUTORFLOW_API_BASE_URL/v1/content/credits" -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" > /dev/null
MODULE_ID="$(curl -sSf -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/modules" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: module:smoke-test:v1" \
-d '{ "title": "Content API smoke test", "content": "<p>Created by the Content API.</p>", "metadata": { "externalId": "smoke-test" } }' \
| jq -er '.id')"
curl -sSf "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/modules/$MODULE_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" > /dev/null
curl -sSf -X PATCH "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/modules/$MODULE_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Content API smoke test, updated" }' > /dev/null
curl -sSf -X DELETE "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/modules/$MODULE_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" | jq -e '.deleted == true' > /dev/null
echo "smoke test passed"Run the same create, read, update, and delete for every resource type the integration manages; the bodies are in Resources. The script fails fast on any non-2xx status and on an empty id.
Common mistakes
| Mistake | Correct behavior |
|---|---|
Calling /v1/platform/** because the task mentions automation | Use /v1/content/** for content managed in TutorFlow classrooms. |
| Taking the first classroom from the list | Select the classroom by name or slug from configuration. |
Reusing an expansion Idempotency-Key for new source content | The key answers 409. Change it with the source version. |
Reading items, totalCount, total, or success on deletes and updates, or sending take | Read data, meta.itemCount, deleted, and the resource fields, and send limit. The old names are deprecated. |
Reading the module again after PATCH /modules/{id} | The PATCH response already is the updated module. |
Sending a full course object on PATCH to change one field | Send only the fields you mean to change; omitted fields keep their values. |
| Passing a slug or an external id where a TutorFlow id goes | Id path parameters must be UUIDs; anything else is 422. |
Treating contentKey, pdfKey, or thumbnailKey as a URL | They are opaque storage references. See Storage references and URLs. |
| Parsing a streamed generation response as JSON | Use async mode, or read it as Server-Sent Events. |
Retrying a streamed build after build-done | It was stored and charged. Use async mode, where a retry with the same key replays. |
Verifying webhooks after JSON.parse | Verify the raw bytes first. |
Treating 402 as a server error | Stop and report it. Retrying cannot succeed until billing or the key's monthly limit changes. |
Sending If-None-Match from fetch without Cache-Control: max-age=0 | fetch adds Cache-Control: no-cache, so the answer is always 200. Send max-age=0. |
Resending the old body with currentETag after a 412 | Read the resource again and reapply the change. |
| Building a storage key, or reusing one from another classroom | Upload the file and use the returned assetKey. Keys from other classrooms answer 400. |
| Expecting a learner route to work with a full-access key | Learner scopes are opt-in. Ask an admin to add learners:read or learners:write to the key. |
| Publishing a build nobody has opened | Keep visibility PRIVATE until a person has checked it. |
Before handing code to a customer
- It discovers classrooms through the API and takes the key from the environment.
- It uses only
/v1/content/**andtf_content_keys. - Every create and
POSTaction sends a stableIdempotency-Key. - It stores TutorFlow ids next to the customer's ids.
- It handles
400,401,402,403,404,409,413,422, and429as in Errors. - Logs redact the key and the webhook secret.
- A small real request has completed before larger batches run.