資料
Content Resource API

Content Resource API

Create, list, update, and control TutorFlow courses, videos, slides, tests, and modules through Content API keys.

Use the Content Resource API when an external system needs direct control of TutorFlow-authored content. These endpoints use the dedicated tf_content_ bearer key. They do not use admin cookies after the key has been created.

Use these endpoints for content that people create, review, or manage in TutorFlow classrooms. Do not use Agent Platform endpoints for this workflow. Agent Platform endpoints live under /v1/platform/**, use separate credentials, and are reserved for AI agent workflows.

All paths are relative to https://api.tutorflow.io.

export TUTORFLOW_API_BASE_URL="https://api.tutorflow.io"

Set TUTORFLOW_CONTENT_API_KEY from the apiKey returned by Content API Key Management. Do not paste the placeholder key from response examples into scripts.

Find the target classroom before running resource examples:

curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/classrooms" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"

Then store the classroom id:

export TUTORFLOW_CLASSROOM_ID="$(curl -s "$TUTORFLOW_API_BASE_URL/v1/content/classrooms" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" | jq -r '.[0].id')"

If TUTORFLOW_CLASSROOM_ID is empty or null, create or select a classroom in TutorFlow before using direct resource CRUD.

Common request headers:

Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY
Content-Type: application/json
Idempotency-Key: source-system-resource-version

Resource model

ResourceCollection pathUse it for
Module/v1/content/classrooms/{classroomId}/modulesOne interactive lesson or learning module.
Course/v1/content/classrooms/{classroomId}/coursesMulti-lesson curriculum with chapters and lesson content.
Video/v1/content/classrooms/{classroomId}/videosEditable video plan, scenes, narration, and review before TutorFlow UI rendering.
Slide/v1/content/classrooms/{classroomId}/slidesEditable slide presentation content.
Test/v1/content/classrooms/{classroomId}/testsAssessment metadata, items, answers, scoring, and submissions.

The response is scoped to the Content API. Internal authoring tokens and internal ownership fields are not returned. Use the IDs returned by these endpoints for later reads, updates, deletes, and downstream reconciliation.

Request and response conventions

Create endpoints and side-effecting POST actions support Idempotency-Key. Use one stable key per external resource version or action version. If the same key and same body are retried, TutorFlow returns the original response. If the same key is reused with a different body, TutorFlow returns 409.

Create and update responses return the same classroom-owned content shape used by the TutorFlow UI, with internal editing fields removed. Store these fields when present:

FieldWhy it matters
idUse as {moduleId}, {courseId}, {videoId}, {slideId}, or {testId} on later calls.
classroomIdConfirms the resource is attached to the intended classroom.
title or nameDisplay label for review queues and sync logs.
slugUseful for human-readable reconciliation when returned.
createdAt, updatedAtUseful for incremental sync checks when returned.

Example trimmed create response:

{
  "id": "00000000-0000-4000-8000-000000000101",
  "classroomId": "00000000-0000-4000-8000-000000000010",
  "title": "Introduction to safety",
  "description": "A concise interactive module for new employees.",
  "type": "markdown",
  "status": "ready",
  "metadata": {
    "externalId": "module:intro-to-safety"
  },
  "createdAt": "2026-07-05T00:00:00.000Z",
  "updatedAt": "2026-07-05T00:00:00.000Z"
}

Create responses are resource-specific:

EndpointSuccess response
POST /modules201 with the created module object
POST /modules/{moduleId}/copy-to-course200 with the copied lesson and course lesson ids
POST /courses201 with the created course object
POST /videos201 with the created video object
POST /videos/{videoId}/scenes201 with the updated video object
POST /slides201 with the created slide object
POST /tests201 with the created test object

Update responses are also resource-specific. Module, slide, and test updates return 200 with { "success": true }. Course and video updates return 200 with the updated resource object. Video scene update and reorder return 200 with the updated video object.

Delete responses are resource-specific:

EndpointSuccess response
DELETE /modules/{moduleId}200 with { "success": true }
DELETE /courses/{courseId}200 with deleted id and optional chatSessionId
DELETE /videos/{videoId}200 with { "success": true }
DELETE /slides/{slideId}200 with the deleted slide object
DELETE /tests/{testId}200 with { "success": true }
DELETE /videos/{videoId}/scenes/{sceneId}200 with the updated video object

Modules

Create an interactive module:

curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/modules" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: module:intro-to-safety:v1" \
  -d '{
    "title": "Introduction to safety",
    "description": "A concise interactive module for new employees.",
    "type": "markdown",
    "isPublic": false,
    "content": "<h2>Safety first</h2><p>Always report hazards.</p>",
    "metadata": {
      "externalId": "module:intro-to-safety"
    }
  }'

Common module endpoints:

GET    /v1/content/classrooms/{classroomId}/modules?limit=20&page=1
GET    /v1/content/classrooms/{classroomId}/modules/{moduleId}
PATCH  /v1/content/classrooms/{classroomId}/modules/{moduleId}
DELETE /v1/content/classrooms/{classroomId}/modules/{moduleId}
POST   /v1/content/classrooms/{classroomId}/modules/{moduleId}/copy-to-course

Update a module:

{
  "title": "Workplace safety basics",
  "description": "A concise interactive module for new employees.",
  "content": "<h2>Safety first</h2><p>Always report hazards.</p>",
  "quizzes": [
    {
      "id": "quiz-1",
      "sequence": 1,
      "title": "Safety check",
      "question": "What should you do when you see a hazard?",
      "type": "select",
      "options": ["Ignore it", "Report it", "Hide it"],
      "correctAnswers": ["Report it"],
      "showFeedback": true
    }
  ]
}

Module quiz type values are select, blank, and true-false.

Copy a module into an existing course:

Set MODULE_ID to the id returned by module create or list.

curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/modules/$MODULE_ID/copy-to-course" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: module:intro-to-safety:copy-to-course:v1" \
  -d '{
    "courseId": "00000000-0000-4000-8000-000000000201",
    "sequence": 1
  }'

Courses

Create a course:

curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/courses" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: course:customer-onboarding:v1" \
  -d '{
    "title": "Customer onboarding",
    "description": "Five onboarding lessons for customer success teams.",
    "contentType": "default",
    "level": "Beginner",
    "visibility": "PRIVATE"
  }'

Course contentType values are lowercase. Use default for ordinary courses and exam for exam-oriented courses. Course visibility values are uppercase, for example PUBLIC, PRIVATE, ORGANIZATION, CLASSROOM, or COURSE.

Create response:

{
  "id": "00000000-0000-4000-8000-000000000201",
  "title": "Customer onboarding",
  "description": "Five onboarding lessons for customer success teams.",
  "slug": "customer-onboarding",
  "contentType": "default",
  "level": "Beginner",
  "isPublic": true,
  "visibility": "PRIVATE",
  "classroomId": "00000000-0000-4000-8000-000000000101"
}

Common course endpoints:

GET   /v1/content/classrooms/{classroomId}/courses?limit=20&page=1
GET   /v1/content/classrooms/{classroomId}/courses/{courseId}
PATCH /v1/content/classrooms/{classroomId}/courses/{courseId}
DELETE /v1/content/classrooms/{classroomId}/courses/{courseId}

When POST /courses is called without a supplied curriculum, TutorFlow creates the starter structure expected by the admin editor: 3 chapters with 4 lessons per chapter, for 12 editable markdown lessons total. The first chapter is available immediately, and the returned course can be opened in the course editor without a separate classroom dashboard handoff.

Use module copy or the TutorFlow UI when you need to move an existing module into a specific chapter and sequence after the starter curriculum has been created.

Read a course by URL:

curl "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/courses/$COURSE_ID" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"

Update a course:

curl -X PATCH "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/courses/$COURSE_ID" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Customer onboarding, revised",
    "description": "Updated onboarding lessons for customer success teams.",
    "slug": "customer-onboarding-revised",
    "visibility": "PRIVATE"
  }'

Course PATCH is partial. Fields omitted from the request keep their existing values. For example, omitting visibility does not change a private course into a public course.

Course delete response:

{
  "id": "00000000-0000-4000-8000-000000000201",
  "chatSessionId": "00000000-0000-4000-8000-000000000301"
}

After deletion, GET /v1/content/classrooms/{classroomId}/courses/{courseId} returns 404 with content_not_found. It does not return 200 null.

Videos

Create an editable video with scenes:

curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/videos" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: video:safety-summary:v1" \
  -d '{
    "title": "Safety summary",
    "description": "Editable video for workplace safety.",
    "visibility": "PRIVATE",
    "scenes": [
      {
        "script": "Welcome to the safety summary.",
        "displayText": "Safety summary",
        "remotionTemplate": "title",
        "keywords": ["safety", "onboarding"],
        "duration": 4,
        "subtitleDuration": 4,
        "subtitles": [
          {
            "text": "Welcome to the safety summary.",
            "offset": 0,
            "duration": 4
          }
        ],
        "transitionOut": "fade",
        "transitionDuration": 0.45
      },
      {
        "script": "Report hazards immediately and follow the posted emergency process.",
        "displayText": "Report hazards immediately",
        "remotionTemplate": "keyword",
        "keywords": ["report", "hazards", "process"],
        "duration": 5,
        "subtitleDuration": 5,
        "subtitles": [
          {
            "text": "Report hazards immediately.",
            "offset": 0,
            "duration": 2.5
          },
          {
            "text": "Follow the posted emergency process.",
            "offset": 2.5,
            "duration": 2.5
          }
        ]
      }
    ]
  }'

scenes is optional. If you omit it, TutorFlow creates the default editable video setup used by the Content API: landscape 16:9, layout mode auto, target duration 120 seconds, and 13 starter scenes that open in the admin video editor. If you provide scenes, TutorFlow stores your supplied scenes in array order.

Useful scene fields:

FieldNotes
scriptNarration or source text for the scene.
displayTextOn-screen title or quote text.
remotionTemplatetitle, keyword, quote, sketch, or none.
keywordsHighlight terms used by keyword and sketch templates.
durationScene length in seconds. Minimum is 0.5.
subtitlesOptional timed subtitles with text, offset, and duration.
transitionOutOptional transition into the next scene, for example fade. Send null or omit for a hard cut.

Numeric timing fields such as audioDuration, videoDuration, visualDuration, and subtitleDuration must be numbers when present. Omit them instead of sending null.

Common video endpoints:

GET    /v1/content/classrooms/{classroomId}/videos
GET    /v1/content/classrooms/{classroomId}/videos/{videoId}
PATCH  /v1/content/classrooms/{classroomId}/videos/{videoId}
DELETE /v1/content/classrooms/{classroomId}/videos/{videoId}
POST   /v1/content/classrooms/{classroomId}/videos/{videoId}/scenes
PATCH  /v1/content/classrooms/{classroomId}/videos/{videoId}/scenes/{sceneId}
DELETE /v1/content/classrooms/{classroomId}/videos/{videoId}/scenes/{sceneId}
PATCH  /v1/content/classrooms/{classroomId}/videos/{videoId}/scenes/reorder

Create a scene:

Send an Idempotency-Key on scene creation so network retries do not add duplicate scenes.

{
  "insertAfterSceneId": null
}

The create-scene response is the updated video object. Read the new scene id from the returned scenes array, then use that id for scene update, delete, or reorder calls.

Video delete response:

{
  "success": true
}

Update a scene:

{
  "script": "Welcome to the safety summary.",
  "displayText": "Welcome to the safety summary.",
  "duration": 8
}

Reorder scenes:

{
  "sceneIds": ["00000000-0000-4000-8000-000000000001"]
}

Video rendering is not exposed as a Content API endpoint yet. Treat videos created here as editable classroom assets and trigger final rendering from TutorFlow after review.

Slides

Create slides:

curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/slides" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: slides:safety-training:v1" \
  -d '{
    "title": "Safety training",
    "description": "Deck for the July onboarding cohort.",
    "outline": "1. Why safety matters\n2. Reporting hazards",
    "visibility": "PRIVATE",
    "content": "<slide><h1>Safety training</h1></slide>"
  }'

Slide creation returns 201 with the created slide object. If content is omitted, TutorFlow creates the default editable deck: 10 slides with generated image prompt markers on every fourth slide, starting with slide 1. If content is included, TutorFlow stores that content instead. The create response and the next GET /slides/{slideId} response include the stored content.

Common slide endpoints:

GET   /v1/content/classrooms/{classroomId}/slides
GET   /v1/content/classrooms/{classroomId}/slides/{slideId}
PATCH /v1/content/classrooms/{classroomId}/slides/{slideId}
DELETE /v1/content/classrooms/{classroomId}/slides/{slideId}

Update slide content:

{
  "title": "Safety training",
  "description": "Updated deck for the July onboarding cohort.",
  "content": "<slide><h1>Safety training</h1></slide>"
}

Tests

Create a test:

curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/tests" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: test:safety-check:v1" \
  -d '{
    "topicPrompt": "Safety check",
    "targetPrompt": "Assess basic workplace safety knowledge.",
    "level": "MEDIUM",
    "passingValueType": "PERCENTAGE",
    "passingValue": 70
  }'

If you omit level, totalScore, passingValueType, or passingValue, TutorFlow uses the Content API defaults: MEDIUM, 100, SCORE, and 80. A new test also receives 20 starter questions worth 5 points each, matching the default editable test setup in the admin UI.

Common test endpoints:

GET    /v1/content/classrooms/{classroomId}/tests?limit=20&page=1
GET    /v1/content/classrooms/{classroomId}/tests/{testId}
PATCH  /v1/content/classrooms/{classroomId}/tests/{testId}
DELETE /v1/content/classrooms/{classroomId}/tests/{testId}

Test item type values are select, blank, open-ended, true-false, and submission.

Update a test item:

{
  "name": "Safety check",
  "timeLimit": 20,
  "items": [
    {
      "question": "What should you do when you see a hazard?",
      "type": "select",
      "options": ["Report it", "Ignore it", "Hide it"],
      "correctAnswers": ["Report it"],
      "explanation": "Reporting hazards helps the team respond quickly.",
      "score": 10
    }
  ]
}

Idempotency

Send an Idempotency-Key on every create request and every side-effecting POST action such as module copy or video scene creation. Reusing the same key returns the original resource or action response instead of creating duplicates.

Good key patterns:

module:{sourceLessonId}:{sourceVersion}
course:{sourceLevelId}:{sourceVersion}
video:{sourceLevelId}:summary:{sourceVersion}
slides:{sourceDeckId}:{sourceVersion}
test:{sourceAssessmentId}:{sourceVersion}

External system checklist

  • Store TutorFlow resource IDs next to source system IDs.
  • Store the idempotency key used for each create request.
  • Use GET after POST before showing content to reviewers.
  • Treat videos created through Content API as editable classroom assets. Rendering is controlled from the TutorFlow UI until a dedicated Content API render endpoint is published.
  • Never store or display internal authoring tokens.
  • Use a separate Content API key for test and production.
  • Do not use /v1/platform/**, platform API keys, Agent Platform ownership identifiers, edit tokens, or Platform public URLs for content that people create or manage in TutorFlow UI.