This quickstart takes you from no key to a module in your classroom. You need a TutorFlow admin account for the organization, curl, and jq.
1. Create a key
- In TutorFlow, open Settings > Content API (
/dashboard/settings/content-api). - Create a key named, for example,
quickstart, and choose Test under Mode (the default is Live). A test key works only in a private sandbox classroom, answers generation and renders with canned content, and never spends AI Credits or touches real classrooms, so it is the recommended way to start. See Test Mode. - Choose scopes. For this quickstart pick
content:readandcontent:write. Addcontent:generateif you want to try the optional expansion in step 5. - Copy the key. A test key starts with
tf_content_test_, a live key withtf_content_. It is shown only once.
Scripting key creation instead? See Create a key with the API.
Set it in your shell:
export TUTORFLOW_API_BASE_URL="https://api.tutorflow.io"
export TUTORFLOW_CONTENT_API_KEY="tf_content_..." # paste your key2. Make your first call
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/credits" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" | jq{
"credit": 500,
"overdraftCredit": 0,
"overdraftLimit": 100,
"availableCredit": 500,
"isPaymentFailed": false,
"livemode": true
}A 200 means the key works. With a live key, the balance is the organization's real balance. A test key sees a fixed sandbox balance with livemode: false instead, and never changes the real one. A 401 means the key is missing, mistyped, revoked, or expired.
3. Pick a classroom
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/classrooms" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" | jq[
{
"id": "00000000-0000-4000-8000-000000000010",
"name": "API sandbox",
"slug": "api-sandbox-1a2b3c4d",
"locale": "en",
"isSandbox": true
}
]A test key lists only the sandbox classroom. A live key lists the organization's classrooms, or those in its allowlist, with isSandbox: false. Copy the id of the classroom you want:
export CLASSROOM_ID="00000000-0000-4000-8000-000000000010" # your classroom id4. Create a module
MODULE_JSON="$(curl -sS -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/modules" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: module:quickstart-safety:v1" \
-d '{
"title": "Introduction to safety",
"description": "A short interactive lesson for new employees.",
"type": "markdown",
"content": "<h2>Safety first</h2><p>Report every hazard you see.</p>",
"metadata": { "externalId": "quickstart-safety" }
}')"
printf '%s\n' "$MODULE_JSON" | jq '{id, classroomId, title, createdAt}'
export MODULE_ID="$(printf '%s' "$MODULE_JSON" | jq -r '.id')"The response is 201 with the module. Open the classroom in TutorFlow and the module is there; with a test key, it is in the API sandbox. Running the same command again with the same Idempotency-Key returns the same module instead of creating a second one.
That is a complete integration loop: authenticate, find a classroom, write content, and read back its id. Creating and updating content you supply costs no credits.
5. Optional: expand source JSON
Expansion turns your own lesson JSON into a module, a video plan, and a quiz. It needs content:generate and is free with any key: the outputs are editable drafts, created without calling a model. This request asks for the module output only.
JOB_JSON="$(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: expansion:language-basics-a1:v1" \
-d "$(jq -n --arg classroomId "$CLASSROOM_ID" '{
classroomId: $classroomId,
requestedOutputs: ["interactive_module"],
payload: {
language: "en",
category: { id: "language-basics", title: "Language Basics" },
level: {
id: "level-a1",
title: "A1 Foundations",
lessons: [
{
id: "lesson-greetings",
type: "vocabulary",
title: "Basic greetings",
items: [{ term: "hello", meaning: "a greeting", example: "Hello, Mina." }]
}
]
}
}
}')")"
export JOB_ID="$(printf '%s' "$JOB_JSON" | jq -r '.id')"
echo "job: $JOB_ID"Poll until the job finishes. The loop waits longer when the API answers 429:
while true; do
RESPONSE="$(curl -sS -w '\n%{http_code}' "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/$JOB_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY")"
HTTP_STATUS="$(printf '%s' "$RESPONSE" | tail -n 1)"
BODY="$(printf '%s' "$RESPONSE" | sed '$d')"
if [ "$HTTP_STATUS" = "429" ]; then sleep 30; continue; fi
JOB_STATUS="$(printf '%s' "$BODY" | jq -r '.status')"
echo "status: $JOB_STATUS"
if [ "$JOB_STATUS" = "completed" ] || [ "$JOB_STATUS" = "failed" ]; then break; fi
sleep 5
done
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/$JOB_ID/result" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
| jq '.outputs[] | {outputType, status, resourceType, resourceId}'The resourceId of the interactive_module output is a module id you can read with GET /modules/{moduleId}.
Next steps
- Test Mode: what a test key does instead of spending, and how to go live.
- Source JSON Expansion and Source JSON Format for the full expansion flow.
- Resources for courses, videos, slides, tests, games, and simulations.
- Webhooks to stop polling.
- Errors for what each status means and whether to retry.