After this guide the agent has a course in its classroom, one graded answer, and a balance that shows exactly what each step cost. It takes about two minutes and 7 of the 50 trial credits.
You need the live Content key (contentApiKey) and defaultClassroomId from Quick Start. With a test key, use sandboxClassroomId instead: the calls are the same, nothing is charged, and the content is canned.
export TUTORFLOW_CONTENT_API_KEY="tf_content_..."
export CLASSROOM_ID="0fce..."Every response on this page was recorded from a real account. Ids are shortened, for example 5d2a....
1. Start a course
curl -i -X POST "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/courses/generate" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: course:intro-python:v1" \
-d '{
"topic": "Beginner Python for adult career changers: variables, loops, and functions",
"level": "beginner",
"lessonCount": 1,
"language": "en"
}'The answer is 202 Accepted. TutorFlow writes the course in the background and hands you a run to follow:
HTTP/2 202
location: /v1/content/classrooms/0fce.../runs/6bff...
x-ratelimit-limit: 60
x-ratelimit-remaining: 59{
"runId": "6bff...",
"status": "queued",
"kind": "course",
"estimatedCredits": 6,
"statusUrl": "/v1/content/classrooms/0fce.../runs/6bff..."
}A course costs 3 credits for the outline and 3 for each lesson, so this one costs 6.
If the request fails, nothing starts and nothing is charged:
| Status | error.code | What to do |
|---|---|---|
400, 422 | content_invalid_request | Fix the field named in error.details. |
402 | content_payment_required | Not enough credits. Top up, then send the same request with the same Idempotency-Key. See When credits run out. |
429 | content_rate_limit_exceeded | Wait Retry-After seconds, then send it again. |
Keep the Idempotency-Key. If the request times out, send it again with the same key and body: you get the first 202 back with the header Idempotent-Replayed: true, and nothing starts or is charged twice. The replayed body is the original one, so it still says queued; read the run for its current status.
2. Wait for the run
Read statusUrl every 20 to 30 seconds:
export RUN_ID="6bff..."
curl "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/runs/$RUN_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"While the lesson is being written:
{
"runId": "6bff...",
"kind": "course",
"classroomId": "0fce...",
"status": "running",
"stage": "generating_lessons",
"progress": { "completed": 0, "total": 1, "unit": "lesson" },
"resourceId": "5d2a...",
"creditsCharged": 3,
"estimatedCredits": 6,
"error": null,
"createdAt": "2026-10-16T18:42:38.524Z",
"finishedAt": null,
"resumedFromRunId": null
}About 75 seconds later:
{
"runId": "6bff...",
"kind": "course",
"classroomId": "0fce...",
"status": "completed",
"stage": "generating_lessons",
"progress": { "completed": 1, "total": 1, "unit": "lesson" },
"resourceId": "5d2a...",
"creditsCharged": 6,
"estimatedCredits": 6,
"error": null,
"createdAt": "2026-10-16T18:42:38.524Z",
"finishedAt": "2026-10-16T18:43:52.506Z",
"resumedFromRunId": null
}resourceId is the course id. It appears as soon as the course exists, before the lessons are done. Longer courses take a few minutes. What each status means, how to handle a failed run, and how to get a webhook instead of polling are in Follow a Generation.
3. Read the course
export COURSE_ID="5d2a..."
curl "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/courses/$COURSE_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"{
"id": "5d2a...",
"classroomId": "0fce...",
"title": "Beginner Python for Adult Career Changers: Variables, Loops, and Functions",
"description": "Learn Python fundamentals through a guided workplace example, with no prior programming experience required. In one 90-minute lesson, practice storing information, repeating calculations, and organizing code into a reusable function.",
"thumbnail": null,
"slug": "beginner-python-for-adult-career-changers-variables-loops-and-functions",
"contentType": "default",
"level": "beginner",
"isPublic": true,
"visibility": "PUBLIC",
"createdAt": "2026-10-16T18:42:56.479Z",
"updatedAt": "2026-10-16T18:42:56.479Z",
"editorUrl": "https://tutorflow.io/org/my-edtech-agent/my-edtech-agent/courses/chat/09cf.../lessons",
"publicUrl": "https://my-edtech-agent.tutorflow.ac/courses/beginner-python-for-adult-career-changers-variables-loops-and-functions"
}editorUrlopens the course in the TutorFlow editor. An agent does not need it. Pass it on only if a person should change the course; see Let a person change it.publicUrlis the public course page. A generated course startsPUBLIC, so it is set as soon as the run completes;PATCH{ "visibility": "PRIVATE" }hides it. See Let a person look at it.
The lessons themselves are under GET /courses/{courseId}/chapters and GET /courses/{courseId}/lessons/{lessonId}. See the Content API Reference.
4. List your courses
curl "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/courses?limit=10" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"{
"data": [
{
"id": "5d2a...",
"classroomId": "0fce...",
"title": "Beginner Python for Adult Career Changers: Variables, Loops, and Functions",
"level": "beginner",
"isPublic": true,
"visibility": "PUBLIC",
"totalLearnerCount": 0,
"editorUrl": "https://tutorflow.io/org/my-edtech-agent/my-edtech-agent/courses/chat/09cf.../lessons",
"publicUrl": "https://my-edtech-agent.tutorflow.ac/courses/beginner-python-for-adult-career-changers-variables-loops-and-functions"
}
],
"meta": {
"page": 1,
"take": 10,
"itemCount": 1,
"pageCount": 1,
"hasPreviousPage": false,
"hasNextPage": false
}
}Each item carries the same fields as a single course, plus totalLearnerCount; the example drops some for space. Page with page and limit, and filter with search. Read data and meta: the answer also repeats the list as items and totalCount for older clients.
5. Grade an answer
Evaluations answer at once. An exact evaluation compares the answer with referenceAnswer:
curl -X POST "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/evaluations" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"evaluationType": "exact",
"questionText": "What is 7 x 8?",
"learnerAnswer": "56",
"referenceAnswer": "56",
"maxScore": 1,
"language": "en",
"learnerExternalId": "learner-42"
}'201 Created:
{
"id": "5023...",
"classroomId": "0fce...",
"status": "COMPLETED",
"evaluationType": "exact",
"mode": "sync",
"isTerminal": true,
"pollAfterMs": null,
"learner": { "externalId": "learner-42", "name": null },
"conceptTags": [],
"score": 1,
"maxScore": 1,
"normalizedScore": 100,
"confidence": 1,
"rubricBreakdown": null,
"strengths": ["Answer matches the expected response."],
"mistakes": [],
"suggestions": [],
"feedbackSummary": "Correct! Your answer matches the expected response.",
"nextStep": null,
"rubricFiles": null,
"answerFiles": null,
"createdAt": "2026-10-16T18:43:39.378Z",
"completedAt": "2026-10-16T18:43:39.419Z"
}An evaluation costs 1 credit. Without referenceAnswer, an exact evaluation answers 400 content_invalid_request. For free-text answers, use open_ended, or rubric_short_answer with a rubric. learnerExternalId is your own id for the learner, and it feeds the learner model. See the Content API Reference.
6. Check what it cost
curl https://api.tutorflow.io/v1/content/credits \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"{
"credit": 43,
"overdraftCredit": 0,
"overdraftLimit": 100,
"availableCredit": 43,
"isPaymentFailed": false,
"livemode": true
}50 trial credits, minus 6 for the course and 1 for the evaluation, leaves 43. GET /v1/platform/agent/account shows the same number as credits.
Every charge has a row in the credit history:
curl "https://api.tutorflow.io/v1/content/credits/history?limit=5" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"{
"data": [
{
"id": "b2c7...",
"createdAt": "2026-10-16T18:43:39.410Z",
"type": "CREATE_EVALUATION",
"kind": "charge",
"amount": 1,
"note": "content evaluation 5023...",
"classroomId": "0fce...",
"apiKeyId": "e9b2...",
"apiKey": { "id": "e9b2...", "name": "Default Agent Key", "keyPrefix": "tf_content_zV8z...", "mode": "live" }
},
{
"id": "117f...",
"createdAt": "2026-10-16T18:42:56.448Z",
"type": "CREATE_CURRICULUM",
"kind": "charge",
"amount": 3,
"note": "Content API generation run 6bff...: course outline",
"classroomId": "0fce...",
"apiKeyId": "e9b2...",
"apiKey": { "id": "e9b2...", "name": "Default Agent Key", "keyPrefix": "tf_content_zV8z...", "mode": "live" }
},
{
"id": "db9d...",
"createdAt": "2026-10-16T18:42:32.470Z",
"type": "AGENT_TRIAL",
"kind": "adjustment",
"amount": 50,
"note": "Agent account trial credits",
"classroomId": null,
"apiKeyId": null,
"apiKey": null
}
],
"meta": { "limit": 5, "hasNextPage": false, "nextCursor": null }
}Every row a run writes has a note that starts with Content API generation run {runId}. See Credit History and Audit Log.
Show the work to a person
Check it yourself
Read the data; no browser and no sign-in are needed:
- A course:
GET /courses/{courseId}/chapters, thenGET /courses/{courseId}/lessons/{lessonId}. - A slide deck:
contentonGET /slides/{slideId}. - A test:
itemsonGET /tests/{testId}. - A game or simulation:
contenton its single read, one HTML page. Render it in a headless browser to see it. - A video:
videoUrlonGET /videos/{videoId}/render, the mp4, once it is rendered.
Let a person look at it
Send publicUrl. Anyone with the link opens it without an account.
-
Read
publicUrlon the resource. New courses and tests, and generated decks and modules, start public, so it may already be set. -
If it is
null, make the page public with onePATCH. For a game:bashcurl -X PATCH "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/games/$GAME_ID" \ -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "visibility": "PUBLIC" }'Resource Body publicUrlis setVideo { "visibility": "UNLISTED" }once it is rendered and has a title Slide deck { "visibility": "PUBLIC" }at once Game, simulation { "visibility": "PUBLIC" }once its first build has finished Module { "isPublic": true }at once -
Send the
publicUrlfrom thePATCHanswer.
A test's public page shows only its cover; answering needs a learner sign-in. For a person who should read the questions, send editorUrl.
Make a page public only when the person wants a link. Only videos and tests have a link-only setting (UNLISTED). A public deck, game, or simulation opens for anyone who has the link, and a public course, module, or test is also listed on the classroom site.
Let a person change it
Send editorUrl. It opens the same TutorFlow editor educators use, after the person signs in to TutorFlow as an admin of the account's organization. At first that is only the person who verified the account email; until someone has, nobody can open it. In that case send publicUrl, or ask for the email to be verified (Email verification). The agent cannot sign in for them.
Resources made with a test key have neither link. The full table for every resource type is in Check and share what you made.
Other generators
Tests, modules, and slide decks work like the course: a POST that answers 202 with a run, then the same run route.
| Generate | Route | Credits |
|---|---|---|
| A test | POST /tests/generate with topic and itemCount | 3 |
| One lesson module | POST /modules/generate with topic | 3 |
| A slide deck | POST /slides/generate with topic and slideCount | 1 per text page, 3 per image page |
Videos, games, and simulations are built in steps. See Generation runs, Video Rendering, Games, and Simulations.
Next
- Follow a Generation: statuses, failures, resume, and webhooks.
- Pricing and Billing: what each call costs and how to top up.