Upload Your First SCORM Package
Learn how to upload, validate, and manage SCORM packages with the AllureLMS SCORM API.
Table of Contents
- Overview
- Prerequisites
- Package Requirements
- Upload Methods
- Understanding the Response
- Package Management
- Common Scenarios
- Troubleshooting
Overview
The SCORM API supports uploading SCORM 1.2 and SCORM 2004 packages. Once uploaded, packages are:
- Validated for SCORM compliance
- Extracted and stored securely
- Made available for launching sessions
- Tracked with version history
Prerequisites
- API key with
writescope - A valid SCORM 1.2 or SCORM 2004 package (ZIP file)
- Your tenant ID
Package Requirements
Supported Formats
- SCORM 1.2 - Full support
- SCORM 2004 (1st-4th Edition) - Full support
- ZIP Archive - Must be a valid ZIP file
Package Structure
Your SCORM package must contain:
- imsmanifest.xml - SCORM manifest file (required)
- Content Files - HTML, JavaScript, media files, etc.
- Valid Structure - Files organized according to SCORM specification
File Size Limits
- Default: 100MB per package
- Large Packages: Use multipart upload for packages >100MB
- Maximum: 10GB (with multipart upload)
Validation
The API automatically validates:
- Manifest structure and syntax
- SCORM version compatibility
- Required files presence
- Package integrity
Upload Methods
Method 1: Presigned upload (recommended for most ZIPs)
Allure Connect mints a short-lived URL; you PUT the ZIP bytes, then call process. See Package upload API — partner integration.
Use the same shell flow as Quick Start — Step 4, or implement the three HTTP calls from your backend.
Direct multipart POST /api/v1/packages is not the supported listing/upload surface — GET /api/v1/packages lists packages; ingestion is upload-url → PUT → process.
Method 2: Multipart Upload (For Large Packages >100MB)
For large packages, use the multipart upload flow:
Step 1: Initialize Upload
curl -X POST https://app.allureconnect.com/api/v1/packages/multipart/init \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"uploaded_by": "user-123",
"filename": "large-course.zip"
}'
Response:
{
"upload_id": "upload_abc123",
"storage_path": "tenant/uploads/tmp_123.zip",
"part_size": 5242880,
"total_parts": 20
}
Step 2: Get Presigned URLs for Each Part
curl -X POST https://app.allureconnect.com/api/v1/packages/multipart/part-url \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"upload_id": "upload_abc123",
"part_number": 1
}'
Response:
{
"part_number": 1,
"url": "https://storage.example.com/upload?presigned=...",
"expires_in": 3600
}
Step 3: Upload Each Part
curl -X PUT "https://storage.example.com/upload?presigned=..." \
-H "Content-Type: application/zip" \
--data-binary @part1.zip
Save the ETag from the response header.
Step 4: Complete Upload
curl -X POST https://app.allureconnect.com/api/v1/packages/multipart/complete \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"upload_id": "upload_abc123",
"parts": [
{ "part_number": 1, "etag": "\"etag1\"" },
{ "part_number": 2, "etag": "\"etag2\"" }
]
}'
Step 5: Process Package
curl -X POST https://app.allureconnect.com/api/v1/packages/process \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"uploaded_by": "user-123",
"storage_path": "tenant/uploads/tmp_123.zip",
"original_filename": "large-course.zip"
}'
Method 3: Validation Only
Test package validity without processing:
curl -X POST https://app.allureconnect.com/api/v1/packages/process \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"uploaded_by": "user-123",
"storage_path": "tenant/uploads/tmp_123.zip",
"original_filename": "course.zip",
"validate_only": true
}'
Response:
{
"validation_only": true,
"manifest": {
"title": "Intro Course",
"version": "1.2",
"launch_url": "index.html",
"sco_count": 5
},
"file_size_bytes": 1421132,
"storage_path": "tenant/uploads/tmp_123.zip"
}
Understanding the Response
After POST /api/v1/packages/process completes successfully, the payload matches the ProcessPackageResponse shape in OpenAPI (GET /api/docs/openapi): manifest summary, upload_id, storage_path, and (when not validate_only) a nested package object with package_id, title, launch_url, etc.
Successful process response (typical)
{
"upload_id": "770e8400-e29b-41d4-a716-446655440001",
"manifest": {
"title": "Introduction to Safety Training",
"version": "1.2",
"launch_url": "index_lms.html",
"sco_count": 12,
"description": "…",
"duration": "PT45M"
},
"file_size_bytes": 5242880,
"storage_path": "tenant-550e8400/uploads/course.zip",
"package": {
"package_id": "pkg_abc123",
"title": "Introduction to Safety Training",
"launch_url": "/api/v1/content/…/index_lms.html",
"version": "1.2",
"current_revision": 1
}
}
Legacy package listing shape (for comparison only)
The following resembles an older list/detail package record — do not expect this exact top-level shape from process alone:
{
"id": "pkg_abc123",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Introduction to Safety Training",
"version": "1.2",
"scorm_version": "1.2",
"launch_url": "index.html",
"manifest_url": "imsmanifest.xml",
"storage_path": "tenant-550e8400/packages/pkg_abc123",
"file_size_bytes": 5242880,
"metadata": {
"identifier": "com.example.course.001",
"schema": "ADL SCORM",
"schemaversion": "1.2",
"description": "Course description",
"sco_count": 5
},
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
Key Fields Explained
- id: Unique package identifier (use for launching sessions)
- title: Extracted from manifest
- version: SCORM version (1.2 or 2004)
- launch_url: Entry point for the course
- storage_path: Internal storage location
- metadata: Additional package information
Package Management
List All Packages
curl -X GET "https://app.allureconnect.com/api/v1/packages?tenant_id=550e8400-e29b-41d4-a716-446655440000" \
-H "X-API-Key: your-api-key-here"
Get Package Details
curl -X GET https://app.allureconnect.com/api/v1/packages/pkg_abc123 \
-H "X-API-Key: your-api-key-here"
Update Package Metadata
curl -X PATCH https://app.allureconnect.com/api/v1/packages/pkg_abc123 \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Updated course description",
"duration": "45m",
"tags": ["onboarding", "2025"],
"custom_metadata": { "level": "advanced" }
}'
Upload New Version
To update a package while keeping the same ID, use the presigned flow and pass package_id on POST /api/v1/packages/process (after the storage PUT). See docs/API/package-upload-integration.md for the full mint → PUT → process sequence.
The launch URL stays stable, but current_revision increments when processing completes.
Get Version History
curl -X GET "https://app.allureconnect.com/api/v1/packages/pkg_abc123/versions?tenant_id=550e8400-e29b-41d4-a716-446655440000" \
-H "X-API-Key: your-api-key-here"
Common Scenarios
Scenario 1: Upload and Launch Immediately
const base = 'https://app.allureconnect.com';
const headers = { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
// 1. Mint upload URL → PUT ZIP → process (see package-upload-integration.md for full detail)
async function ingestZip(file: File) {
const mint = await fetch(`${base}/api/v1/packages/upload-url`, {
method: 'POST',
headers,
body: JSON.stringify({
tenant_id: tenantId,
uploaded_by: 'user-123',
filename: file.name,
file_size: file.size
})
});
if (!mint.ok) {
throw new Error(`Mint failed: ${mint.status} ${await mint.text()}`);
}
const ticket = await mint.json();
const put = await fetch(ticket.presigned_url, {
method: 'PUT',
headers: ticket.required_put_headers,
body: file
});
if (!put.ok) {
throw new Error(`Storage PUT failed: ${put.status} ${await put.text()}`);
}
const proc = await fetch(`${base}/api/v1/packages/process`, {
method: 'POST',
headers,
body: JSON.stringify({
tenant_id: tenantId,
uploaded_by: 'user-123',
storage_path: ticket.storage_path,
original_filename: file.name
})
});
if (!proc.ok) {
throw new Error(`Process failed: ${proc.status} ${await proc.text()}`);
}
return proc.json();
}
const processed = await ingestZip(zipFile);
const packageId = processed.package?.package_id;
if (!packageId) throw new Error('Package id missing from process response');
// 2. Launch session
const launchResponse = await fetch(`${base}/api/v1/packages/${packageId}/launch`, {
method: 'POST',
headers,
body: JSON.stringify({
user_id: 'user-123',
session_id: crypto.randomUUID()
})
});
const launchPayload = await launchResponse.json();
if (!launchResponse.ok) {
throw new Error(`Launch failed: ${launchResponse.status} ${JSON.stringify(launchPayload)}`);
}
// Launch API returns `launch_url` (see OpenAPI PackageLaunchResponse)
const { launch_url } = launchPayload;
// 3. Embed in iframe
(document.getElementById('player') as HTMLIFrameElement).src = launch_url;
Scenario 2: Batch Upload Multiple Packages
// Reuse ingestZip() from Scenario 1
for (const file of fileList) {
const processed = await ingestZip(file);
const pid = processed.package?.package_id;
console.log(`Uploaded: ${processed.manifest?.title} (${pid})`);
}
Scenario 3: Validate Before Uploading
// 1. Upload to temporary storage first
const tempUpload = await uploadToTempStorage(file);
// 2. Validate without processing
const validateResponse = await fetch('/api/v1/packages/process', {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tenant_id: tenantId,
uploaded_by: userId,
storage_path: tempUpload.storage_path,
original_filename: file.name,
validate_only: true
})
});
const validation = await validateResponse.json();
// 3. If valid, process for real
if (validation.manifest) {
await fetch('/api/v1/packages/process', {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tenant_id: tenantId,
uploaded_by: userId,
storage_path: tempUpload.storage_path,
original_filename: file.name,
validate_only: false
})
});
}
Troubleshooting
Error: "Invalid SCORM Package"
Causes:
- Missing
imsmanifest.xml - Invalid manifest structure
- Unsupported SCORM version
Solutions:
- Verify package contains valid SCORM manifest
- Check SCORM version compatibility
- Review Package Validation Guide
Error: "File Too Large"
Causes:
- Package exceeds size limits
- Quota exceeded
Solutions:
- Use multipart upload for large packages
- Check your storage quota
- Contact support to increase limits
Error: "Package Processing Failed"
Causes:
- Corrupted ZIP file
- Missing required files
- Storage issues
Solutions:
- Verify ZIP file integrity
- Re-export package from authoring tool
- Check storage backend status
- Review error logs for details
Error: "Quota Exceeded"
Causes:
- Package limit reached
- Storage limit reached
Solutions:
- Delete unused packages
- Upgrade subscription plan
- Contact support for quota increase
Best Practices
- Validate Before Processing: Use
validate_only: trueto check packages before committing - Use Versioning: Upload new versions to existing packages to maintain stable IDs
- Monitor Quotas: Track your package and storage usage
- Handle Errors: Implement retry logic for transient failures
- Use Webhooks: Subscribe to
package.processing.completedevents for async workflows
Next Steps
- Launch Your First Session - Create and launch a learning session
- Package Validation Guide - Learn about SCORM requirements
- Webhook Setup - Get notified when packages are processed
- API Guide - Complete endpoint documentation (API Reference coming soon)
Last Updated: 2025-01-15
Related Documentation: