Use the Content Integration API when an external system needs to send existing source content into TutorFlow or control content authored in TutorFlow. For direct courses, videos, slides, tests, and modules, see Content Resource API. For the full endpoint list, see the API Reference.
Authentication
Expansion and resource endpoints use a dedicated Content Integration API key scoped to one TutorFlow organization. Create the key with a signed-in TutorFlow admin session before sending source JSON or calling resource endpoints.
For email and password admin accounts, first create an admin cookie jar:
export TUTORFLOW_API_BASE_URL="https://api.tutorflow.io"
curl -c tutorflow-admin.cookies -X POST "$TUTORFLOW_API_BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-H "Referer: https://tutorflow.io/sign-in" \
-d '{
"email": "admin@example.com",
"password": "your-password",
"timezone": "Asia/Seoul"
}'curl "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
-b tutorflow-admin.cookiesChoose the organization id from that response, then create the Content API key:
export TUTORFLOW_CONTENT_ORG_ID="$(curl -s "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
-b tutorflow-admin.cookies | jq -r '.[0].id')"
KEY_RESPONSE="$(curl -sS -X POST "$TUTORFLOW_API_BASE_URL/v1/content/organizations/$TUTORFLOW_CONTENT_ORG_ID/api-keys" \
-H "Content-Type: application/json" \
-b tutorflow-admin.cookies \
-d '{
"name": "external-content-test",
"rateLimitPerMinute": 60
}')"
printf '%s\n' "$KEY_RESPONSE" | jqThe key format starts with tf_content_. Requests with any other bearer token format are rejected.
TutorFlow returns the full key once. Store it in a secret manager and keep only the visible prefix in logs or support tickets. Use a separate key per environment so test retries, pilot payloads, and production automation can be rate-limited and rotated independently.
export TUTORFLOW_CONTENT_API_KEY="$(printf '%s' "$KEY_RESPONSE" | jq -r '.apiKey')"
export TUTORFLOW_CONTENT_KEY_PREFIX="$(printf '%s' "$KEY_RESPONSE" | jq -r '.keyPrefix')"If TUTORFLOW_CONTENT_API_KEY is empty or null, inspect KEY_RESPONSE before sending requests.
Authorization: Bearer tf_content_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: source-content-level-a1-2026-07-02Recommended setup steps:
- Create one test key and one production key.
- Store the key in the external system's secret manager.
- Send one small content group with an
Idempotency-Key. - Poll the returned job id until the status is
completedorfailed. - Store the result manifest in the external system.
- Rotate the key if the integration owner or environment changes.
After key creation, external systems should use the tf_content_ bearer key for expansion and direct resource control. Admin sessions are only needed for key and webhook management.
Find the target classroom
Use the Content API key to list classrooms in the key's organization:
curl "$TUTORFLOW_API_BASE_URL/v1/content/classrooms" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"Response:
[
{
"id": "00000000-0000-4000-8000-000000000010",
"name": "Customer Academy",
"slug": "customer-academy",
"locale": "en"
}
]Use the returned id as classroomId when submitting expansion jobs or controlling content resources.
Create expansion
POST /v1/content/integrations/expansionsRequest:
{
"classroomId": "00000000-0000-4000-8000-000000000010",
"requestedOutputs": [
"interactive_module",
"summary_video",
"expanded_quiz"
],
"payload": {
"category": "Language Basics",
"level": {
"id": "level-a1",
"title": "A1 Foundations",
"lessons": [
{
"id": "lesson-1",
"type": "vocabulary",
"title": "Greetings",
"items": [
{
"term": "hello",
"meaning": "a greeting",
"example": "Hello, Min."
}
]
}
]
}
}
}Request fields:
| Field | Type | Required | Notes |
|---|---|---|---|
classroomId | uuid | Yes | Target classroom returned by GET /v1/content/classrooms. |
requestedOutputs | string[] | No | Defaults to all supported outputs in priority order. Supported values are interactive_module, summary_video, and expanded_quiz. |
payload | object | Yes | Existing source JSON. Send the customer's current structure rather than converting to a TutorFlow format. |
sourceType | string | No | Omit for the default source_json. |
idempotencyKey | string | No | Optional body-level idempotency key. The Idempotency-Key header is preferred for integration clients. |
curl example:
curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: source-content-level-a1-2026-07-02" \
-d '{
"classroomId": "00000000-0000-4000-8000-000000000010",
"requestedOutputs": ["interactive_module", "summary_video", "expanded_quiz"],
"payload": {
"category": "Language Basics",
"level": {
"id": "level-a1",
"title": "A1 Foundations",
"lessons": [
{
"id": "lesson-1",
"type": "vocabulary",
"title": "Greetings",
"items": [
{
"term": "hello",
"meaning": "a greeting",
"example": "Hello, Min."
}
]
}
]
}
}
}'Response:
{
"id": "3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1",
"sourceType": "source_json",
"status": "queued",
"sourceTitle": "Language Basics, A1 Foundations",
"requestedOutputs": [
"interactive_module",
"summary_video",
"expanded_quiz"
],
"classroomId": "00000000-0000-4000-8000-000000000010",
"sourceResourceId": null,
"sourceResourceType": null,
"error": null,
"completedAt": null,
"createdAt": "2026-07-03T04:00:00.000Z",
"updatedAt": "2026-07-03T04:00:00.000Z",
"outputs": [
{
"id": "9d3f5286-836c-4fe2-a939-9b8c4dd3aa92",
"outputType": "interactive_module",
"status": "queued",
"resourceType": null,
"resourceId": null,
"manifest": null,
"error": null,
"completedAt": null
},
{
"id": "20ae2e83-b45b-4f21-b1a0-a51962bb138d",
"outputType": "summary_video",
"status": "queued",
"resourceType": null,
"resourceId": null,
"manifest": null,
"error": null,
"completedAt": null
},
{
"id": "c2b752db-2a5e-44af-b2c5-00b928c3b7ed",
"outputType": "expanded_quiz",
"status": "queued",
"resourceType": null,
"resourceId": null,
"manifest": null,
"error": null,
"completedAt": null
}
]
}Response fields:
| Field | Notes |
|---|---|
id | Store this job id in the external system. Use it for status polling and result retrieval. |
status | queued, processing, completed, or failed. |
sourceTitle | Human-readable title inferred from the submitted source JSON. |
outputs | One output row per requested output. Each row later receives a manifest or an error. |
resourceId | TutorFlow content request id for the generated module, video, or test once that output is completed. It is null only before the output finishes or if generation fails before a resource is created. |
If the bearer token is missing or does not use the Content Integration key format, the API rejects the request:
{
"statusCode": 401,
"message": "Invalid or missing Content Integration API key",
"error": "Unauthorized"
}Get status
GET /v1/content/integrations/expansions/{jobId}Use this endpoint while the job is queued or processing.
Recommended polling interval is 2 to 5 seconds for a pilot. Stop polling after a terminal status: completed or failed. Reuse the same job id instead of resubmitting the payload unless the source content changes.
curl example:
curl "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"Response:
{
"id": "3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1",
"sourceType": "source_json",
"status": "processing",
"sourceTitle": "Language Basics, A1 Foundations",
"requestedOutputs": [
"interactive_module",
"summary_video",
"expanded_quiz"
],
"outputs": [
{
"outputType": "interactive_module",
"status": "processing",
"resourceType": null,
"resourceId": null,
"manifest": null,
"error": null,
"completedAt": null
}
]
}When status is failed, inspect error.code, error.message, and each output row's error. Fix the source payload or requested outputs before creating a new job.
Get result
GET /v1/content/integrations/expansions/{jobId}/resultThe result includes one manifest per output.
Treat the result as the handoff contract between TutorFlow and the external system. Store job.id, each outputType, the resourceType, the resourceId, and the full manifest payload. The resourceId can be used with /v1/content/classrooms/{classroomId}/modules, /v1/content/classrooms/{classroomId}/videos, or /v1/content/classrooms/{classroomId}/tests depending on the returned resourceType.
curl example:
curl "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1/result" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"{
"job": {
"id": "3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1",
"sourceType": "source_json",
"status": "completed",
"sourceTitle": "Language Basics, A1 Foundations",
"requestedOutputs": [
"interactive_module",
"summary_video",
"expanded_quiz"
],
"classroomId": "00000000-0000-4000-8000-000000000010",
"sourceResourceId": null,
"sourceResourceType": null,
"error": null,
"completedAt": "2026-07-03T04:00:05.000Z",
"createdAt": "2026-07-03T04:00:00.000Z",
"updatedAt": "2026-07-03T04:00:05.000Z"
},
"outputs": [
{
"id": "9d3f5286-836c-4fe2-a939-9b8c4dd3aa92",
"outputType": "interactive_module",
"status": "completed",
"resourceType": "module",
"resourceId": "module-request-id",
"manifest": {
"outputType": "interactive_module",
"resourceType": "module",
"resourceId": "module-request-id",
"title": "A1 Foundations module",
"description": "Interactive module generated from Language Basics, A1 Foundations.",
"status": "completed",
"urls": {
"publicUrl": null,
"previewUrl": null
},
"externalRefs": {
"videoId": null,
"classroomTestId": null
},
"completedAt": "2026-07-03T04:00:05.000Z",
"raw": {
"title": "A1 Foundations module",
"language": "en",
"hasQuiz": true,
"id": "module-request-id"
}
},
"error": null,
"completedAt": "2026-07-03T04:00:05.000Z"
},
{
"id": "20ae2e83-b45b-4f21-b1a0-a51962bb138d",
"outputType": "summary_video",
"status": "completed",
"resourceType": "video",
"resourceId": "video-request-id",
"manifest": {
"outputType": "summary_video",
"resourceType": "video",
"resourceId": "video-request-id",
"title": "A1 Foundations summary",
"description": "Summary video generated from Language Basics, A1 Foundations.",
"status": "completed",
"raw": {
"sceneCount": 3,
"aspectRatio": "16:9",
"targetDurationSeconds": 45,
"id": "video-request-id",
"mode": "sync"
}
},
"error": null,
"completedAt": "2026-07-03T04:00:05.000Z"
},
{
"id": "c2b752db-2a5e-44af-b2c5-00b928c3b7ed",
"outputType": "expanded_quiz",
"status": "completed",
"resourceType": "test",
"resourceId": "test-request-id",
"manifest": {
"outputType": "expanded_quiz",
"resourceType": "test",
"resourceId": "test-request-id",
"title": "A1 Foundations quiz",
"description": "Expanded quiz generated from Language Basics, A1 Foundations.",
"status": "completed",
"raw": {
"subject": "Language Basics",
"itemCount": 10,
"timeLimit": 30,
"id": "test-request-id",
"mode": "sync"
}
},
"error": null,
"completedAt": "2026-07-03T04:00:05.000Z"
}
],
"manifest": {
"id": "3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1",
"status": "completed",
"sourceType": "source_json",
"sourceTitle": "Language Basics, A1 Foundations",
"outputs": [
{
"outputType": "interactive_module",
"resourceType": "module",
"resourceId": "module-request-id",
"title": "A1 Foundations module"
},
{
"outputType": "summary_video",
"resourceType": "video",
"resourceId": "video-request-id",
"title": "A1 Foundations summary"
},
{
"outputType": "expanded_quiz",
"resourceType": "test",
"resourceId": "test-request-id",
"title": "A1 Foundations quiz"
}
]
}
}Control TutorFlow-authored content
Use Content Resource API endpoints for content created or maintained through TutorFlow. The external system only needs the Content API key and the resource id returned by create, list, expansion result, or a previous sync.
POST /v1/content/classrooms/{classroomId}/modules
GET /v1/content/classrooms/{classroomId}/modules
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
POST /v1/content/classrooms/{classroomId}/courses
GET /v1/content/classrooms/{classroomId}/courses
GET /v1/content/classrooms/{classroomId}/courses/{courseId}
PATCH /v1/content/classrooms/{classroomId}/courses/{courseId}
DELETE /v1/content/classrooms/{classroomId}/courses/{courseId}
POST /v1/content/classrooms/{classroomId}/videos
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
POST /v1/content/classrooms/{classroomId}/slides
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}
POST /v1/content/classrooms/{classroomId}/tests
GET /v1/content/classrooms/{classroomId}/tests
GET /v1/content/classrooms/{classroomId}/tests/{testId}
PATCH /v1/content/classrooms/{classroomId}/tests/{testId}
DELETE /v1/content/classrooms/{classroomId}/tests/{testId}Use Content Resource API for concrete request and response examples, including resource-specific delete responses and video scene payloads.
Optional legacy standalone lesson export
POST /v1/content/classrooms/{classroomId}/integrations/exportsSkip this endpoint for new integrations. Use it only when TutorFlow support confirms that an existing standalone lesson is not represented by the course, video, slide, test, or module resource APIs. It uses the signed-in TutorFlow admin session because it exports from a specific classroom. For courses, videos, slides, tests, and modules, use the resource endpoints above because they use the same tf_content_ bearer key as the rest of Content Integration.
Request:
{
"lessonId": "standalone-lesson-id",
"idempotencyKey": "standalone-lesson:standalone-lesson-id"
}Response:
{
"job": {
"id": "export-job-id",
"sourceType": "classroom",
"status": "completed"
},
"outputs": [
{
"outputType": "interactive_module",
"resourceType": "standalone_lesson",
"resourceId": "standalone-lesson-id",
"manifest": {
"title": "Existing TutorFlow lesson",
"source": "tutorflow_ui"
}
}
]
}This creates a completed Content Integration result for an existing standalone lesson.
Response handling is the same as expansion results: store the job.id, resourceType, resourceId, and manifest. The export endpoint does not enqueue generation because the content already exists.
Webhooks
Subscribe to these events after the polling flow is verified:
content.completedcontent.failed
Webhook payloads include job id, status, source type, requested outputs, and completion time.
Webhook requests use Content Integration headers:
X-Content-Integration-Event: content.completed
X-Content-Integration-Signature: hmac_sha256_signatureRate limits and billing
- Rate limits follow the dedicated Content Integration API key settings.
- Interactive module, summary video, and expanded quiz generation use the existing TutorFlow billing meters for those content types.
- MP4 rendering is not triggered by Content Integration. Trigger rendering after reviewing generated video scenes.