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/:sessionId will return HTTP 401 without it. The launch endpoint returns a complete launch_url with 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

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 write scope
  • 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_url as-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_url returned by POST /api/v1/packages/:id/launch (Method 1) or the launchUrl returned 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:

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_url field from POST /api/v1/packages/:id/launch as-is — it already contains the signed token.
  • For LMS hand-off, use dispatch launchUrl from POST /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_url missing from manifest

Solutions:

  • Check browser console for errors
  • Verify CORS configuration
  • Ensure the package upload completed successfully

Best Practices

  1. Use Unique Session IDs: Generate UUIDs for session IDs to avoid conflicts
  2. Handle Version Conflicts: Always implement retry logic for session updates
  3. Poll for Progress: Check session status periodically for real-time updates
  4. Resume Sessions: Check for existing incomplete sessions before creating new ones
  5. Monitor Expiration: Track session expiration times and refresh if needed
  6. Error Handling: Implement comprehensive error handling for all API calls

Next Steps


Last Updated: 2025-01-15
Related Documentation: