LMS Platform Development with xAPI (Experience API) Support

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
LMS Platform Development with xAPI (Experience API) Support
Complex
from 2 weeks to 3 months
FAQ
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

xAPI (Experience API / Tin Can) Support in LMS

xAPI is a modern replacement for SCORM. Unlike SCORM, xAPI doesn't require iframes and works via REST API to a Learning Record Store (LRS). It records "statements" — assertions like "Ivan completed Python lesson", "Maria scored 85 on test".

xAPI Statement Concept

{
  "actor": {
    "objectType": "Agent",
    "name": "Ivan Ivanov",
    "mbox": "mailto:[email protected]"
  },
  "verb": {
    "id": "http://adlnet.gov/expapi/verbs/completed",
    "display": { "en-US": "completed", "ru-RU": "completed" }
  },
  "object": {
    "objectType": "Activity",
    "id": "https://lms.example.com/courses/python-basics/lessons/variables",
    "definition": {
      "name": { "en-US": "Variables in 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": "2026-03-28T10:30:00Z"
}

Custom LRS (Learning Record Store)

LRS is a REST service that accepts and stores xAPI statements. You can use ready-made solutions (SCORM Cloud, Learning Locker, ADL LRS) or build 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 not present
      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 — query 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: '',  // Pagination URL if more results
  });
});

Updating Progress from Statements

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;
  }
}

LRS Authentication

xAPI 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();
}

xAPI Launch — Running Content Without iframe

Unlike SCORM, xAPI content launches directly and sends statements via fetch:

// Generate Launch URL with parameters
function generateXAPILaunchUrl(
  courseUrl: string,
  userId: string,
  userEmail: string
): string {
  const params = new URLSearchParams({
    endpoint: `${process.env.APP_URL}/xapi/`,
    auth: `Basic ${Buffer.from(`${lrsKey}:${lrsSecret}`).toString('base64')}`,
    actor: JSON.stringify({
      objectType: 'Agent',
      name: userId,
      mbox: `mailto:${userEmail}`,
    }),
    registration: crypto.randomUUID(),
  });

  return `${courseUrl}?${params.toString()}`;
}

Analytics via xAPI

LRS accumulates rich data about learner behavior — enables detailed analytics:

-- Average score by lessons
SELECT
  s.object->>'id' AS activity_id,
  s.object->'definition'->'name'->>'en-US' 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;

Timeframe

Basic LRS with statement acceptance and progress update — 1 week. With OAuth2, analytics, and xAPI Launch support — additional 3–5 days.