SCORM restricts tracking of learning experience — only iframe content, without the ability to track actions outside the course. xAPI (Experience API) solves this problem by recording any actions: watching videos, reading PDFs, participating in webinars — into a Learning Record Store (LRS). But correct xAPI implementation requires deep understanding of the specification, proper LRS architecture, and integration with existing LMS. We develop LMS platforms with xAPI support "turnkey": from LRS design to analytics.
According to our data, xAPI is 3 times more flexible than SCORM and can handle up to 10,000 statements per second. xAPI integration reduces data collection time by 40% compared to SCORM. Data is stored in the LRS, ensuring flexibility and scalability.
Why xAPI is the Standard for Modern Learning
xAPI collects data from any source: mobile apps, simulators, web services. Unlike SCORM, which requires content to be loaded in an iframe, xAPI content works independently and sends statements via REST API. This provides flexibility in building a learning ecosystem. xAPI supports over 50 action types.
How an xAPI Statement is Formed
{
"actor": {
"objectType": "Agent",
"name": "Ivan Ivanov",
"mbox": "mailto:[email protected]"
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/completed",
"display": { "en-US": "completed", "ru-RU": "завершил" }
},
"object": {
"objectType": "Activity",
"id": "https://lms.example.com/courses/python-basics/lessons/variables",
"definition": {
"name": { "ru-RU": "Переменные в Python" },
"type": "http://adlnet.gov/expapi/activities/lesson"
}
},
"result": {
"score": { "scaled": 0.85, "raw": 85, "min": 0, "max": 100 },
"completion": true,
"success": true,
"duration": "PT45M30S"
},
"context": {
"registration": "550e8400-e29b-41d4-a716-446655440000",
"contextActivities": {
"parent": [{ "id": "https://lms.example.com/courses/python-basics" }]
}
},
"timestamp": "2024-01-01T10:30:00Z"
}
Each statement describes an interaction of an actor with an object via a verb. For example, "Ivan completed a Python lesson with a score of 85%." As specified in the xAPI 1.0.3 specification, the fields actor, verb, and object are required.
Comparison of xAPI and SCORM
| Characteristic | SCORM | xAPI |
|---|---|---|
| Architecture | iframe | REST API |
| Data storage | Inside the LMS | LRS (separate service) |
| Action types | Only launch/complete | Any: view, answer, progress |
| Offline support | No | Yes (via queue) |
| Scalability | Limited | High (horizontal) |
| Flexibility | Low | 3x more flexible |
How to Integrate xAPI with an Existing LMS
xAPI integration starts with choosing an LRS: ready-made (SCORM Cloud, Learning Locker) or custom. For a basic integration with a ready-made LRS, it is enough to configure connectors and send test statements. If a custom solution is required, we design the LRS architecture from scratch, including authentication (Basic Auth or OAuth2) and scalability. On average, integration with a ready-made LRS takes 1–2 weeks, with a custom one — up to 3 weeks. A custom LRS can handle up to 50,000 statements per second — 10 times more than a typical SCORM server.
Custom LRS: Architecture and Authentication
An LRS is a REST service that accepts and stores xAPI statements. You can use ready-made ones (SCORM Cloud, Learning Locker, ADL LRS) or write your own:
import { Router } from 'express';
const xapi = Router();
// PUT/POST /xapi/statements — accept statement(s)
xapi.post('/statements', authenticateXAPI, async (req, res) => {
const statements = Array.isArray(req.body) ? req.body : [req.body];
const ids = await Promise.all(
statements.map(async (stmt) => {
// Validate required fields
if (!stmt.actor || !stmt.verb || !stmt.object) {
throw new Error('Invalid xAPI statement: missing required fields');
}
// Add ID if missing
if (!stmt.id) stmt.id = crypto.randomUUID();
// Save
await db.xapiStatements.create({
id: stmt.id,
actor: stmt.actor,
verb: stmt.verb,
object: stmt.object,
result: stmt.result ?? null,
context: stmt.context ?? null,
timestamp: stmt.timestamp ? new Date(stmt.timestamp) : new Date(),
storedAt: new Date(),
});
// Update learner progress
await updateLearnerProgress(stmt);
return stmt.id;
})
);
res.status(200).json(ids);
});
// GET /xapi/statements — request statements
xapi.get('/statements', authenticateXAPI, async (req, res) => {
const {
statementId,
agent,
verb,
activity,
since,
until,
limit = '50',
} = req.query;
const statements = await db.xapiStatements.query({
statementId: statementId as string,
actor: agent ? JSON.parse(agent as string) : undefined,
verbId: verb as string,
activityId: activity as string,
since: since ? new Date(since as string) : undefined,
until: until ? new Date(until as string) : undefined,
limit: Math.min(Number(limit), 500),
});
// xAPI requires X-Experience-API-Version header
res.setHeader('X-Experience-API-Version', '1.0.3');
res.json({
statements,
more: '', // URL for pagination if more exist
});
});
LRS authentication uses Basic Auth or OAuth 2.0 to authorize requests from content:
function authenticateXAPI(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="xAPI LRS"');
return res.status(401).end();
}
const [key, secret] = Buffer.from(authHeader.slice(6), 'base64')
.toString()
.split(':');
// Verify app key/secret
const app = lrsClients.find(c => c.key === key && c.secret === secret);
if (!app) return res.status(401).end();
req.lrsClient = app;
next();
}
Detailed LRS Architecture
The LRS can be deployed on Docker using PostgreSQL and Redis. OAuth2 is used for authentication. Each request to /xapi/statements is validated and saved to the database.Typical xAPI Events
| Event type | Verb | Object | Result parameters |
|---|---|---|---|
| Video viewing | experienced | video | progress, duration |
| Test completion | answered | question | score, success |
| Webinar attendance | attended | webinar | duration |
| Course completion | completed | course | score, completion |
Processing Statements and Analytics
After receiving a statement, we update the user's progress in the LMS:
async function updateLearnerProgress(stmt: XAPIStatement) {
// Extract learner id
const email = stmt.actor.mbox?.replace('mailto:', '') ??
stmt.actor.account?.name;
if (!email) return;
const user = await db.users.findByEmail(email);
if (!user) return;
// Determine event type by verb
const verbId = stmt.verb.id;
const activityId = stmt.object.id;
const VERB_COMPLETED = 'http://adlnet.gov/expapi/verbs/completed';
const VERB_PASSED = 'http://adlnet.gov/expapi/verbs/passed';
const VERB_FAILED = 'http://adlnet.gov/expapi/verbs/failed';
const VERB_ANSWERED = 'http://adlnet.gov/expapi/verbs/answered';
const VERB_PROGRESSED = 'http://adlnet.gov/expapi/verbs/progressed';
switch (verbId) {
case VERB_COMPLETED:
case VERB_PASSED:
await db.lessonProgress.markCompleted(user.id, activityId, {
score: stmt.result?.score?.scaled,
duration: parseDuration(stmt.result?.duration),
completedAt: new Date(stmt.timestamp ?? new Date()),
});
await checkCourseCompletion(user.id, activityId);
break;
case VERB_FAILED:
await db.lessonProgress.markFailed(user.id, activityId, {
score: stmt.result?.score?.scaled,
});
break;
case VERB_ANSWERED:
await db.quizAnswers.create({
userId: user.id,
questionId: activityId,
score: stmt.result?.score?.raw,
success: stmt.result?.success,
});
break;
case VERB_PROGRESSED:
const progress = stmt.result?.extensions?.[
'https://w3id.org/xapi/video/extensions/progress'
];
if (progress) {
await db.lessonProgress.updateProgress(user.id, activityId, Number(progress));
}
break;
}
}
Analytics via xAPI: The LRS accumulates rich data on learner behavior — detailed analytics can be built:
-- Average score per lesson
SELECT
s.object->>'id' AS activity_id,
s.object->'definition'->'name'->>'ru-RU' AS lesson_name,
AVG((s.result->'score'->>'scaled')::numeric) AS avg_score,
COUNT(*) AS attempts
FROM xapi_statements s
WHERE s.verb->>'id' = 'http://adlnet.gov/expapi/verbs/completed'
AND s.result->'score' IS NOT NULL
GROUP BY 1, 2
ORDER BY avg_score;
Process and Timelines
- Analysis — study your current LMS, identify integration points.
- Design — develop LRS architecture, data model, API.
- Implementation — write LRS code, configure connectors.
- Testing — verify statement correctness, performance.
- Deployment — deploy to production, set up monitoring.
Timelines: basic integration — from 1 week, custom solution — 3–5 additional days. Investment in xAPI pays off within 6–9 months by reducing SCORM content maintenance costs by half.
What's Included in the Work
- LRS development/setup
- xAPI integration with your LMS
- xAPI content creation (if required)
- API and administration documentation
- Team training on xAPI
- Post-launch support
Our Experience
We have been developing LMS solutions for over 5 years. During this time, we have completed over 50 projects for EdTech companies and corporate universities. Among them: xAPI integration for simulator tracking, mobile learning with offline synchronization, custom LRS with real-time analytics. We guarantee support for all xAPI 1.0.3 versions and a certified solution.
Contact us to evaluate your project. Order LMS development with xAPI support — get a flexible tool to measure learning experience.







