Create Course

Generate an AI-powered course curriculum from a prompt and receive edit/preview URLs.

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.

UnitPrice
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 tier field: 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, or advanced. The default tier appears only in response priceSnapshot and 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:

TypeDescriptionBest for
ai-tutorInteractive AI tutor that discusses concepts with the studentMath, science, conceptual topics
coding-lessonHands-on coding tutorial with IDEProgramming topics only
coding-testCoding exercise/challengeProgramming practice only
chat-aiAI conversation lessonPrompt engineering, AI topics
reading-comprehensionReading passage with comprehension checksLanguage courses
examQuiz/assessmentPractice problems, end-of-chapter review
flashcardReview/vocabulary/formula cardsLanguage learning, key concepts
markdownText-only readingCourse 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

FieldTypeRequiredDescription
promptstringYesPrompt describing the course to generate
titlestringNoOverride AI-generated title
descriptionstringNoOverride AI-generated description
languagestringNoCourse language (default: en)
levelstringNobeginner, intermediate, or advanced (validated enum)
subjectstringNoSubject area (e.g. mathematics, physics, programming)
lessonCountnumberNoTarget number of lessons for AI to generate (1–50)
classroomIdstringNoClassroom ID to create the course in. Uses the default classroom if omitted
tierstringNoLegacy request value only: basic, standard, or advanced. Omit for new integrations. Do not send default; default is a response/catalog tier.
hasQuizbooleanNoWhether to include quiz lessons in the generated course
hasPracticebooleanNoWhether to include practice exercises in the generated course
idempotencyKeystringNoPrevents duplicate processing. Can also be sent via Idempotency-Key HTTP header
modestringNosync (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

FieldTypeDescription
idstringPlatform course request ID
statusstringPENDING, PROCESSING, COMPLETED, or FAILED
courseIdstring | nullActual course ID in TutorFlow (null until generation completes)
titlestring | nullCourse title
descriptionstring | nullCourse description
levelstring | nullDifficulty level
languagestring | nullCourse language (echoed from request, e.g. en)
subjectstring | nullSubject area (echoed from request)
slugstring | nullURL-friendly course slug
tierstringPricing tier used
modestring | nullExecution mode (sync or async)
hasQuizboolean | nullWhether quizzes were requested
hasPracticeboolean | nullWhether practice exercises were requested
lessonCountnumber | nullNumber of lessons in the generated course (length of lessons). Null until generation completes.
priceSnapshotobject | nullPricing details captured at request time
chaptersarray | nullChapter summaries with title, sequence, lessonCount, and slug
lessonsarray | nullLesson summaries with title, type, sequence, chapterTitle, chapterSlug, lessonSlug, and lessonUrl
shareTokenstring | nullToken used to construct the public URL and to resolve edit tokens for the list-mode preview link
thumbnailUrlstring | nullCourse thumbnail URL. Populated after lesson image generation when available.
previewUrlstring | nullEditor 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)
publicUrlstring | nullPublic read-only learner URL, opens at the first lesson
isTerminalbooleanWhether the request has reached a final state
pollAfterMsnumber | nullRecommended polling interval for async requests
idempotencyKeystring | nullEchoed idempotency key
idempotentReplayboolean | nulltrue if this response reused an existing request, false for a fresh request, null when no idempotencyKey was provided
createdAtstringISO 8601 timestamp
completedAtstring | nullISO 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

URLPurposeAuthExpiry
previewUrlCourse editor (lecture, title, curriculum, chapters, lessons)Public, no loginEdit token expires after 1h
publicUrlPublic course URL for learners, opens at the first lessonPublic, no loginNone

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

MethodPathDescription
GET/v1/platform/courses/edit/:editTokenGet course summary
GET/v1/platform/courses/edit/:editToken/fullGet full course with all lesson content
GET/v1/platform/courses/edit/:editToken/lessons/:sequenceGet single lesson
GET/v1/platform/courses/edit/:editToken/canonicalGet the canonical editor payload with chapters and lessons
PATCH/v1/platform/courses/edit/:editToken/canonicalBulk-update the canonical editor payload
GET/v1/platform/courses/edit/:editToken/lessons/by-id/:lessonId/fullGet one full lesson by UUID
POST/v1/platform/courses/edit/:editToken/lessons/by-id/:lessonId/generateStream generated lesson content for one lesson by UUID
PATCH/v1/platform/courses/edit/:editToken/lessons/by-id/:lessonId/fullUpdate one full lesson by UUID
PATCH/v1/platform/courses/edit/:editTokenUpdate course title/description
PATCH/v1/platform/courses/edit/:editToken/lessons/:sequenceUpdate lesson content, title, slug, or description
POST/v1/platform/courses/edit/:editToken/lessonsAdd a new lesson to the course
POST/v1/platform/courses/edit/:editToken/infer-typeInfer 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}/canonical

Returns 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}/full
curl -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
  }'
FieldTypeRequiredDescription
titlestringYesLesson title (≤ 255 chars)
typestringYesLesson type (ai-tutor, coding-lesson, coding-test, chat-ai, exam, flashcard, markdown)
descriptionstringNoLesson description (≤ 2000 chars)
quizCountnumberNoNumber of quizzes to auto-generate (0–20)
chapterSequencenumberNoChapter 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." }'
FieldTypeRequiredDescription
titlestringNoUpdated course title
descriptionstringNoUpdated 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" }'
FieldTypeRequiredDescription
lecturestringNoUpdated lecture HTML content
titlestringNoUpdated lesson title
descriptionstringNoUpdated lesson description
lessonSlugstringNoUpdated 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" }
    ]
  }'
FieldTypeRequiredDescription
lessonTitlestringYesTitle of the lesson being inferred
lessonDescriptionstringNoOptional description for additional context
courseTitlestringNoCourse title for context
courseDescriptionstringNoCourse description for context
existingLessonsarrayNoOther 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.