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. Containers that declare CASE standards — either in LOM metadata under the organizational <item>, or as caseStandards / caseItems in the item's timeback-metadata JSON — have those standards linked to the container content item. Links are reconciled on re-import when the declared set changes; an intentionally empty declaration clears prior links.
When a resource or container references a CASE standard identifier that is not present in the platform catalogue, that link is skipped and a warning is logged rather than failing the item. If every declared identifier is missing from the catalogue, existing links are left unchanged (so a bad package cannot wipe prior CASE mappings).
Beyond cumulative-review discovery reads CONTAINER items with extendedMetadata.containerType = "test" that are linked to the course CF item via tb_content_item_case_item (case_item_id = the course identifier).
Extended Metadata
Content packages can include optional metadata fields via the LOM Classification timeback-extended-attributes taxon path. These values are extracted during ingestion and stored as first-class fields on the content item.
| Field | Type | Description |
|---|---|---|
difficulty |
Decimal (0.0–1.0) | Difficulty level of the content item. Stored on the question details for QUESTION type items. |
baseXp |
Decimal | Base experience points awarded on completion. Stored directly on the content item. |
Include these in the manifest's <imsmd:classification> block:
<imsmd:classification>
<imsmd:taxonPath>
<imsmd:source><imsmd:string>timeback-extended-attributes</imsmd:string></imsmd:source>
<imsmd:taxon>
<imsmd:id>difficulty</imsmd:id>
<imsmd:entry><imsmd:string>0.65</imsmd:string></imsmd:entry>
</imsmd:taxon>
<imsmd:taxon>
<imsmd:id>baseXp</imsmd:id>
<imsmd:entry><imsmd:string>15</imsmd:string></imsmd:entry>
</imsmd:taxon>
</imsmd:taxonPath>
</imsmd:classification>
Both fields are optional. If omitted, they remain null on the content item.
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
Most content files are served through the private CloudFront CDN with signed cookies:
- 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 private
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 to the private content host and expire after a set period. Re-initialize the session when cookies expire.
Public content CDN (Beyond-reachable)
A second, unsigned CloudFront distribution serves the same private content lake for consumers that cannot use session cookies (notably Beyond players loading article HTML and QTI <img src> assets). Production hostname: public.content.platform.timeback.com.
- Private host (e.g.
content.platform.timeback.com): signed-cookie access to the content lake - Public host: plain HTTPS GET of the same lake keys (S3 still private; CloudFront OAC only)
- Upload-url ingest (
usePublicContentCdnon the job) bakes the public CDN host + private lakeversionIdquery params into HTML/QTI bodies in a single private-bucket write. AEStudio import omits that flag and keeps the private CloudFront host - The Beyond publisher emits public CDN article URLs using private-lake VersionIds — no separate public bucket
Do not embed expiring signed URLs in published course content — use the public CDN host for durable external links.
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.
