POST /v1/platform/courses
Creates a course request. The AI model generates a structured curriculum with chapters and lessons, creates the course in TutorFlow, and returns edit and preview URLs.
Pricing
The Course API is billed per generated lesson. One unit on the catalog = one lesson. Lecture content + quizzes + practice are all included in the per-lesson price.
| Unit | Price |
|---|---|
| Per lesson | $0.05 |
A course request generates lessonCount lessons (default 5, hard cap 50). The
billable amount equals unitPrice × actual generated lessons, not the
requested lessonCount. Billing follows what the AI actually produces.
Legacy
tierfield: still accepted for backwards compatibility but has no effect on price. Omit it in new integrations. If you send it, use one of the legacy request values:basic,standard, oradvanced. Thedefaulttier appears only in responsepriceSnapshotand the pricing catalog.
The priceSnapshot returned with the course response carries the full
billing record:
{
"category": "course",
"tier": "default",
"unit": "lesson",
"unitPrice": 0.05,
"units": 10,
"amountUsd": 0.50,
"currency": "USD",
"source": "platform_pricing_catalog_v2"
}Lesson Types
The AI selects interactive lesson types based on the course topic:
| Type | Description | Best for |
|---|---|---|
ai-tutor | Interactive AI tutor that discusses concepts with the student | Math, science, conceptual topics |
coding-lesson | Hands-on coding tutorial with IDE | Programming topics only |
coding-test | Coding exercise/challenge | Programming practice only |
chat-ai | AI conversation lesson | Prompt engineering, AI topics |
reading-comprehension | Reading passage with comprehension checks | Language courses |
exam | Quiz/assessment | Practice problems, end-of-chapter review |
flashcard | Review/vocabulary/formula cards | Language learning, key concepts |
markdown | Text-only reading | Course overview, reference material |
The coding-lesson and coding-test types are reserved for programming topics. For
math, science, language, or general-knowledge courses, the AI uses ai-tutor,
chat-ai, exam, flashcard, or markdown instead.
TutorFlow also normalizes the first lesson to a practical starter type. Web and
general conceptual courses start with ai-tutor, AI-focused courses start with
chat-ai, programming courses start with coding-lesson, and language courses
start with reading-comprehension.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes | Prompt describing the course to generate |
title | string | No | Override AI-generated title |
description | string | No | Override AI-generated description |
language | string | No | Course language (default: en) |
level | string | No | beginner, intermediate, or advanced (validated enum) |
subject | string | No | Subject area (e.g. mathematics, physics, programming) |
lessonCount | number | No | Target number of lessons for AI to generate (1–50) |
classroomId | string | No | Classroom ID to create the course in. Uses the default classroom if omitted |
tier | string | No | Legacy request value only: basic, standard, or advanced. Omit for new integrations. Do not send default; default is a response/catalog tier. |
hasQuiz | boolean | No | Whether to include quiz lessons in the generated course |
hasPractice | boolean | No | Whether to include practice exercises in the generated course |
idempotencyKey | string | No | Prevents duplicate processing. Can also be sent via Idempotency-Key HTTP header |
mode | string | No | sync (default) or async |
Example Request
curl -X POST https://api.tutorflow.io/v1/platform/courses \
-H "Authorization: Bearer tf_platform_..." \
-H "Content-Type: application/json" \
-d '{
"prompt": "Create a beginner Python course covering variables, loops, and functions with hands-on exercises",
"language": "en",
"level": "beginner",
"lessonCount": 5
}'Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Platform course request ID |
status | string | PENDING, PROCESSING, COMPLETED, or FAILED |
courseId | string | null | Actual course ID in TutorFlow (null until generation completes) |
title | string | null | Course title |
description | string | null | Course description |
level | string | null | Difficulty level |
language | string | null | Course language (echoed from request, e.g. en) |
subject | string | null | Subject area (echoed from request) |
slug | string | null | URL-friendly course slug |
tier | string | Pricing tier used |
mode | string | null | Execution mode (sync or async) |
hasQuiz | boolean | null | Whether quizzes were requested |
hasPractice | boolean | null | Whether practice exercises were requested |
lessonCount | number | null | Number of lessons in the generated course (length of lessons). Null until generation completes. |
priceSnapshot | object | null | Pricing details captured at request time |
chapters | array | null | Chapter summaries with title, sequence, lessonCount, and slug |
lessons | array | null | Lesson summaries with title, type, sequence, chapterTitle, chapterSlug, lessonSlug, and lessonUrl |
shareToken | string | null | Token used to construct the public URL and to resolve edit tokens for the list-mode preview link |
thumbnailUrl | string | null | Course thumbnail URL. Populated after lesson image generation when available. |
previewUrl | string | null | Editor URL, direct link to /{locale}/platform/courses/edit/{editToken} (single-course endpoints) or /{locale}/platform/courses/{shareToken} (list endpoint, auto-resolves the latest edit token) |
publicUrl | string | null | Public read-only learner URL, opens at the first lesson |
isTerminal | boolean | Whether the request has reached a final state |
pollAfterMs | number | null | Recommended polling interval for async requests |
idempotencyKey | string | null | Echoed idempotency key |
idempotentReplay | boolean | null | true if this response reused an existing request, false for a fresh request, null when no idempotencyKey was provided |
createdAt | string | ISO 8601 timestamp |
completedAt | string | null | ISO 8601 timestamp when generation completed |
Example Response
{
"id": "a1c2d3e4-f5g6-7h8i-9j0k-l1m2n3o4p5q6",
"status": "COMPLETED",
"courseId": "c7d8e9f0-1a2b-3c4d-5e6f-7g8h9i0j1k2l",
"title": "Python Quick Start",
"description": "A beginner-friendly Python course covering variables, loops, and functions.",
"language": "en",
"subject": null,
"level": "beginner",
"slug": "python-quick-start-2e5ad6",
"tier": "default",
"mode": "sync",
"hasQuiz": true,
"hasPractice": true,
"lessonCount": 6,
"priceSnapshot": {
"category": "course",
"catalogKey": "course.default",
"tier": "default",
"unit": "lesson",
"unitPrice": 0.05,
"units": 6,
"amountUsd": 0.30,
"currency": "USD",
"source": "platform_pricing_catalog_v2"
},
"shareToken": "b018172542f9a3c4d5e6f7890abcdef12345678",
"thumbnailUrl": null,
"previewUrl": "https://tutorflow.io/en/platform/courses/edit/9f3a...",
"publicUrl": "https://tutorflow.io/en/platform/courses/b018172542f9.../lessons/1",
"chapters": [
{ "title": "Introduction to Variables", "sequence": 1, "lessonCount": 2, "slug": "introduction-to-variables-cf1710" },
{ "title": "Working with Loops", "sequence": 2, "lessonCount": 2, "slug": "working-with-loops-253716" },
{ "title": "Defining Functions", "sequence": 3, "lessonCount": 2, "slug": "defining-functions-ad2b35" }
],
"lessons": [
{ "title": "Welcome to Python", "type": "ai-tutor", "sequence": 1, "chapterTitle": "Introduction to Variables", "chapterSlug": "introduction-to-variables-cf1710", "lessonSlug": "welcome-to-python-29ae64", "lessonUrl": "https://tutorflow.io/en/platform/courses/b018.../lessons/1" },
{ "title": "Variable Practice", "type": "coding-test", "sequence": 2, "chapterTitle": "Introduction to Variables", "chapterSlug": "introduction-to-variables-cf1710", "lessonSlug": "variable-practice-1b3f82", "lessonUrl": "https://tutorflow.io/en/platform/courses/b018.../lessons/2" },
{ "title": "For and While Loops", "type": "coding-lesson", "sequence": 3, "chapterTitle": "Working with Loops", "chapterSlug": "working-with-loops-253716", "lessonSlug": "for-and-while-loops-59c305", "lessonUrl": "https://tutorflow.io/en/platform/courses/b018.../lessons/3" },
{ "title": "Loop Challenges", "type": "coding-test", "sequence": 4, "chapterTitle": "Working with Loops", "chapterSlug": "working-with-loops-253716", "lessonSlug": "loop-challenges-a4b5c6", "lessonUrl": "https://tutorflow.io/en/platform/courses/b018.../lessons/4" },
{ "title": "Writing Functions", "type": "coding-lesson", "sequence": 5, "chapterTitle": "Defining Functions", "chapterSlug": "defining-functions-ad2b35", "lessonSlug": "writing-functions-d7e8f9", "lessonUrl": "https://tutorflow.io/en/platform/courses/b018.../lessons/5" },
{ "title": "Function Exercises", "type": "coding-test", "sequence": 6, "chapterTitle": "Defining Functions", "chapterSlug": "defining-functions-ad2b35", "lessonSlug": "function-exercises-0a1b2c", "lessonUrl": "https://tutorflow.io/en/platform/courses/b018.../lessons/6" }
],
"idempotencyKey": null,
"idempotentReplay": null,
"isTerminal": true,
"createdAt": "2026-03-24T10:32:15.108Z",
"completedAt": "2026-03-24T10:32:37.671Z"
}Preview URL vs Public URL
| URL | Purpose | Auth | Expiry |
|---|---|---|---|
previewUrl | Course editor (lecture, title, curriculum, chapters, lessons) | Public, no login | Edit token expires after 1h |
publicUrl | Public course URL for learners, opens at the first lesson | Public, no login | None |
The previewUrl returned by POST /v1/platform/courses and GET /v1/platform/courses/:id
is a direct link to the editor for the current edit token
(/{locale}/platform/courses/edit/{editToken}). Anyone with the URL can edit lesson
content, title, description, and curriculum structure without logging in. The edit
token is valid for one hour; the share-token endpoints
(GET /v1/platform/courses/public/:shareToken/full) auto-refresh it on access, or you can mint a new one explicitly via
POST /v1/platform/courses/:id/edit-token.
The list endpoint (GET /v1/platform/courses) returns a previewUrl of the form
/{locale}/platform/courses/{shareToken} instead. That page resolves the latest
edit token server-side and redirects into the editor, so you do not need to refresh
tokens before generating list links.
The publicUrl is the read-only link you share with learners. Each lesson summary
also includes a lessonUrl that links directly to that lesson by sequence.
Refreshing an Edit Token
curl -X POST https://api.tutorflow.io/v1/platform/courses/a1c2d3e4-5678-4abc-9def-0123456789ab/edit-token \
-H "Authorization: Bearer tf_platform_..."{
"editToken": "new-token-hex...",
"editTokenExpiresAt": "2026-03-24T13:30:00.000Z"
}Edit Token API
The edit URL provides access to these public endpoints (no API key needed):
| Method | Path | Description |
|---|---|---|
GET | /v1/platform/courses/edit/:editToken | Get course summary |
GET | /v1/platform/courses/edit/:editToken/full | Get full course with all lesson content |
GET | /v1/platform/courses/edit/:editToken/lessons/:sequence | Get single lesson |
GET | /v1/platform/courses/edit/:editToken/canonical | Get the canonical editor payload with chapters and lessons |
PATCH | /v1/platform/courses/edit/:editToken/canonical | Bulk-update the canonical editor payload |
GET | /v1/platform/courses/edit/:editToken/lessons/by-id/:lessonId/full | Get one full lesson by UUID |
POST | /v1/platform/courses/edit/:editToken/lessons/by-id/:lessonId/generate | Stream generated lesson content for one lesson by UUID |
PATCH | /v1/platform/courses/edit/:editToken/lessons/by-id/:lessonId/full | Update one full lesson by UUID |
PATCH | /v1/platform/courses/edit/:editToken | Update course title/description |
PATCH | /v1/platform/courses/edit/:editToken/lessons/:sequence | Update lesson content, title, slug, or description |
POST | /v1/platform/courses/edit/:editToken/lessons | Add a new lesson to the course |
POST | /v1/platform/courses/edit/:editToken/infer-type | Infer the best lesson type from a title/description |
For AI agents and editor adapters, prefer the canonical and UUID-based routes over sequence-based routes. Lesson UUIDs stay stable when lessons are reordered.
Canonical Course Payload
curl https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/canonicalReturns the same course shape used by the shared course editor: course metadata, chapter IDs/slugs, and lesson rows with UUIDs. Use this response as the source of truth before bulk saves.
To save a full curriculum edit:
curl -X PATCH https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/canonical \
-H "Content-Type: application/json" \
-d '{
"title": "Python Foundations",
"description": "A practical Python course.",
"slug": "python-foundations",
"chapters": [
{ "id": "getting-started", "title": "Getting Started", "slug": "getting-started", "sequence": 1 }
],
"lessons": [
{
"id": "2ca91d88-7b0c-4287-89f1-89a74d8a5b26",
"chapterId": "getting-started",
"title": "Variables and Types",
"description": "Learn how Python stores data.",
"slug": "variables-and-types",
"type": "ai-tutor",
"sequence": 1,
"lecture": "<p>Python variables are names for values...</p>",
"content": { "blocks": [] },
"quizzes": [],
"problems": [],
"metadata": { "duration": 12 }
}
]
}'Returns { "success": true }.
Full Lesson by UUID
Use these routes when an agent edits one lesson after a reorder:
curl https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/lessons/by-id/{lessonId}/fullcurl -X PATCH https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/lessons/by-id/{lessonId}/full \
-H "Content-Type: application/json" \
-d '{
"title": "Control Flow",
"type": "ai-tutor",
"chapterId": "python-basics",
"sequence": 2,
"lecture": "<p>Use if statements to branch...</p>",
"content": { "blocks": [] },
"quizzes": [],
"problems": [],
"metadata": { "duration": 15 }
}'Returns the canonical lesson payload.
Generate Lesson Content by UUID
This endpoint streams Server-Sent Events. It generates lecture, content,
quizzes, or practice content for the lesson but does not save the final payload
by itself. Save the final payload with
PATCH /v1/platform/courses/edit/:editToken/lessons/by-id/:lessonId/full.
curl -N -X POST https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/lessons/by-id/{lessonId}/generate \
-H "Content-Type: application/json" \
-d '{
"chapter": { "id": "python-basics", "name": "Python Basics" },
"courseTitle": "Python Foundations",
"courseDescription": "A practical Python course.",
"shouldGenerateImage": false
}'The backend overrides the course and lesson identity from the editToken and
lessonId, so agents should not invent those identifiers in the body.
Add Lesson
curl -X POST https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/lessons \
-H "Content-Type: application/json" \
-d '{
"title": "Solving Quadratic Equations",
"description": "Practice the quadratic formula on real problems.",
"type": "ai-tutor",
"quizCount": 0,
"chapterSequence": 2
}'| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Lesson title (≤ 255 chars) |
type | string | Yes | Lesson type (ai-tutor, coding-lesson, coding-test, chat-ai, exam, flashcard, markdown) |
description | string | No | Lesson description (≤ 2000 chars) |
quizCount | number | No | Number of quizzes to auto-generate (0–20) |
chapterSequence | number | No | Chapter sequence to add the lesson to. Defaults to the last chapter. |
Returns the created lesson as PlatformLessonPublicDto (id, sequence, title, type,
chapterTitle, description, lectureContent, codeContent, quizzes, lessonSlug,
chapterSlug).
Update Course
curl -X PATCH https://api.tutorflow.io/v1/platform/courses/edit/{editToken} \
-H "Content-Type: application/json" \
-d '{ "title": "New Title", "description": "Updated overview." }'| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Updated course title |
description | string | No | Updated course description |
Returns { "success": true }.
Update Lesson
curl -X PATCH https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/lessons/3 \
-H "Content-Type: application/json" \
-d '{ "lecture": "<p>Updated HTML</p>", "title": "Loops, Revisited" }'| Field | Type | Required | Description |
|---|---|---|---|
lecture | string | No | Updated lecture HTML content |
title | string | No | Updated lesson title |
description | string | No | Updated lesson description |
lessonSlug | string | No | Updated lesson slug |
Returns { "success": true }.
Infer Lesson Type
Given a lesson title (and optional context), the AI returns the best lesson type to use. Useful when an editor wants to insert a new lesson without picking a type manually.
curl -X POST https://api.tutorflow.io/v1/platform/courses/edit/{editToken}/infer-type \
-H "Content-Type: application/json" \
-d '{
"lessonTitle": "Practice: Factoring Polynomials",
"lessonDescription": "Hands-on practice problems for factoring.",
"courseTitle": "Intermediate Algebra",
"courseDescription": "Algebra fundamentals for high school students.",
"existingLessons": [
{ "title": "Welcome to Algebra", "type": "ai-tutor" },
{ "title": "Variables and Expressions", "type": "ai-tutor" }
]
}'| Field | Type | Required | Description |
|---|---|---|---|
lessonTitle | string | Yes | Title of the lesson being inferred |
lessonDescription | string | No | Optional description for additional context |
courseTitle | string | No | Course title for context |
courseDescription | string | No | Course description for context |
existingLessons | array | No | Other lessons in the course as { title, type } items |
Response:
{ "type": "exam" }Async Mode
Set mode: "async" to queue course generation as a background job. The response
returns immediately with status: "PENDING" and a pollAfterMs value. Poll
GET /v1/platform/courses/:id until isTerminal is true.
Idempotency
Pass an idempotencyKey in the request body or Idempotency-Key header to prevent
duplicate course generation. Reusing the same key returns the original response
with idempotentReplay: true.