사용 안내서
Content Integration Quickstart

Content Integration Quickstart

Create a Content API key, submit source JSON, poll status, and retrieve a result manifest.

Use this quickstart to run one complete Content Integration test. The flow creates a tf_content_ key, submits one small source JSON payload, polls the job, and retrieves the result manifest.

Prerequisites

  • A TutorFlow admin account with access to the target organization.
  • One small source JSON content group.
  • A secure place to store the generated tf_content_ key.

Set the API base URL:

export TUTORFLOW_API_BASE_URL="https://api.tutorflow.io"

Key management endpoints use the signed-in TutorFlow admin session. The session cookie is named jwt. If your admin account uses email and password sign-in, create a cookie jar before calling the key endpoint:

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, sign in through the TutorFlow admin UI and create the key from the authenticated admin environment.

Step 1: Find your organization ID

List the organizations your admin account can manage for Content Integration:

curl "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
  -b tutorflow-admin.cookies

Response:

[
  {
    "id": "00000000-0000-4000-8000-000000000001",
    "name": "Customer Academy",
    "slug": "customer-academy",
    "role": "ADMIN"
  }
]

Select an organization from the response. For a single-organization account, you can extract the first id with jq:

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

If the response is empty, your account is signed in but does not have admin access to an organization that can manage Content Integration.

Step 2: Create a Content API key

Create the key while signed in as a TutorFlow admin. The full key is returned once.

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

Response:

{
  "apiKey": "tf_content_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "keyId": "0b063e58-7e19-4787-90f0-d081c85f50d3",
  "keyPrefix": "tf_content_abc123def456",
  "name": "external-content-test",
  "rateLimitPerMinute": 60
}

Step 3: Save the bearer token

Store the full key in a secret manager. Do not log it.

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 continuing. Do not continue with a copied example value.

Use it on expansion, status, and result requests:

Authorization: Bearer tf_content_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The same key also authorizes direct resource endpoints such as /v1/content/classrooms/{classroomId}/modules, /v1/content/classrooms/{classroomId}/courses, /v1/content/classrooms/{classroomId}/videos, /v1/content/classrooms/{classroomId}/slides, and /v1/content/classrooms/{classroomId}/tests. See Content Resource API for those examples.

Step 4: Find the target classroom

List the classrooms available to this Content API key:

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"
  }
]

Select the classroom where generated modules, videos, tests, slides, and courses should appear:

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

Step 5: Submit source JSON

Save this as source-content.json:

{
  "classroomId": "00000000-0000-4000-8000-000000000010",
  "requestedOutputs": ["interactive_module", "summary_video", "expanded_quiz"],
  "payload": {
    "language": "en",
    "categories": [
      {
        "id": "language-basics",
        "title": "Language Basics",
        "levels": [
          {
            "id": "level-a1",
            "title": "A1 Foundations",
            "lessons": [
              {
                "id": "lesson-vocabulary-1",
                "type": "vocabulary",
                "title": "Basic greetings",
                "items": [
                  {
                    "term": "hello",
                    "meaning": "a greeting used when meeting someone",
                    "example": "Hello, Mina."
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  }
}

TutorFlow also accepts the compact single-level shape:

{
  "classroomId": "00000000-0000-4000-8000-000000000010",
  "payload": {
    "category": {
      "id": "language-basics",
      "title": "Language Basics"
    },
    "level": {
      "id": "level-a1",
      "title": "A1 Foundations",
      "lessons": [
        {
          "id": "lesson-vocabulary-1",
          "type": "vocabulary",
          "title": "Basic greetings"
        }
      ]
    }
  }
}

Create the job:

Use one idempotency key for one exact source content version. Retrying the same key with the same payload returns the existing job. Reusing the same key after changing the source JSON, classroom, or requested outputs returns 409.

JOB_RESPONSE="$(curl -sS -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 "$(jq --arg classroomId "$TUTORFLOW_CLASSROOM_ID" '.classroomId = $classroomId' source-content.json)")"
 
printf '%s\n' "$JOB_RESPONSE" | jq

Store the returned id:

export TUTORFLOW_CONTENT_JOB_ID="$(printf '%s' "$JOB_RESPONSE" | jq -r '.id')"

If TUTORFLOW_CONTENT_JOB_ID is empty or null, inspect JOB_RESPONSE before polling. Do not continue with a placeholder id.

Step 6: Poll status

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

Poll every 2 to 5 seconds until the job reaches completed or failed.

Step 7: Retrieve result

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

Store these fields:

FieldPurpose
job.idReconciliation with TutorFlow.
job.statusTerminal state for the source content group.
outputs[].outputTypeinteractive_module, summary_video, or expanded_quiz.
outputs[].statusPer-output state.
outputs[].resourceTypeReturned content type.
outputs[].resourceIdTutorFlow content id when available.
outputs[].manifestData to reinsert or review in the external system.

Step 8: Store the manifest

At minimum, store a record like this:

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

If the test fails

Send TutorFlow support:

  • Content job id.
  • Key prefix, never the full key.
  • Request timestamp.
  • Endpoint path.
  • Idempotency key.
  • Final error response.
  • Source JSON sample, if it can be shared.