사용 안내서
Content Integration Examples

Content Integration Examples

Copy-ready curl, Node.js, and Python examples for Content Integration.

These examples show the full path from key creation to result retrieval. Resource calls require a classroom id, discovered with the Content API key after key creation.

Before running the examples

The curl examples use jq and an admin session cookie. If your admin account uses email and password sign-in, create the cookie jar first:

set -euo pipefail
 
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"
  }'

If your organization uses SSO or OAuth sign-in, create the key from an authenticated TutorFlow admin environment instead of using the password-login example.

curl: find organization ID

curl "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
  -b tutorflow-admin.cookies
 
export TUTORFLOW_CONTENT_ORG_ID="$(curl -s "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
  -b tutorflow-admin.cookies | jq -r '.[0].id')"

curl: create key

KEY_RESPONSE="$(curl -s -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}')"
 
export TUTORFLOW_CONTENT_API_KEY="$(printf '%s' "$KEY_RESPONSE" | jq -r '.apiKey')"
export TUTORFLOW_CONTENT_KEY_PREFIX="$(printf '%s' "$KEY_RESPONSE" | jq -r '.keyPrefix')"

curl: find classroom ID

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

curl: check AI Credit balance

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

This returns the TutorFlow organization's normal credit, overdraft fields, availableCredit, and isPaymentFailed. It does not return Agent Platform credits.

curl: create expansion

cat > source-content.json <<JSON
{
  "classroomId": "$TUTORFLOW_CLASSROOM_ID",
  "requestedOutputs": ["interactive_module", "summary_video", "expanded_quiz"],
  "payload": {
    "category": {
      "id": "language-basics",
      "title": "Language Basics"
    },
    "language": "en",
    "level": {
      "id": "level-a1",
      "title": "A1 Foundations",
      "lessons": [
        {
          "id": "lesson-vocabulary-1",
          "type": "vocabulary",
          "title": "Basic greetings",
          "items": [
            {
              "term": "hello",
              "meaning": "a greeting"
            }
          ]
        }
      ]
    }
  }
}
JSON
 
JOB_RESPONSE="$(curl -s -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: language-basics-level-a1-2026-07-04" \
  --data-binary @source-content.json)"
 
export JOB_ID="$(printf '%s' "$JOB_RESPONSE" | jq -r '.id')"

curl: poll and retrieve

curl "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/$JOB_ID" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"
 
curl "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/$JOB_ID/result" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"

Node.js: create expansion

const tutorFlowApiBaseUrl =
  process.env.TUTORFLOW_API_BASE_URL ?? 'https://api.tutorflow.io'
 
const response = await fetch(
  `${tutorFlowApiBaseUrl}/v1/content/integrations/expansions`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.TUTORFLOW_CONTENT_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': 'language-basics-level-a1-2026-07-04',
    },
    body: JSON.stringify({
      classroomId: process.env.TUTORFLOW_CLASSROOM_ID,
      requestedOutputs: ['interactive_module', 'summary_video', 'expanded_quiz'],
      payload: {
        category: { id: 'language-basics', title: 'Language Basics' },
        language: 'en',
        level: {
          id: 'level-a1',
          title: 'A1 Foundations',
          lessons: [
            {
              id: 'lesson-vocabulary-1',
              type: 'vocabulary',
              title: 'Basic greetings',
              items: [{ term: 'hello', meaning: 'a greeting' }],
            },
          ],
        },
      },
    }),
  },
)
 
if (!response.ok) {
  throw new Error(await response.text())
}
 
const job = await response.json()

Node.js: polling helper

async function waitForContentJob(jobId) {
  const tutorFlowApiBaseUrl =
    process.env.TUTORFLOW_API_BASE_URL ?? 'https://api.tutorflow.io'
 
  for (let attempt = 0; attempt < 60; attempt++) {
    const response = await fetch(
      `${tutorFlowApiBaseUrl}/v1/content/integrations/expansions/${jobId}`,
      {
        headers: {
          Authorization: `Bearer ${process.env.TUTORFLOW_CONTENT_API_KEY}`,
        },
      },
    )
 
    if (!response.ok) {
      throw new Error(await response.text())
    }
 
    const job = await response.json()
 
    if (job.status === 'completed' || job.status === 'failed') {
      return job
    }
 
    await new Promise((resolve) => setTimeout(resolve, 3000))
  }
 
  throw new Error('Content job did not finish within the polling window')
}

Python: create expansion

import os
import requests
 
api_base_url = os.environ.get("TUTORFLOW_API_BASE_URL", "https://api.tutorflow.io")
 
response = requests.post(
    f"{api_base_url}/v1/content/integrations/expansions",
    headers={
        "Authorization": f"Bearer {os.environ['TUTORFLOW_CONTENT_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": "language-basics-level-a1-2026-07-04",
    },
    json={
        "classroomId": os.environ["TUTORFLOW_CLASSROOM_ID"],
        "requestedOutputs": ["interactive_module", "summary_video", "expanded_quiz"],
        "payload": {
            "category": {"id": "language-basics", "title": "Language Basics"},
            "language": "en",
            "level": {
                "id": "level-a1",
                "title": "A1 Foundations",
                "lessons": [
                    {
                        "id": "lesson-vocabulary-1",
                        "type": "vocabulary",
                        "title": "Basic greetings",
                        "items": [{"term": "hello", "meaning": "a greeting"}],
                    }
                ],
            },
        },
    },
    timeout=30,
)
response.raise_for_status()
job = response.json()

curl: create and build a game

Building streams Server-Sent Events. -N disables curl's buffering so events print as they arrive, and --max-time is set in minutes because hanging up aborts the build.

GAME_ID="$(curl -sS -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/games" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: game:fractions-run:v1" \
  -d '{
    "title": "Fractions run",
    "visibility": "PRIVATE",
    "metadata": {
      "topic": "Comparing fractions with unlike denominators",
      "audience": "Grade 5",
      "language": "en"
    }
  }' | jq -r '.id')"
 
# 1 credit. Ends with a brief-done event carrying the plan.
curl -sS -N --max-time 300 -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/games/$GAME_ID/brief" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
 
# 20 credits. Ends with build-done or error.
curl -sS -N --max-time 1200 -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/games/$GAME_ID/build" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"
 
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/games/$GAME_ID" \
  -H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" | jq '{contentVersion, buildHistory}'

Simulations use the same calls under /simulations: 1 credit for the brief, then 15 per build or revision, or 20 when the brief chose a 3D representation.

Node.js: build a game and read the stream

const tutorFlowApiBaseUrl =
  process.env.TUTORFLOW_API_BASE_URL ?? 'https://api.tutorflow.io'
 
async function readContentStream(response, onEvent) {
  const decoder = new TextDecoder()
  let buffer = ''
 
  for await (const chunk of response.body) {
    buffer += decoder.decode(chunk, { stream: true })
 
    let boundary = buffer.indexOf('\n\n')
 
    while (boundary !== -1) {
      const block = buffer.slice(0, boundary)
      buffer = buffer.slice(boundary + 2)
 
      const event = block.match(/^event: (.+)$/m)?.[1]
      const data = block.match(/^data: (.+)$/m)?.[1]
 
      if (event && data) {
        onEvent(event, JSON.parse(data))
      }
 
      boundary = buffer.indexOf('\n\n')
    }
  }
}
 
async function buildGame(classroomId, gameId) {
  const response = await fetch(
    `${tutorFlowApiBaseUrl}/v1/content/classrooms/${classroomId}/games/${gameId}/build`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.TUTORFLOW_CONTENT_API_KEY}`,
      },
      // A build runs for minutes. A shorter signal hangs up and aborts it.
      signal: AbortSignal.timeout(20 * 60 * 1000),
    },
  )
 
  // Credits, readiness, and ownership are checked before the stream opens,
  // so a non-2xx here is the normal JSON error envelope.
  if (!response.ok) {
    throw new Error(await response.text())
  }
 
  let result = null
 
  await readContentStream(response, (event, data) => {
    if (event === 'progress') {
      console.log(`stage: ${data.stage}`)
    } else if (event === 'build-done') {
      result = data
    } else if (event === 'error') {
      throw new Error(
        [data.message, ...(data.violations ?? [])].join(' | '),
      )
    }
  })
 
  if (!result) {
    throw new Error('Build stream ended without a terminal event')
  }
 
  return result
}

Manifest storage shape

{
  "sourceContentId": "language-basics:level-a1",
  "tutorFlowJobId": "3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1",
  "outputType": "interactive_module",
  "status": "completed",
  "resourceType": "module",
  "resourceId": "module-request-id",
  "manifest": {}
}