Generate Your First Content

Generate a one-lesson course with your Content API key, follow the run to the end, read the course and its editor link, grade one answer, and check what it all cost.

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.

bash
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

bash
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
HTTP/2 202
location: /v1/content/classrooms/0fce.../runs/6bff...
x-ratelimit-limit: 60
x-ratelimit-remaining: 59
JSON
{
  "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:

Statuserror.codeWhat to do
400, 422content_invalid_requestFix the field named in error.details.
402content_payment_requiredNot enough credits. Top up, then send the same request with the same Idempotency-Key. See When credits run out.
429content_rate_limit_exceededWait 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:

bash
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:

JSON
{
  "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:

JSON
{
  "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

bash
export COURSE_ID="5d2a..."
curl "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/courses/$COURSE_ID" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"
JSON
{
  "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"
}
  • editorUrl opens 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.
  • publicUrl is the public course page. A generated course starts PUBLIC, 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

bash
curl "https://api.tutorflow.io/v1/content/classrooms/$CLASSROOM_ID/courses?limit=10" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"
JSON
{
  "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:

bash
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:

JSON
{
  "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

bash
curl https://api.tutorflow.io/v1/content/credits \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"
JSON
{
  "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:

bash
curl "https://api.tutorflow.io/v1/content/credits/history?limit=5" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"
JSON
{
  "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, then GET /courses/{courseId}/lessons/{lessonId}.
  • A slide deck: content on GET /slides/{slideId}.
  • A test: items on GET /tests/{testId}.
  • A game or simulation: content on its single read, one HTML page. Render it in a headless browser to see it.
  • A video: videoUrl on GET /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.

  1. Read publicUrl on the resource. New courses and tests, and generated decks and modules, start public, so it may already be set.

  2. If it is null, make the page public with one PATCH. For a game:

    bash
    curl -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" }'
    ResourceBodypublicUrl is set
    Video{ "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
  3. Send the publicUrl from the PATCH answer.

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.

GenerateRouteCredits
A testPOST /tests/generate with topic and itemCount3
One lesson modulePOST /modules/generate with topic3
A slide deckPOST /slides/generate with topic and slideCount1 per text page, 3 per image page

Videos, games, and simulations are built in steps. See Generation runs, Video Rendering, Games, and Simulations.

Next