Resources
Content Integration Testing Guide

Content Integration Testing Guide

End-to-end steps for testing Content Integration from key request to result storage.

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.cookies

Choose 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" | jq

The 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" | jq

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

StatusMeaningClient action
queuedThe request was accepted.Poll again after 2 to 5 seconds.
processingTutorFlow is creating outputs.Keep polling the same job id.
completedAll requested outputs finished.Call the result endpoint.
failedThe 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:

FieldWhy it matters
job.idReconciliation key for TutorFlow support and retries.
job.statusTerminal state for the content group.
outputs[].outputTypeDistinguishes module, video, and quiz outputs.
outputs[].statusPer-output success or failure.
outputs[].resourceTypeTarget content type represented by the manifest.
outputs[].resourceIdTutorFlow content id for the generated module, video, or test.
outputs[].manifestData to reinsert, review, or sync downstream.

Minimum success criteria:

  1. job.status is completed.
  2. Every requested outputType appears once.
  3. The interactive module output has a completed manifest.
  4. The summary video output returns editable video structure before MP4 rendering.
  5. 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:

CheckExpected result
POST with a new Idempotency-KeyCreates one resource.
Repeat the same POST with the same key and same bodyReturns the original resource.
Repeat the same key with a different bodyReturns 409.
GET the returned resource idReturns the classroom-owned resource without internal edit tokens.
PATCH the returned resource idUpdates the same object visible in the TutorFlow UI.
DELETE the returned resource idRemoves 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:

EventTrigger
content.completedA Content Integration job reached completed.
content.failedA Content Integration job reached failed.

Webhook requests include:

X-Content-Integration-Event: content.completed
X-Content-Integration-Signature: hmac_sha256_signature

The receiver should verify the signature, return a 2xx response quickly, and fetch the result endpoint if it needs the full manifest.

Troubleshooting

SymptomLikely causeFix
401Missing key or wrong key format.Use a tf_content_ key in the Authorization header.
400Payload has no level or no lessons.Send one level or content group with at least one lesson.
Same job returned after retrySame idempotency key.This is expected. Change the key only when the source payload changes.
Output has failed statusOne generated output failed.Inspect the output error, adjust the source content, and create a new job.
Video is not an MP4Content Integration returns editable video structure first.Trigger rendering separately after review.

Pilot handoff

At the end of the first pilot, share:

  1. The submitted source JSON.
  2. The idempotency key.
  3. The returned job id.
  4. The final result manifest.
  5. Any output review notes.
  6. 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 only keyPrefix.