Launch Your First SCORM Session
Learn how to create learning sessions, launch SCORM players, and track learner progress.
⚠️ Every 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 URL to an external LMS, use the dispatch flow instead, which produces a tokenized, multi-learner 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 token is short-lived (10 min default).
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": 600
}
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 launchUrl to the LMS. The URL is self-authenticating for every learner who loads it, and 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_…",
"token": "eyJhbGciOi…",
"launchUrl": "https://app.allureconnect.com/player/dispatch/eyJhbGciOi…"
}
}
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 600s — expired tokens also 401. Re-mint on expiry.
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 session expiration times and refresh if needed
- 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: