資料
Source JSON Expansion

Source JSON Expansion

Send your own lesson JSON to TutorFlow, follow the expansion job, and store the module, video, and quiz it creates.

このページの内容

An expansion job takes one level of your existing lesson JSON and creates editable TutorFlow content from it in a classroom: an interactive module, a summary video plan, and an expanded quiz. Your JSON stays the input of record; the job result tells you which TutorFlow resources came from it.

The flow

  1. POST /v1/content/integrations/expansions with a classroom id, the outputs you want, and your JSON. Store the returned job id.
  2. Poll GET /v1/content/integrations/expansions/{jobId} until status is completed or failed, or subscribe to the content.completed and content.failed webhooks.
  3. Read GET /v1/content/integrations/expansions/{jobId}/result and store each output's resourceType, resourceId, and manifest next to your source ids.
  4. Review the created module, video, and test in TutorFlow, or read and edit them with the resource routes.

Outputs

outputTypeCreatesresourceTypeNext step
interactive_moduleA module (type: "text", isPublic: false) titled after the level, whose content holds a lesson brief built from your lessons.moduleReview and finish it in the TutorFlow module editor, or edit it with PATCH /modules/{moduleId}.
summary_videoA PRIVATE video titled after the level, with no scenes yet. It is not an mp4.videoAdd scenes with the scene routes, then render it.
expanded_quizA test titled after the level, with level: "MEDIUM", a 70% passing score, your lesson brief kept as its reference material, and no items yet.testReview and complete its items in the TutorFlow test editor, or with PATCH /tests/{testId}.

Outputs always run in this order. Omit requestedOutputs to get all three.

Create a job

HTTP
POST /v1/content/integrations/expansions
Authorization: Bearer tf_content_...
Content-Type: application/json
Idempotency-Key: expansion:language-basics-a1:v1
JSON
{
  "classroomId": "00000000-0000-4000-8000-000000000010",
  "requestedOutputs": ["interactive_module", "summary_video", "expanded_quiz"],
  "payload": {
    "language": "en",
    "category": { "id": "language-basics", "title": "Language Basics" },
    "level": {
      "id": "level-a1",
      "title": "A1 Foundations",
      "lessons": [
        {
          "id": "lesson-greetings",
          "type": "vocabulary",
          "title": "Basic greetings",
          "items": [{ "term": "hello", "meaning": "a greeting", "example": "Hello, Mina." }]
        }
      ]
    }
  }
}
FieldTypeRequiredNotes
classroomIdUUIDYesFrom GET /v1/content/classrooms.
requestedOutputsstring[]NoAny of interactive_module, summary_video, expanded_quiz. Defaults to all three.
payloadobjectYesYour source JSON. See Source JSON Format.
sourceTypestringNoOnly source_json, the default. Any other value returns 400.
idempotencyKeystringNoSame as the Idempotency-Key header, up to 255 characters. If both are sent, the body field is used.

Before the job is queued, TutorFlow checks that payload contains a level with at least one lesson (400 if not). Expansion jobs are free: they create editable drafts without calling a model, so no credit balance is needed.

Response 202:

JSON
{
  "id": "00000000-0000-4000-8000-000000000030",
  "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-09-30T09:00:00.000Z",
  "updatedAt": "2026-09-30T09:00:00.000Z",
  "outputs": [
    {
      "id": "00000000-0000-4000-8000-000000000031",
      "outputType": "interactive_module",
      "status": "queued",
      "resourceType": null,
      "resourceId": null,
      "manifest": null,
      "error": null,
      "completedAt": null,
      "creditAmount": 0,
      "creditChargedAt": null
    }
  ]
}

The response lists one output row per requested output; the example shows one.

Idempotency for expansion jobs

Expansion jobs follow the same idempotency rules as every other create. Send the key in the Idempotency-Key header or as idempotencyKey in the body; moving it from one to the other is still the same request.

RequestResponse
Same key and same body within 24 hours202 with the first job in its current state (status and outputs), plus Idempotent-Replayed: true. No new job is queued.
Same key, different body409 content_conflict.
Same key while the first request is still being accepted409 ("already in progress"). Retry after a moment.
Same key after 24 hoursA new job.
Same key string in another classroomA separate job. Keys are scoped to your API key and the classroomId.

So:

  • Use one key per source content version, for example expansion:{sourceLevelId}:v{sourceVersion}.
  • When the source changes, change the key. The same key with the changed source returns 409.
  • Never generate the key from the current time; a retry after a timeout must send the same key.

Jobs created before 2026-09-30 with an idempotencyKey are still returned for that key, organization-wide and with no expiry, now with Idempotent-Replayed: true. Such a key that belongs to a job in a classroom your key is not allowed to use returns 409.

Poll the job

HTTP
GET /v1/content/integrations/expansions/{jobId}
Authorization: Bearer tf_content_...

The response has the same shape as the create response.

statusMeaningWhat to do
queuedAccepted, waiting to run.Poll again in 3 to 5 seconds.
processingCreating outputs. A job that TutorFlow is trying again stays processing.Keep polling the same job id.
completedEvery requested output finished.Read the result.
failedThe job cannot finish. TutorFlow already tried it 3 times.Read error and each outputs[].error, fix the source, and submit it with a new key.

A job is tried up to 3 times, 5 and then 25 seconds apart. The content.failed webhook is sent once, after the final attempt; attempts that are tried again send nothing. A job whose server stops is picked up again by another server after about two minutes, and an attempt cut short by a normal server restart is not counted. In a rare case, a job interrupted by a restart right after it finished can send content.completed twice, so skip job ids you have already handled.

A copy-paste poll loop that honors 429 is in the Quickstart, and in Node.js and Python in Examples.

Read the result

HTTP
GET /v1/content/integrations/expansions/{jobId}/result
Authorization: Bearer tf_content_...

The result has three parts: job (the job fields), outputs (one row per output), and manifest (the job id, status, and every output's manifest in one object).

JSON
{
  "job": {
    "id": "00000000-0000-4000-8000-000000000030",
    "sourceType": "source_json",
    "status": "completed",
    "sourceTitle": "Language Basics, A1 Foundations",
    "requestedOutputs": ["interactive_module", "summary_video", "expanded_quiz"],
    "classroomId": "00000000-0000-4000-8000-000000000010",
    "error": null,
    "completedAt": "2026-09-30T09:00:08.000Z",
    "createdAt": "2026-09-30T09:00:00.000Z",
    "updatedAt": "2026-09-30T09:00:08.000Z"
  },
  "outputs": [
    {
      "id": "00000000-0000-4000-8000-000000000031",
      "outputType": "interactive_module",
      "status": "completed",
      "resourceType": "module",
      "resourceId": "00000000-0000-4000-8000-000000000101",
      "manifest": {
        "outputType": "interactive_module",
        "resourceType": "module",
        "resourceId": "00000000-0000-4000-8000-000000000101",
        "title": "A1 Foundations module",
        "description": "Interactive module generated from Language Basics, A1 Foundations.",
        "status": "ready",
        "urls": { "publicUrl": null, "previewUrl": null },
        "externalRefs": { "videoId": null, "classroomTestId": null },
        "completedAt": null,
        "raw": {
          "id": "00000000-0000-4000-8000-000000000101",
          "title": "A1 Foundations module",
          "description": "Interactive module generated from Language Basics, A1 Foundations.",
          "type": "text",
          "status": "ready",
          "slug": "a1-foundations-module",
          "isPublic": false,
          "classroomId": "00000000-0000-4000-8000-000000000010",
          "createdAt": "2026-09-30T09:00:02.000Z",
          "updatedAt": "2026-09-30T09:00:02.000Z"
        }
      },
      "error": null,
      "completedAt": "2026-09-30T09:00:03.000Z",
      "creditAmount": 0,
      "creditChargedAt": null
    }
  ],
  "manifest": {
    "id": "00000000-0000-4000-8000-000000000030",
    "status": "completed",
    "sourceType": "source_json",
    "sourceTitle": "Language Basics, A1 Foundations",
    "outputs": [{ "outputType": "interactive_module", "resourceType": "module", "resourceId": "00000000-0000-4000-8000-000000000101" }]
  }
}

The example shows one output, and job and manifest.outputs are shortened. Store these fields:

FieldWhy
job.idReconciliation and support.
outputs[].outputTypeWhich output this is.
outputs[].statuscompleted or failed per output.
outputs[].resourceType, outputs[].resourceIdThe created module, video, or test. Use the id with /modules/{moduleId}, /videos/{videoId}, or /tests/{testId} in the job's classroom.
outputs[].manifestA summary of the created resource. title is the resource's title, or a test's name for expanded_quiz. raw is a short summary of the resource as it was created: whichever of id, title, name, description, type, status, slug, isPublic, visibility, classroomId, createdAt, and updatedAt it has, with dates as ISO 8601 strings. Read the resource route for its full, current state.
outputs[].creditAmount, outputs[].creditChargedAtAlways 0 and null: expansion outputs are not charged. Kept so existing parsers keep working.

Errors

Statuserror.codeCause
400content_invalid_requestNo level with at least one lesson in payload, an unsupported sourceType, or an idempotency key over 255 characters.
403content_insufficient_scope, content_classroom_not_allowedThe key lacks content:generate, or may not use the classroom.
404content_not_foundThe classroom, or the job on a status or result read, was not found or is in a classroom the key cannot use.
409content_conflictThe idempotency key was used with a different body, the first request with it is still being accepted, or it belongs to an older job in a classroom your key cannot use.
422content_invalid_requestThe body failed validation: classroomId is not a UUID, payload is not an object, or an output name is unknown. error.details names the field. A jobId in the path that is not a UUID also returns 422.

All errors use the envelope in Errors, for example:

JSON
{
  "error": {
    "code": "content_invalid_api_key",
    "message": "Invalid or missing Content Integration API key",
    "status": 401,
    "requestId": "3f0c2a9e-6b1d-4c8e-9f2a-7d5b1e0c4a11"
  }
}

このページは役に立ちましたか?