TrainingOS Integration Guide
TrainingOS Integration Guide
Complete guide for integrating the AllureLMS SCORM API with TrainingOS.
Table of Contents
- Overview
- Prerequisites
- Setup
- Core Integration
- Advanced Features
- Error Handling
- Best Practices
- Production Checklist
Overview
This guide covers integrating the SCORM API with TrainingOS to enable:
- SCORM package upload and management
- Session creation and tracking
- Learner progress monitoring
- Completion and score reporting
Prerequisites
- TrainingOS account with admin access
- SCORM API account and API key
- Node.js/TypeScript development environment
- Understanding of REST APIs
Setup
1. Get API Credentials
- Sign in to Allure Admin Dashboard
- Navigate to Admin → SCORM API
- Create a new tenant for TrainingOS (if not exists)
- Generate an API key with
readandwritescopes - Copy your tenant ID and API key
2. Environment Variables
Add to your TrainingOS .env file:
SCORM_API_URL=https://scorm-api.allurelms.com
SCORM_API_KEY=scorm_your-api-key-here
SCORM_TENANT_ID=your-tenant-uuid
3. Install Dependencies
No special dependencies required - use native fetch or your preferred HTTP client.
Core Integration
TypeScript Client Class
Create a reusable client class:
// lib/scorm-api-client.ts
export class ScormAPIClient {
private apiKey: string;
private baseUrl: string;
private tenantId: string;
constructor() {
this.apiKey = process.env.SCORM_API_KEY!;
this.baseUrl = process.env.SCORM_API_URL!;
this.tenantId = process.env.SCORM_TENANT_ID!;
}
private async request(
path: string,
options: RequestInit = {}
): Promise<Response> {
const url = `${this.baseUrl}${path}`;
const headers = {
'X-API-Key': this.apiKey,
...options.headers,
};
return this.fetchWithRetry(url, { ...options, headers });
}
private async fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (4xx)
if (response.ok || (response.status >= 400 && response.status < 500)) {
return response;
}
// Retry server errors (5xx) with exponential backoff
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
if (attempt === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
throw new Error('Max retries exceeded');
}
async uploadPackage(file: File, uploadedBy: string) {
const formData = new FormData();
formData.append('file', file);
formData.append('tenant_id', this.tenantId);
formData.append('uploaded_by', uploadedBy);
const response = await this.request('/api/v1/packages', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
return response.json();
}
async listPackages(page = 1, limit = 20) {
const url = `/api/v1/packages?tenant_id=${this.tenantId}&page=${page}&limit=${limit}`;
const response = await this.request(url);
return response.json();
}
async getPackage(packageId: string) {
const response = await this.request(`/api/v1/packages/${packageId}`);
return response.json();
}
async launchSession(packageId: string, userId: string, sessionId?: string) {
const response = await this.request(`/api/v1/packages/${packageId}/launch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: userId,
session_id: sessionId || crypto.randomUUID(),
}),
});
return response.json();
}
async getSession(sessionId: string) {
const response = await this.request(`/api/v1/sessions/${sessionId}`);
return response.json();
}
async updateSession(sessionId: string, updates: any, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const session = await this.getSession(sessionId);
const payload = {
version: session.version,
cmi_data: {
...session.cmi_data,
...updates.cmi_data,
},
...updates,
};
const response = await this.request(`/api/v1/sessions/${sessionId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (response.ok) {
return 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}`);
}
throw new Error('Max retries exceeded');
}
}
export const scormAPI = new ScormAPIClient();
Upload Flow
// services/scorm-upload.ts
import { scormAPI } from '@/lib/scorm-api-client';
export async function uploadScormPackage(
file: File,
userId: string
): Promise<{ packageId: string; title: string }> {
try {
const pkg = await scormAPI.uploadPackage(file, userId);
// Store package ID in TrainingOS database
await savePackageToDatabase({
scormPackageId: pkg.id,
title: pkg.title,
version: pkg.version,
uploadedBy: userId,
});
return {
packageId: pkg.id,
title: pkg.title,
};
} catch (error) {
console.error('SCORM upload failed:', error);
throw new Error('Failed to upload SCORM package');
}
}
Launch Flow
// services/scorm-launch.ts
import { scormAPI } from '@/lib/scorm-api-client';
export async function launchScormCourse(
packageId: string,
userId: string
): Promise<{ sessionId: string; playerUrl: string }> {
try {
// Check for existing incomplete session
const existingSession = await findIncompleteSession(packageId, userId);
if (existingSession) {
return {
sessionId: existingSession.id,
playerUrl: `https://scorm-api.allurelms.com/player/${existingSession.id}`,
};
}
// Create new session
const launch = await scormAPI.launchSession(packageId, userId);
return {
sessionId: launch.session_id,
playerUrl: launch.player_url,
};
} catch (error) {
console.error('SCORM launch failed:', error);
throw new Error('Failed to launch SCORM course');
}
}
Progress Tracking
// services/scorm-tracking.ts
import { scormAPI } from '@/lib/scorm-api-client';
export async function trackScormProgress(sessionId: string) {
const session = await scormAPI.getSession(sessionId);
// Update TrainingOS database
await updateCourseProgress({
sessionId,
completionStatus: session.completion_status,
successStatus: session.success_status,
score: session.score?.scaled,
timeSpent: session.time_spent_seconds,
});
return {
completed: session.completion_status === 'completed',
passed: session.success_status === 'passed',
score: session.score?.scaled,
timeSpent: session.time_spent_seconds,
};
}
// Poll for completion
export function pollScormCompletion(
sessionId: string,
onComplete: (result: any) => void
) {
const interval = setInterval(async () => {
try {
const progress = await trackScormProgress(sessionId);
if (progress.completed) {
clearInterval(interval);
onComplete(progress);
}
} catch (error) {
console.error('Progress tracking failed:', error);
clearInterval(interval);
}
}, 5000); // Poll every 5 seconds
return () => clearInterval(interval);
}
Advanced Features
Webhook Integration
Subscribe to package processing events:
// Setup webhook
await scormAPI.request('/api/v1/webhooks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenant_id: process.env.SCORM_TENANT_ID,
url: 'https://trainingos.com/api/webhooks/scorm',
secret: process.env.WEBHOOK_SECRET,
event_type: 'package.processing.completed',
}),
});
// Webhook handler in TrainingOS
export async function POST(request: Request) {
const signature = request.headers.get('X-Allure-Signature');
const body = await request.text();
// Verify signature
const expectedSignature = createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(body)
.digest('hex');
if (signature !== expectedSignature) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
if (event.event === 'package.processing.completed') {
await handlePackageProcessed(event.data);
}
return new Response('OK');
}
Version Management
Upload new versions while keeping stable package IDs:
async function updatePackageVersion(
existingPackageId: string,
newFile: File,
userId: string
) {
const formData = new FormData();
formData.append('file', newFile);
formData.append('tenant_id', process.env.SCORM_TENANT_ID!);
formData.append('uploaded_by', userId);
formData.append('package_id', existingPackageId); // Keep same ID
const response = await scormAPI.request('/api/v1/packages', {
method: 'POST',
body: formData,
});
return response.json();
}
Error Handling
Version Conflicts
Always handle 409 conflicts when updating sessions:
async function updateSessionWithRetry(
sessionId: string,
updates: any
): Promise<any> {
try {
return await scormAPI.updateSession(sessionId, updates);
} catch (error) {
if (error.message.includes('409')) {
// Retry with fresh data
return updateSessionWithRetry(sessionId, updates);
}
throw error;
}
}
Rate Limiting
Handle 429 rate limit errors:
private async handleRateLimit(response: Response): Promise<Response> {
if (response.status === 429) {
const retryAfter = parseInt(
response.headers.get('Retry-After') || '60'
);
console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
// Retry the request
return this.request(response.url);
}
return response;
}
Best Practices
- Cache Package Data: Store package metadata in TrainingOS to reduce API calls
- Use Webhooks: Subscribe to events instead of polling when possible
- Handle Errors Gracefully: Implement retry logic for transient failures
- Monitor Quotas: Track package and storage usage
- Version Management: Use package versioning for updates
- Session Resumption: Check for existing incomplete sessions before creating new ones
Production Checklist
- Environment variables configured
- API client implemented with error handling
- Version conflict handling (409 errors)
- Retry logic for server errors (500/503)
- Rate limit handling (429 errors)
- Upload flow tested with real SCORM packages
- Session creation and tracking tested
- Concurrent user scenarios tested
- Completion tracking implemented
- Score reporting working
- Webhook integration (optional but recommended)
- Monitoring and alerting configured
Related Documentation
Last Updated: 2025-01-15
See Also: TrainingOS Integration (Original)