Use this guide for the first technical test with an external content system. It covers key creation, source JSON submission, polling, result retrieval, direct resource control, and the minimum checks before production automation.
Set the API base URL before running the test:
export TUTORFLOW_API_BASE_URL="https://api.tutorflow.io"1. Find the organization ID
After creating tutorflow-admin.cookies, list the organizations your admin account can manage:
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
-b tutorflow-admin.cookiesChoose the row for the organization you want to connect and set its id:
export TUTORFLOW_CONTENT_ORG_ID="$(curl -s "$TUTORFLOW_API_BASE_URL/v1/content/organizations" \
-b tutorflow-admin.cookies | jq -r '.[0].id')"If the list is empty, the signed-in account is not an admin for a manageable organization.
2. Create a test key
Create a Content Integration API key while signed in as a TutorFlow admin.
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" | jqThe response includes an apiKey that starts with tf_content_. Store the full value as a secret. Share only the key prefix in support messages.
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.
3. Find the target classroom
Use the Content API key to list classrooms available to the integration:
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/classrooms" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"Choose the classroom where TutorFlow should create generated modules, videos, tests, slides, and courses:
export TUTORFLOW_CLASSROOM_ID="$(curl -s "$TUTORFLOW_API_BASE_URL/v1/content/classrooms" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" | jq -r '.[0].id')"If TUTORFLOW_CLASSROOM_ID is empty or null, the Content API key is valid but has no classroom to operate on.
4. Prepare one small payload
Start with one category and one level, or one similarly small content group. Keep the customer's current lesson fields and include answer keys when they exist.
Save a test file named source-content.json:
{
"classroomId": "00000000-0000-4000-8000-000000000010",
"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 used when meeting someone",
"example": "Hello, Mina."
}
]
},
{
"id": "lesson-check-1",
"type": "quiz",
"title": "Greeting check",
"questions": [
{
"question": "Which phrase is a greeting?",
"choices": ["Hello", "Blue", "Desk"],
"answer": "Hello",
"explanation": "Hello is used to greet someone."
}
]
}
]
}
}
}Step 5 replaces the sample classroomId with TUTORFLOW_CLASSROOM_ID before sending the request.
5. Create an expansion job
Use a stable idempotency key for each source content group. If the same request is retried with the same key, TutorFlow returns the existing job instead of creating a duplicate.
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" | jqExpected response:
{
"id": "3f9440c4-7b15-48d7-a02f-4c1c50a8c3e1",
"sourceType": "source_json",
"status": "queued",
"sourceTitle": "Language Basics, A1 Foundations",
"requestedOutputs": ["interactive_module", "summary_video", "expanded_quiz"],
"outputs": [
{
"outputType": "interactive_module",
"status": "queued",
"manifest": null
},
{
"outputType": "summary_video",
"status": "queued",
"manifest": null
},
{
"outputType": "expanded_quiz",
"status": "queued",
"manifest": null
}
]
}Store the returned id as the TutorFlow job id:
export JOB_ID="$(printf '%s' "$JOB_RESPONSE" | jq -r '.id')"If JOB_ID is empty or null, inspect JOB_RESPONSE before polling. Do not continue with a placeholder id.
6. Poll until terminal status
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/$JOB_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"Status values:
| Status | Meaning | Client action |
|---|---|---|
queued | The request was accepted. | Poll again after 2 to 5 seconds. |
processing | TutorFlow is creating outputs. | Keep polling the same job id. |
completed | All requested outputs finished. | Call the result endpoint. |
failed | The job cannot finish. | Read error and fix the payload or requested outputs. |
Do not create a second job while the first job is still queued or processing.
7. Retrieve the result
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/integrations/expansions/$JOB_ID/result" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"Store these fields in the external system:
| Field | Why it matters |
|---|---|
job.id | Reconciliation key for TutorFlow support and retries. |
job.status | Terminal state for the content group. |
outputs[].outputType | Distinguishes module, video, and quiz outputs. |
outputs[].status | Per-output success or failure. |
outputs[].resourceType | Target content type represented by the manifest. |
outputs[].resourceId | TutorFlow content id for the generated module, video, or test. |
outputs[].manifest | Data to reinsert, review, or sync downstream. |
Minimum success criteria:
job.statusiscompleted.- Every requested
outputTypeappears once. - The interactive module output has a completed manifest.
- The summary video output returns editable video structure before MP4 rendering.
- The expanded quiz output preserves source answer intent.
8. Test direct resource control
Use the Content API key to read and update resources by id. The resourceId values returned by the result endpoint can be used directly.
export MODULE_ID="resource-id-from-interactive-module-output"
export TEST_ID="resource-id-from-expanded-quiz-output"
curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/modules/$MODULE_ID" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"
curl -sS -X PATCH "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$TUTORFLOW_CLASSROOM_ID/tests/$TEST_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-d '{
"name": "Updated assessment title"
}'Use Content Resource API for course, video, slide, test, and module examples.
Expected direct resource control behavior:
| Check | Expected result |
|---|---|
POST with a new Idempotency-Key | Creates one resource. |
Repeat the same POST with the same key and same body | Returns the original resource. |
| Repeat the same key with a different body | Returns 409. |
GET the returned resource id | Returns the classroom-owned resource without internal edit tokens. |
PATCH the returned resource id | Updates the same object visible in the TutorFlow UI. |
DELETE the returned resource id | Removes or soft-deletes that classroom resource according to the resource type. |
9. Add webhooks after polling works
Use polling for the first pilot. Add webhooks after job creation, polling, and result storage are verified.
Supported events:
| Event | Trigger |
|---|---|
content.completed | A Content Integration job reached completed. |
content.failed | A Content Integration job reached failed. |
Webhook requests include:
X-Content-Integration-Event: content.completed
X-Content-Integration-Signature: hmac_sha256_signatureThe receiver should verify the signature, return a 2xx response quickly, and fetch the result endpoint if it needs the full manifest.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 | Missing key or wrong key format. | Use a tf_content_ key in the Authorization header. |
400 | Payload has no level or no lessons. | Send one level or content group with at least one lesson. |
| Same job returned after retry | Same idempotency key. | This is expected. Change the key only when the source payload changes. |
Output has failed status | One generated output failed. | Inspect the output error, adjust the source content, and create a new job. |
| Video is not an MP4 | Content Integration returns editable video structure first. | Trigger rendering separately after review. |
Pilot handoff
At the end of the first pilot, share:
- The submitted source JSON.
- The idempotency key.
- The returned job id.
- The final result manifest.
- Any output review notes.
- Expected production request volume.
Notes for developers and AI coding agents
- Treat
/v1/content/**as the only API surface for human-owned content CRUD. Do not call/v1/platform/**in Content API integration tests. - Discover organization IDs and classroom IDs through the API. Do not hard-code values from examples.
- Use stable idempotency keys in generated scripts. Prefer
{resourceType}:{externalId}:{version}. - Keep the source JSON sample small for first tests, then scale after the result manifest is stored correctly.
- Redact full
tf_content_keys from logs and support transcripts. Keep onlykeyPrefix.