Four routes write content for you from a topic, the way the TutorFlow editor does: a test with its questions, one module, a course with its chapters and lessons, or a slide deck with its pages and presenter notes. Each request starts a run that keeps going on TutorFlow's servers after the 202. You follow it with a webhook or by polling the run.
Games and simulations are generated differently, in brief, build, and revise steps; see Generation: Brief, Build, Revise.
Every path below is under /v1/content/classrooms/{classroomId}. The generate routes need content:generate and spend AI Credits; the run routes need content:read.
| Method | Path | Scope | Success |
|---|---|---|---|
POST | /tests/generate | content:generate | 202 with the run |
POST | /modules/generate | content:generate | 202 with the run |
POST | /courses/generate | content:generate | 202 with the run |
POST | /slides/generate | content:generate | 202 with the run |
GET | /runs | content:read | 200 with a page of runs |
GET | /runs/{runId} | content:read | 200 with one run |
Start a run
curl -i -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/courses/generate" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: course-generate:customer-onboarding:v1" \
-d '{
"topic": "Onboarding for new customer success managers: our product, the first call, and escalations",
"level": "beginner",
"lessonCount": 6,
"language": "en"
}'import { request } from './tutorflow.js'
const classroomPath = `/v1/content/classrooms/${process.env.CLASSROOM_ID}`
const run = await request('POST', `${classroomPath}/courses/generate`, {
idempotencyKey: 'course-generate:customer-onboarding:v1',
body: {
topic: 'Onboarding for new customer success managers: our product, the first call, and escalations',
level: 'beginner',
lessonCount: 6,
language: 'en',
},
})
console.log(run.runId, run.status, run.estimatedCredits)HTTP/1.1 202 Accepted
Location: /v1/content/classrooms/00000000-0000-4000-8000-000000000010/runs/00000000-0000-4000-8000-000000000b01{
"runId": "00000000-0000-4000-8000-000000000b01",
"status": "queued",
"kind": "course",
"estimatedCredits": 21,
"statusUrl": "/v1/content/classrooms/00000000-0000-4000-8000-000000000010/runs/00000000-0000-4000-8000-000000000b01"
}The generate routes always answer 202; they never stream. The Location header repeats statusUrl. Hanging up after the 202 has no effect: the run keeps going. A body that fails validation answers 422 with error.details before a run exists, and unknown fields are dropped.
Request bodies
Test
POST /tests/generate takes the options of the TutorFlow test editor, with the same defaults.
| Field | Required | Notes |
|---|---|---|
topic | Yes | What the test covers. Up to 4,000 characters. |
instructions | No | Who takes the test and what to stress. Up to 4,000 characters. |
level | No | EASY, MEDIUM, or HARD. Default MEDIUM. |
itemCount | No | 1 to 30 questions. Default 20. |
itemTypes | No | One type per question, in order: select, blank, open-ended, true-false, or submission. Its length must equal itemCount, otherwise 422. Without it, the editor's mix is used: 3 true or false, then multiple choice, 3 fill in the blank, and 2 open-ended last. |
totalScore | No | An integer of at least 1. Default 100. |
passingValueType | No | SCORE or PERCENTAGE. Default SCORE. |
passingValue | No | 0 or more. Default 80. |
language | No | See Output language. |
Module
POST /modules/generate writes one module.
| Field | Required | Notes |
|---|---|---|
topic | Yes | What the module teaches. Up to 4,000 characters. |
type | No | markdown, ai-tutor, chat-ai, coding-lesson, coding-test, notebook, web, exam, flashcard, dictation, reading-comprehension, translation, shadowing, or roleplay. Without it, the type is chosen from the topic, as in the editor. |
referenceText | No | Plain text or Markdown, up to 100,000 characters, used the way an attached file is used in the editor. File uploads are not accepted on this route. |
language | No | See Output language. |
Course
POST /courses/generate plans an outline, creates the course, then writes each lesson.
| Field | Required | Notes |
|---|---|---|
topic | Yes | What the course is about and who it is for. Up to 4,000 characters. prompt is accepted as a deprecated name for the same field, read only when topic is absent. |
title, description | No | Used as given, up to 255 and 4,000 characters. Otherwise the outline writes them. |
level | No | beginner, intermediate, or advanced. |
lessonCount | No | 1 to 30. Without it, the outline plans 12 lessons in 3 chapters of 4. |
language | No | See Output language. |
Slides
POST /slides/generate writes an outline, creates the deck, then writes its pages four at a time, with a presenter script on every page. It takes the slide editor's options, except attached files.
| Field | Required | Notes |
|---|---|---|
topic | Yes | What the deck teaches. Up to 4,000 characters. |
instructions | No | Who the deck is for and what to stress. Up to 4,000 characters. It shapes the outline and is kept on the deck as its context (metadata.target, where the editor keeps it). |
slideCount | No | 5 to 20 pages, counting the title, table of contents, and summary pages. Default 10. |
textLength | No | concise or detailed: how much text each page carries. Default detailed. |
themeId | No | default, breeze, sunset, forest, rose, alien, aurora, velvet-tides, or midnight. Default default. |
language | No | See Output language. |
curl -sS -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/slides/generate" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: slides-generate:safety-week:v1" \
-d '{ "topic": "Workplace safety for new warehouse staff", "slideCount": 10, "textLength": "concise", "themeId": "forest", "language": "en" }'The answer is 202 with "kind": "slide" and "estimatedCredits": 16. How the deck is made:
- The outline is written as the editor writes it. If it has more sections than
slideCount, content sections are dropped and the title, table of contents, and summary stay. If it has fewer, the deck has fewer pages and costs less. - The deck is a classroom slide deck,
PUBLIClike a deck made in the editor, titled after the outline's first section.resourceIdis its id as soon as it exists, and it carriesmetadata.generationRunId. - Pages are written in batches of four. Every fourth page from the first may carry a generated picture, a layout image or an inline infographic, where the editor puts them. A picture that fails to generate is left out, and its page stays.
- The deck is saved after every batch, so it can be opened while later pages are still being written. It is the slide editor's own document, so it opens in the editor, in the classroom, and on
GET /slides/{slideId}like a deck made in the editor.
Two things look different from a deck made in the editor:
- No thumbnail at first. The deck's list thumbnail is drawn by the editor when a teacher saves the deck, so a generated deck has none until a teacher opens and saves it.
- Deck-wide layout can vary between batches. Each batch of four pages is written on its own, so a layout rule meant for the whole deck, such as alternating image sides, can slip where one batch ends and the next begins.
Output language
The language is decided once, when the run is accepted, and kept for the whole run:
language, if you send it. A BCP 47 tag of up to 35 characters, such asko,pt-BR, orzh-TW.- Otherwise the language the request is written in: the
topic(theninstructions) of a test or a slide deck, thetopicof a module, or thetopic(thentitle) of a course. - Otherwise English.
For Chinese, name the written form: zh-TW for Traditional or zh-CN for Simplified. A bare zh, or Chinese text with no tag, takes the form the text is written in, and Simplified when the text does not tell. Every lesson of a course is written in the course's language, and every page, presenter note, and image prompt of a deck in the deck's language.
Credits
Generation charges what the TutorFlow editor charges. Prices are in Pricing; the table shows how estimatedCredits is worked out.
| Kind | What is charged, and when | estimatedCredits |
|---|---|---|
| Test | 3 credits once, when the test is saved. | 3 |
| Module | 3 credits once, when the module is saved. | 3 |
| Course | 3 credits for the outline, then 3 credits for each lesson as it is saved. | 3 + 3 x lessonCount, or 39 without lessonCount |
| Slide deck | The slide editor's price: 1 credit per text page and 3 per image page, every fourth page from the first being an image page. The outline is free. Charged per batch of four pages as it is written. | The price of slideCount pages: 16 without slideCount |
A deck costs the same as in the editor; only the moment of charging differs. The editor charges the whole deck when the teacher confirms the outline. The API charges each batch when it is written: a full batch of four is one image page and three text pages, 6 credits, and a shorter last batch of k pages costs k + 2. The images are included.
| Pages | Image pages | Credits | Batches |
|---|---|---|---|
| 5 | 2 | 9 | 6 + 3 |
| 10 | 3 | 16 | 6 + 6 + 4 |
| 15 | 4 | 23 | 6 + 6 + 6 + 5 |
| 20 | 5 | 30 | 6 + 6 + 6 + 6 + 6 |
- The request is refused with
402before any work starts whenestimatedCreditsis more than the key's remaining monthly credit limit (content_key_budget_exceeded), or more than the organization's available credits (content_payment_required), or when the organization has a failed payment (content_payment_failed). - A course checks the balance and the key limit again before each lesson, and a deck before each batch. If either runs out partway, the run fails with a
402error and keeps what it wrote, which stays charged. - Without
lessonCount, the outline decides the number of lessons, and a deck outline with fewer sections thanslideCountmakes a smaller deck, so the final cost can differ from the estimate. The run'screditsChargedis the exact amount. - Charges are recorded under your key in the credit history and count toward its monthly limit. A deck's rows have type
CREATE_SLIDEand the noteContent API generation run {runId}: slide pages {first}-{last}. - Nothing is charged twice, whether you retry with the same
Idempotency-Keyor a server restarts during the run.
Follow a run
When the run ends, TutorFlow sends one webhook: test.generation.completed or test.generation.failed, and the same for module, course, and slide. Subscribe to them on an endpoint; see Generation events. There are no progress events.
To poll instead, read the statusUrl every 10 to 30 seconds:
GET /v1/content/classrooms/{classroomId}/runs/{runId}
Authorization: Bearer tf_content_...{
"runId": "00000000-0000-4000-8000-000000000b01",
"kind": "course",
"classroomId": "00000000-0000-4000-8000-000000000010",
"status": "running",
"stage": "generating_lessons",
"progress": { "completed": 4, "total": 6, "unit": "lesson" },
"resourceId": "00000000-0000-4000-8000-000000000201",
"creditsCharged": 15,
"estimatedCredits": 21,
"error": null,
"createdAt": "2026-10-01T09:00:00.000Z",
"finishedAt": null
}| Field | Meaning |
|---|---|
kind | test, module, course, or slide. |
status | queued, running, completed, or failed. |
stage | null while queued. Test: generating. Module: creating, then generating. Course: outlining, creating_course, then generating_lessons. Slides: outlining, creating_deck, then generating_pages. New stages may be added; display unknown ones as they are. |
progress | completed of total, counted in unit: test (total 1), step for a module (create, then write: total 2), lesson for a course, or page for a deck (for both, total 0 until the outline exists). |
resourceId | The test, module, course, or deck id, set as soon as it exists. A course or deck can be opened while it is still being written. |
creditsCharged | Credits charged so far, for finished steps. |
error | Why the run failed, or null. |
finishedAt | When the run ended, or null while it runs. |
List runs
GET /runs lists the classroom's runs, newest first, with a cursor:
| Parameter | Notes |
|---|---|
kind | test, module, course, or slide. |
status | queued, running, completed, or failed. |
limit | Default 20. Values above 100 are clamped to 100. |
cursor | meta.nextCursor from the previous page. Send the same filters with it. |
{
"data": [{ "runId": "00000000-0000-4000-8000-000000000b01", "kind": "course", "status": "running" }],
"meta": { "limit": 20, "hasNextPage": false, "nextCursor": null }
}The list shows runs started through the Content API in that classroom, by any key of your organization. A classroom outside your key's allowlist answers 403 content_classroom_not_allowed, and a runId from another classroom or organization answers 404 content_not_found. A cursor that is malformed, or from another classroom or organization, answers 422. Runs are kept, and there is no route to delete one.
Retries and failures
- A step that fails for a passing reason, such as a model timeout, is tried again. Each step gets 3 attempts in all, counting attempts cut short by a server restart. After that the run fails with an
errorlikeStopped after 3 attempts at generating_lessons: <the last error>. - Errors that retrying cannot fix fail the run at once, with their own message in
error:402(credits or the key's monthly limit),401or403(the key or the classroom is no longer allowed),404, and422. - If the server running a generation stops, another server continues it from the last finished step, usually within two to three minutes. Nothing already made is made or charged again.
- Time limits: 20 minutes for a test, 30 minutes for a module, 15 minutes plus 8 minutes per lesson for a course, and 10 minutes plus 8 minutes per batch of four pages for a deck (50 minutes for 20 pages). A run past its limit fails with "The generation ran out of time and was stopped. Start it again."
- A failed course or deck run keeps the course or deck and what it already wrote, and
resourceIdpoints to it. A retry makes a new one.
Idempotency
Send an Idempotency-Key with every generate request. A retry with the same key and body within 24 hours returns the first 202, with Idempotent-Replayed: true, and starts nothing. The same key with a different body answers 409 content_conflict.
When a run fails, its key is released before the *.failed webhook is sent. A retry with the same key after a failure starts a new run with a new runId, so on *.generation.failed you can retry with the key you already have.
What the run makes
- Tests are saved
PUBLIC, as the test editor saves them. - Modules are published when the run completes, and carry
metadata.generationRunIdwith the run id. - Courses have their lessons published as each is written.
- Slide decks are saved
PUBLIC, as the slide editor saves them, after every batch.
Review generated content in TutorFlow before learners see it. Change it afterwards with the normal resource routes.