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

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.