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

On this page

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. It is an editable plan, not an mp4.videoReview the scenes, then render it.
expanded_quizA test titled after the level, with level: "MEDIUM", a 70% passing score, and your lesson brief kept as its reference material.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.Keep polling the same job id.
completedEvery requested output finished.Read the result.
failedThe job cannot finish. TutorFlow already retried it internally.Read error and each outputs[].error, fix the source, and submit it with a new key.

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", "type": "text", "status": "ready" }
      },
      "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. raw is the resource as it was created; read the resource route for its 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"
  }
}

Was this page helpful?