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/: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 package to an external LMS, use the dispatch flow instead, which produces a durable record-backed handoff 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 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_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 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_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 14,400s. Player integrations should refresh via POST /api/v1/sessions/{sessionId}/refresh-token before expiry or re-mint a new launch on retry.

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 expires_in_seconds and refresh the session token before expiry
  6. Error Handling: Implement comprehensive error handling for all API calls

Next Steps


Last Updated: 2025-01-15
Related Documentation: