xAPI (Experience API) is a specification that allows you to track learning experiences. It uses a simple structure of "Actor - Verb - Object" to describe activities, similar to how you might say "John completed the course" in plain English.
What is a DID?
A DID (Decentralized Identifier) is a unique identifier for your user that works across different systems. Think of it like an email address that works everywhere but is more secure and private.
Sending xAPI Statements
Here's how to send an xAPI statement to LearnCloud:
DID Usage: Always use the same DID in both actor.name and actor.account.name. This DID should come from your authentication process.
Verb Selection: Use standard xAPI verbs when possible. Common ones include:
attempted
completed
mastered
demonstrated
failed
progressed
Activity IDs: Use consistent, unique URLs for your activity IDs. They don't need to be real URLs, but they should be unique identifiers following URL format.
Authentication: The JWT token should be sent in the X-VP header. This is specific to LearnCloud's implementation.
Use this JWT in the X-VP header to read statements
Voiding Statements
To remove a statement (mark it as void):
// First get the statement ID from the original POST response
const statementId = (await postResponse.json())[0];
// Create void statement
const voidStatement = {
actor,
verb: XAPI.Verbs.VOIDED,
object: {
objectType: 'StatementRef',
id: statementId
}
};
// Send void request
const voidResponse = await fetch(`${endpoint}/statements`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Experience-API-Version': '1.0.3',
'X-VP': jwt
},
body: JSON.stringify(voidStatement)
});
Important: You can only void statements that you created.
Validation Tips
Check Response Status:
200: Success
401: Authentication/permission error
Other: Server/request error
Common Implementation Issues:
JWT not matching actor DID
Missing or malformed agent parameter
Incorrect content type header
Missing xAPI version header
Testing Checklist:
Can read own statements
Cannot read others' statements
Delegate access works as expected
Can void own statements
Cannot void others' statements
Remember: The xAPI server maintains strict permissions - users can only read and modify their own statements unless explicitly delegated access by the statement owner.
Advanced xAPI Statement Queries
Filtering Large Statement Sets
When dealing with large statement volumes or statements with extensive data in extensions, you can use the following techniques to retrieve more manageable subsets of data.
Basic Query Parameters
The xAPI API supports several query parameters to limit and filter your results:
// Basic query with filtering
const queryParams = new URLSearchParams({
agent: JSON.stringify(actor),
limit: "10", // Limit to 10 results
since: "2024-03-01T00:00:00Z", // Only statements after this date
until: "2024-03-31T23:59:59Z", // Only statements before this date
verb: "http://adlnet.gov/expapi/verbs/completed" // Only specific verb
});
const response = await fetch(`${endpoint}/statements?${queryParams}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Experience-API-Version': '1.0.3',
'X-VP': jwt
}
});
Key Filtering Parameters
Parameter
Description
Example
limit
Maximum number of statements to return
limit=20
since
ISO 8601 date to filter statements after
since=2024-03-01T00:00:00Z
until
ISO 8601 date to filter statements before
until=2024-03-31T23:59:59Z
verb
Filter by verb ID
verb=http://adlnet.gov/expapi/verbs/completed
activity
Filter by activity ID
activity=http://yourgame.com/activities/level-1
ascending
Return in ascending order (oldest first)
ascending=true
Using Pagination
For very large datasets, implement pagination:
// First page
let more = "";
const getPage = async (more) => {
const url = more || `${endpoint}/statements?${queryParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Experience-API-Version': '1.0.3',
'X-VP': jwt
}
});
const data = await response.json();
// Process the statements
processStatements(data.statements);
// Check if there are more pages
return data.more || null;
};
// Initial request
more = await getPage();
// Get next page if available
if (more) {
more = await getPage(more);
}
Reducing Statement Size
If dealing with extremely large data in extensions:
Reference Instead of Embed: Store large data elsewhere and include a reference URL in your statement: