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
- Platform ceiling: 500MB per package by default — configurable per deployment via
SCORM_MAX_UPLOAD_MB - Plan tiers: your plan's upload cap applies on top of the platform ceiling (the effective limit is the lower of the two; check
GET /api/v1/packages/upload-limit) - Large packages: use multipart upload for packages >100MB (resumable parts, abortable)
- Browser (
multipart/form-data) upload: small packages only (~4MB serverless body limit) — use presigned or multipart for anything larger
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",
"file_size": 262144000
}'
file_size is required: the server enforces your plan's upload ceiling up
front (413 FILE_TOO_LARGE), plans the part count, and signs each part URL for
its exact expected byte length.
Response:
{
"upload_id": "upload_abc123",
"multipart_upload_id": "2~x9Yf...r2-issued-id",
"storage_path": "tenant/multipart/tmp_123.zip",
"part_size_bytes": 52428800,
"max_upload_mb": 500,
"part_count": 5
}
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:
{
"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\"" }
]
}'
Aborting an interrupted upload
If the upload is interrupted or you decide not to finish it, abort it so the staged parts are freed from storage immediately (staged parts otherwise accumulate until a bucket lifecycle rule removes them):
curl -X POST https://app.allureconnect.com/api/v1/packages/multipart/abort \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{ "upload_id": "upload_abc123" }'
Response:
{
"upload_id": "upload_abc123",
"storage_path": "tenant/multipart/tmp_123.zip",
"status": "aborted",
"aborted": true
}
Abort is idempotent — repeating it (or aborting an already-completed upload)
returns "status": "already_aborted" / "already_completed" with
"aborted": false instead of an error. After aborting, retry by starting a
fresh init.
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/multipart/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/multipart/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/multipart/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: