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": {}
}