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:
- Obtain OAuth credentials with the
content.writeandcontent.readscopes (see Authentication Guide) - 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.xmlat 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
xmlnsattribute referencinghttp://www.imsglobal.org/xsd/imscc - A
typeattribute starting withimscc_ - A
<schema>element containingIMS 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:
- Get a session URL — call
GET /content/1.0/session-urlto obtain a signed URL (requirescontent.readscope) - Exchange for cookies — redirect the client (browser) to the signed URL, which issues CloudFront signed cookies via
Set-Cookieheaders - Access content — subsequent requests to
contentUrlvalues 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.
Related Docs
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.
