Upload Your First SCORM Package

Learn how to upload, validate, and manage SCORM packages with the AllureLMS SCORM API.

Table of Contents

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 write scope
  • 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:

  1. imsmanifest.xml - SCORM manifest file (required)
  2. Content Files - HTML, JavaScript, media files, etc.
  3. 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 surfaceGET /api/v1/packages lists packages; ingestion is upload-urlPUTprocess.

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:

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

  1. Validate Before Processing: Use validate_only: true to check packages before committing
  2. Use Versioning: Upload new versions to existing packages to maintain stable IDs
  3. Monitor Quotas: Track your package and storage usage
  4. Handle Errors: Implement retry logic for transient failures
  5. Use Webhooks: Subscribe to package.processing.completed events for async workflows

Next Steps


Last Updated: 2025-01-15
Related Documentation: