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"Step 0: Get an admin session cookie
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.cookiesResponse:
[
{
"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" | jqResponse:
{
"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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThe 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" | jqStore 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:
| Field | Purpose |
|---|---|
job.id | Reconciliation with TutorFlow. |
job.status | Terminal state for the source content group. |
outputs[].outputType | interactive_module, summary_video, or expanded_quiz. |
outputs[].status | Per-output state. |
outputs[].resourceType | Returned content type. |
outputs[].resourceId | TutorFlow content id when available. |
outputs[].manifest | Data 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.