Launch Your First SCORM Session
Learn how to create learning sessions, launch SCORM players, and track learner progress.
⚠️ Every session launch URL must carry a signed token. The player route at
/player/:sessionIdwill return HTTP 401 without it. The launch endpoint returns a completelaunch_urlwith the token already embedded — use it as-is. Never build.../player/<id>yourself. If you are handing the package to an external LMS, use the dispatch flow instead, which produces a durable record-backed handoff URL.
Table of Contents
- Overview
- Prerequisites
- Creating a Session
- Launching the Player
- Tracking Progress
- Updating Session Data
- Common Scenarios
- Troubleshooting
Overview
A SCORM session represents a learner's interaction with a SCORM package. Sessions track:
- CMI Data: Progress, scores, completion status
- Time Spent: Total learning time
- Attempts: Number of times the course was accessed
- State: Current learning state (incomplete, completed, passed, failed)
Prerequisites
- API key with
writescope - A successfully uploaded SCORM package
- A user ID (your system's learner identifier)
Creating a Session
Method 1: Launch Endpoint (Recommended for direct-to-learner launches)
The launch endpoint creates a session and returns a fully signed launch URL in one call. Mint the URL when the learner clicks Play, not earlier. The default token TTL is 14,400 seconds (4 hours), and the response reports the effective TTL after platform min/max clamping.
curl -X POST https://app.allureconnect.com/api/v1/packages/pkg_abc123/launch \
-H "Authorization: Bearer your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"user_id": "user-123",
"session_id": "session-456"
}'
Response:
{
"package_id": "pkg_abc123",
"session_id": "session-456",
"learner_id": "user-123",
"content_type": "scorm",
"storage_backend": "r2",
"launch_url": "https://app.allureconnect.com/player/session-456?token=eyJhbGciOi...",
"expires_in_seconds": 14400
}
Use
launch_urlas-is. The?token=query param is the learner's signed session token. Treat the whole string as one opaque URL — don't strip, shorten, or rebuild it.
Method 2: Dispatch (Recommended for hand-off to an external LMS)
If you are embedding a SCORM package into a third-party LMS (Moodle, Docebo, etc.), don't use Method 1. Instead, create a dispatch — one per LMS destination — and hand the returned dispatch_url / dispatch.launchUrl to the LMS. Connect mints short-lived learner sessions when the dispatch URL is launched, and the dispatch record can be rotated or revoked centrally.
See the full guide in Custom LMS Integration. Short version:
curl -X POST https://app.allureconnect.com/api/v1/packages/pkg_abc123/dispatches \
-H "Authorization: Bearer your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"label": "Acme LMS – Onboarding",
"destination": "acme-moodle"
}'
Response:
{
"dispatch": {
"id": "dsp_…",
"launchUrl": "https://app.allureconnect.com/player/dispatch/dsp_…"
},
"dispatch_url": "https://app.allureconnect.com/player/dispatch/dsp_…"
}
Launching the Player
Use the
launch_urlreturned byPOST /api/v1/packages/:id/launch(Method 1) or thelaunchUrlreturned by the dispatch endpoint (Method 2). Do not hand-construct…/player/<sessionId>— that will 401.
Option 1: Embed in iframe (Recommended)
<!-- `launchUrl` is the exact string returned by the launch or dispatch endpoint -->
<iframe
src="<launchUrl>"
width="100%"
height="800px"
frameborder="0"
allow="fullscreen"
title="SCORM Player"
></iframe>
Option 2: Redirect to Player
// launchUrl comes from the launch endpoint response — use as-is.
window.location.href = launchUrl;
Option 3: Open in New Window
window.open(launchUrl, 'SCORM Player', 'width=1200,height=800');
Tracking Progress
Get Session Data
Retrieve current session state and CMI data:
curl -X GET https://app.allureconnect.com/api/v1/sessions/session-456 \
-H "X-API-Key: your-api-key-here"
Response:
{
"id": "session-456",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "user-123",
"package_id": "pkg_abc123",
"cmi_data": {
"cmi.core.lesson_status": "incomplete",
"cmi.core.score.raw": "75",
"cmi.core.score.max": "100",
"cmi.core.session_time": "PT15M30S"
},
"completion_status": "incomplete",
"success_status": "unknown",
"score": {
"scaled": 0.75,
"raw": 75,
"max": 100,
"min": 0
},
"time_spent_seconds": 930,
"version": 3,
"created_at": "2025-01-15T10:00:00.000Z",
"updated_at": "2025-01-15T10:15:30.000Z"
}
Polling for Updates
Check session progress periodically:
async function pollSessionProgress(sessionId: string) {
const interval = setInterval(async () => {
const response = await fetch(`/api/v1/sessions/${sessionId}`, {
headers: { 'X-API-Key': apiKey }
});
const session = await response.json();
console.log(`Progress: ${session.completion_status}`);
console.log(`Score: ${session.score?.scaled || 0}`);
if (session.completion_status === 'completed') {
clearInterval(interval);
console.log('Course completed!');
}
}, 5000); // Poll every 5 seconds
}
Updating Session Data
Update CMI Data
The SCORM player automatically updates session data, but you can also update it programmatically:
curl -X PUT https://app.allureconnect.com/api/v1/sessions/session-456 \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"version": 3,
"cmi_data": {
"cmi.core.lesson_status": "completed",
"cmi.core.score.raw": "85",
"cmi.core.score.max": "100",
"cmi.core.session_time": "PT20M45S"
},
"completion_status": "completed",
"success_status": "passed",
"score": {
"scaled": 0.85,
"raw": 85,
"max": 100,
"min": 0
},
"session_time": "PT20M45S"
}'
Important: Always include the version field for optimistic locking. If you get a 409 Conflict error, fetch the latest session data and retry.
Handling Version Conflicts
async function updateSessionWithRetry(
sessionId: string,
updates: any,
maxRetries = 3
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
// 1. Get current session
const session = await fetch(`/api/v1/sessions/${sessionId}`, {
headers: { 'X-API-Key': apiKey }
}).then(r => r.json());
// 2. Merge updates
const payload = {
version: session.version,
cmi_data: {
...session.cmi_data,
...updates.cmi_data
},
...updates
};
// 3. Attempt update
const response = await fetch(`/api/v1/sessions/${sessionId}`, {
method: 'PUT',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
return await response.json();
}
if (response.status === 409 && attempt < maxRetries - 1) {
console.log(`Version conflict, retrying... (${attempt + 1}/${maxRetries})`);
continue;
}
throw new Error(`Update failed: ${response.status}`);
}
}
Common Scenarios
Scenario 1: Launch and Track Completion
// 1. Launch session
const launchResponse = await fetch(`/api/v1/packages/${packageId}/launch`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
session_id: sessionId
})
});
const { launch_url, session_id } = await launchResponse.json();
// 2. Embed player — launch_url already includes the signed ?token=
const iframe = document.createElement('iframe');
iframe.src = launch_url;
iframe.width = '100%';
iframe.height = '800px';
document.body.appendChild(iframe);
// 3. Poll for completion
const checkCompletion = setInterval(async () => {
const session = await fetch(`/api/v1/sessions/${session_id}`, {
headers: { 'X-API-Key': apiKey }
}).then(r => r.json());
if (session.completion_status === 'completed') {
clearInterval(checkCompletion);
showCompletionMessage(session);
}
}, 5000);
Scenario 2: Resume Previous Session
// 1. Find existing session
const sessionsResponse = await fetch(
`/api/v1/sessions?package_id=${packageId}&user_id=${userId}`,
{ headers: { 'X-API-Key': apiKey } }
);
const { sessions } = await sessionsResponse.json();
// 2. Find incomplete session
const incompleteSession = sessions.find(
s => s.completion_status === 'incomplete'
);
// Resuming or creating, always hit /launch — it re-mints a fresh signed
// launch_url whether the session already existed or was just created.
const launchResponse = await fetch(`/api/v1/packages/${packageId}/launch`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
session_id: incompleteSession?.id ?? crypto.randomUUID()
})
});
const { launch_url } = await launchResponse.json();
window.location.href = launch_url;
Scenario 3: Track Multiple Users
async function launchForMultipleUsers(packageId: string, userIds: string[]) {
const sessions = [];
for (const userId of userIds) {
const response = await fetch(`/api/v1/packages/${packageId}/launch`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
session_id: crypto.randomUUID()
})
});
if (response.ok) {
const session = await response.json();
sessions.push(session);
}
}
return sessions;
}
Troubleshooting
Error: "Session Not Found"
Causes:
- Invalid session ID
- Session expired
- Session belongs to different tenant
Solutions:
- Verify session ID is correct
- Check session expiration time
- Ensure API key matches tenant
Error: "Version Conflict" (409)
Causes:
- Concurrent updates to same session
- Using outdated version number
Solutions:
- Fetch latest session data
- Merge your changes with latest data
- Retry with updated version number
- See Handling Version Conflicts
Error: "Package Not Found"
Causes:
- Invalid package ID
- Package deleted
- Package belongs to different tenant
Solutions:
- Verify package ID
- Check package exists
- Ensure API key matches tenant
Player Returns 401 / "Launch link unavailable"
Most common cause: the URL was constructed as .../player/<sessionId> without the ?token= query parameter. Learners hitting the player without a signed session token are unauthenticated and always 401.
Fix:
- Always use the
launch_urlfield fromPOST /api/v1/packages/:id/launchas-is — it already contains the signed token. - For LMS hand-off, use dispatch
launchUrlfromPOST /api/v1/packages/:id/dispatches. - Do not cache or store tokenless URLs. Re-mint when the learner clicks Play.
- Token TTL defaults to 14,400s. Player integrations should refresh via
POST /api/v1/sessions/{sessionId}/refresh-tokenbefore expiry or re-mint a new launch on retry.
Player Not Loading (other causes)
Causes:
- CORS issues
- Network connectivity
- Package
launch_urlmissing from manifest
Solutions:
- Check browser console for errors
- Verify CORS configuration
- Ensure the package upload completed successfully
Best Practices
- Use Unique Session IDs: Generate UUIDs for session IDs to avoid conflicts
- Handle Version Conflicts: Always implement retry logic for session updates
- Poll for Progress: Check session status periodically for real-time updates
- Resume Sessions: Check for existing incomplete sessions before creating new ones
- Monitor Expiration: Track
expires_in_secondsand refresh the session token before expiry - Error Handling: Implement comprehensive error handling for all API calls
Next Steps
- Generate Your First Report - View learner progress and analytics
- CMI Data Guide - Understand SCORM CMI data structure
- Session Management - Complete session API reference (API Reference coming soon)
- Webhook Setup - Get notified of session events
Last Updated: 2025-01-15
Related Documentation: