Content Ingestion

Upload QTI 3.0 and IMS Common Cartridge zip packages to make learning content available through the Content API — questions, articles, stimuli, videos, and organized containers.

Note: This guide covers the ingestion workflow and package requirements. See the API Reference for complete endpoint documentation, all parameters, and example requests.

Prerequisites

Before ingesting content:

  1. Obtain OAuth credentials with the content.write and content.read scopes (see Authentication Guide)
  2. Prepare a valid zip package in either QTI 3.0 or IMS Common Cartridge format (see Supported Package Types below)

Request both scopes when registering:

scope: https://timeback-platform.trilogy.com/content/scope/content.write
scope: https://timeback-platform.trilogy.com/content/scope/content.read

Supported Package Types

TimeBack accepts two standard content package formats:

Format Description Content Produced
QTI 3.0 IMS Question & Test Interoperability packages containing assessment items, stimuli, and articles QUESTION, STIMULUS, ARTICLE
IMS Common Cartridge Broader course packages that can include QTI items, HTML articles, video web links, and an organizational hierarchy QUESTION, STIMULUS, ARTICLE, CONTAINER, VIDEO

Both formats must be uploaded as a single .zip file containing an imsmanifest.xml at the root. The system auto-detects which format the package uses — you do not need to specify the type.

Package Requirements

Every zip package must include:

  • imsmanifest.xml at the zip root — the manifest describes all resources and their relationships
  • LOM metadata on each resource — including a title and at least one CASE standard identifier (curriculum alignment)
  • CASE standard linkage — every resource must be linked to at least one CASE curriculum item. Resources without CASE standards are rejected.

QTI 3.0 Packages

QTI packages contain resources with types:

Resource Type Type Identifier Content Created
Assessment Item imsqti_item_xmlv3p0 QUESTION
Stimulus imsqti_stimulus_xmlv3p0 STIMULUS
Article imsqti_article_xmlv3p0 ARTICLE

Resources reference each other via <dependency> elements in the manifest. For example, a question that depends on a stimulus resource will have a STIMULUS association created automatically.

Common Cartridge Packages

Common Cartridge packages support a wider variety of resource types:

Resource Type Type Identifier Content Created
QTI Assessment Item imsqti_item_xmlv3p0 (or any type containing imsqti/assessment) QUESTION
HTML Web Content webcontent (with .html/.htm href) ARTICLE
IMS Web Link (video) imswl_xmlv1p1 (when metadata indicates video MIME type or video origin like YouTube/Vimeo) VIDEO
Organization Items (no identifierref) Inferred from <organization> structure CONTAINER

CC packages also use the <organizations> section of the manifest to define a hierarchy. Items within organizations that reference resources become IS_CHILD_OF associations to their parent container, preserving sort order.

Package Type Detection

The system determines the package format by inspecting imsmanifest.xml. A package is classified as Common Cartridge if the manifest matches any of these patterns:

  • An xmlns attribute referencing http://www.imsglobal.org/xsd/imscc
  • A type attribute starting with imscc_
  • A <schema> element containing IMS Common Cartridge

If none of these patterns match, the package is treated as QTI 3.0.

Ingestion Workflow

The ingestion process has three steps: obtain an upload URL, upload the zip file, and poll for completion.

Step 1: Obtain a Presigned Upload URL

Request a presigned S3 URL by providing the item count and zip file size:

curl -X POST https://platform.timeback.com/content/1.0/ingest/upload-url \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -d '{
    "itemCount": 25,
    "zipFileSize": 1048576
  }'

Response:

{
  "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "uploadUrl": "https://s3.amazonaws.com/...",
  "expiresAt": "2026-02-19T14:00:00.000Z"
}

The presigned URL expires after 1 hour. The jobId is used to track processing status.

async function requestUploadUrl(
  accessToken: string,
  itemCount: number,
  zipFileSize: number,
): Promise<{ jobId: string; uploadUrl: string; expiresAt: string }> {
  const response = await fetch('https://platform.timeback.com/content/1.0/ingest/upload-url', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({ itemCount, zipFileSize }),
  });

  return await response.json();
}

Step 2: Upload the Zip File

Upload your zip file directly to the presigned S3 URL using an HTTP PUT request:

curl -X PUT "<UPLOAD_URL>" \
  -H "Content-Type: application/zip" \
  --data-binary @package.zip
async function uploadZipFile(uploadUrl: string, zipBuffer: Buffer): Promise<void> {
  const response = await fetch(uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/zip' },
    body: zipBuffer,
  });

  if (!response.ok) {
    throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
  }
}

Once the upload completes, processing begins automatically. The system downloads the zip, detects the package type, and processes each resource.

Step 3: Poll for Job Status

Poll the job status endpoint until processing completes:

curl https://platform.timeback.com/content/1.0/ingest/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

Response (processing):

{
  "status": "PROCESSING",
  "lastUpdated": "2026-02-19T13:05:00.000Z",
  "itemStatuses": [
    {
      "itemId": "item-001",
      "status": "COMPLETED",
      "lastUpdated": "2026-02-19T13:04:55.000Z"
    },
    {
      "itemId": "item-002",
      "status": "PROCESSING",
      "lastUpdated": "2026-02-19T13:05:00.000Z"
    }
  ]
}

Response (completed):

{
  "status": "COMPLETED",
  "lastUpdated": "2026-02-19T13:06:00.000Z",
  "itemStatuses": [
    {
      "itemId": "item-001",
      "status": "COMPLETED",
      "lastUpdated": "2026-02-19T13:04:55.000Z"
    },
    {
      "itemId": "item-002",
      "status": "COMPLETED",
      "lastUpdated": "2026-02-19T13:05:30.000Z"
    }
  ]
}
async function pollJobStatus(
  accessToken: string,
  jobId: string,
  intervalMs = 5000,
): Promise<{ status: string; itemStatuses: Array<{ itemId: string; status: string; message?: string }> }> {
  const terminalStatuses = new Set(['COMPLETED', 'FAILED', 'COMPLETED_WITH_ERRORS']);

  while (true) {
    const response = await fetch(`https://platform.timeback.com/content/1.0/ingest/jobs/${jobId}`, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    const job = await response.json();

    if (terminalStatuses.has(job.status)) {
      return job;
    }

    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
}

Job Status Reference

Job-Level Statuses

Status Description
AWAITING_UPLOAD Job created. The presigned URL has been issued but the zip file has not been uploaded yet.
PROCESSING Zip file received. The system is extracting and processing individual resources.
COMPLETED All resources processed successfully.
FAILED All resources failed to process, or an unrecoverable error occurred.
COMPLETED_WITH_ERRORS Some resources processed successfully but others failed. Check itemStatuses for details.

Item-Level Statuses

Status Description
PROCESSING The individual resource is currently being processed.
COMPLETED The resource was processed and a content item was created.
FAILED The resource could not be processed. The message field contains the error details.

Content Types Created

Ingestion creates content items of the following types, depending on the package format and resource type:

Content Type Created From Description
QUESTION QTI assessment items (imsqti_item_xmlv3p0) Interactive assessment questions with various interaction types (multiple choice, text entry, matching, etc.)
STIMULUS QTI stimulus resources (imsqti_stimulus_xmlv3p0) Shared reading passages or media that multiple questions reference
ARTICLE QTI article resources (imsqti_article_xmlv3p0) or CC HTML webcontent Standalone reading material or reference content
CONTAINER CC organization structure Hierarchical groupings (chapters, units, modules) that organize other content items
VIDEO CC web links (imswl_xmlv1p1) with video metadata External video resources (YouTube, Vimeo, etc.) linked via IMS Web Link descriptors

Content Associations

The system automatically creates relationships between content items:

Association Type Meaning Created When
STIMULUS Target is a stimulus/passage for the source question A QTI item has a <dependency> on a stimulus resource
SEE_ALSO Related content items A QTI item has a <dependency> on a non-stimulus resource
IS_CHILD_OF Source belongs to the target container CC organization items reference resources under a container

After Ingestion: Retrieving Content

Once ingestion completes, use the Content API to search and retrieve your content items.

Search Content Items

Find content items by curriculum alignment, content type, difficulty, and more:

curl -X POST https://platform.timeback.com/content/1.0/items/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -d '{
    "contentType": "QUESTION",
    "curriculumItemIds": ["<CASE_STANDARD_UUID>"],
    "limit": 10
  }'

Get a Single Content Item

Retrieve a specific content item by ID:

curl https://platform.timeback.com/content/1.0/items/<ITEM_ID> \
  -H "Authorization: Bearer <ACCESS_TOKEN>"

The response includes a contentUrl pointing to the content on CloudFront. To access this URL, your client needs a content access session.

Content Access Sessions

Content files are served through CloudFront with signed cookies. To access content URLs:

  1. Get a session URL — call GET /content/1.0/session-url to obtain a signed URL (requires content.read scope)
  2. Exchange for cookies — redirect the client (browser) to the signed URL, which issues CloudFront signed cookies via Set-Cookie headers
  3. Access content — subsequent requests to contentUrl values will be authenticated by the cookies
async function initializeContentSession(accessToken: string): Promise<string> {
  const response = await fetch('https://platform.timeback.com/content/1.0/session-url', {
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  const { url } = await response.json();
  return url;
}

The session cookies are domain-scoped and expire after a set period. Re-initialize the session when cookies expire.


Authentication

Obtain OAuth client credentials and access tokens required for content ingestion.

API Reference

Complete endpoint documentation for all Content API endpoints — search, get item, ingest, grading, and sessions.