Resources
Content Integration API

Content Integration API

Create, poll, and retrieve Content Integration jobs for external content systems.

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.cookies

Choose 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" | jq

The 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-02

Recommended setup steps:

  1. Create one test key and one production key.
  2. Store the key in the external system's secret manager.
  3. Send one small content group with an Idempotency-Key.
  4. Poll the returned job id until the status is completed or failed.
  5. Store the result manifest in the external system.
  6. 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/expansions

Request:

{
  "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:

FieldTypeRequiredNotes
classroomIduuidYesTarget classroom returned by GET /v1/content/classrooms.
requestedOutputsstring[]NoDefaults to all supported outputs in priority order. Supported values are interactive_module, summary_video, and expanded_quiz.
payloadobjectYesExisting source JSON. Send the customer's current structure rather than converting to a TutorFlow format.
sourceTypestringNoOmit for the default source_json.
idempotencyKeystringNoOptional 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:

FieldNotes
idStore this job id in the external system. Use it for status polling and result retrieval.
statusqueued, processing, completed, or failed.
sourceTitleHuman-readable title inferred from the submitted source JSON.
outputsOne output row per requested output. Each row later receives a manifest or an error.
resourceIdTutorFlow 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}/result

The 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/exports

Skip 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.completed
  • content.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_signature

Rate 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.