How to Send Custom xAPI Statements in 2021

How to Send Custom xAPI Statements in 2021 Tutorial Cover Photo

In 2021, there’s no reason to rely on the out-of-the-box reporting capabilities of our authoring programs and learning tools.

With xAPI, we can track every aspect of a learning experience no matter where the learning takes place. This includes on mobile devices, LMSs, simulations, websites, PDFs, podcasts, videos, and more.

To use xAPI to its full potential, we often need to send custom statements. This tutorial will help you do exactly that.

What is xAPI 

xAPI refers to the Experience API, which was initially known as the Tin Can API or "Project Tin Can." It is a technical specification for the Learning and Development (L&D) industry. 

The xAPI specification defines how to structure, send, and retrieve learning and performance data. When we adhere to the specification, it ensures that our people, tools, and programs are all speaking the same language.

This makes it easier for people and machines to understand the data, which in turn makes it more efficient for us to analyze and act on the data.

xAPI Statements

The basic building block of xAPI is an xAPI statement. These statements capture data about human experiences, and they are in a format that both humans and computers can understand.

Each xAPI statement is composed of three core components: an actor, verb, and object. For example:

  • Devlin (actor) read (verb) xAPI Article (object).
  • Yeo (actor) approved (verb) Project 1 (object).
  • Team A (actor) won (verb) the Knowledge Championship (object).

The actor, verb, and object components are required on every xAPI statement, but you can add much more detail as needed. For example, more detailed xAPI statements can tell you the following information:

  • Devlin spent 2 minutes and 47 seconds reading the xAPI article on www.devlinpeck.com.
  • Yeo commented, “Great article, but I’m having trouble with the statements section” on Devlin’s xAPI article.
  • Team A selected Choice B on the tenth question in the CPR simulation. This is the correct answer and their current score is 80%.

We’ll cover xAPI statements in much more depth  later in this tutorial. For now, it’s important to know that xAPI data is sent and received in the form of xAPI statements. These statements can hold specific, flexible data about any human experience. 

You send the xAPI statements to a Learning Record Store (LRS). An LRS is a database for your xAPI data that makes it easy to create dashboards and run reports. 

This tutorial will not cover LRSs in detail; I will create an article on different LRS options in the future. The important thing to know is that you cannot use xAPI without an LRS to store the statements in.

xAPI Benefits

The main benefit of xAPI is that it allows you to track learning wherever it occurs. It empowers you to bring data from many different learning tools and systems into a single tool (which is often the LRS) for analysis.

This means that tracking is no longer limited to what occurs on a Learning Management System (LMS). You can track learning that occurs in an eLearning course, on a website, in a mobile app, in a flight simulator, in a face-to-face session, or anywhere else.

On top of this, xAPI is very flexible. For example, you can use xAPI data to determine things like:

  • Which resources people access within an eLearning course and how much time they spend viewing each resource
  • Which notes and responses people type into an online, xAPI-enabled workbook
  • Which potential clients a sales rep had conversations with (along with their notes about each conversation)
  • Which actions a pilot takes in a flight simulator (control wheel movements, buttons pressed, etc.), as well as which actions the pilot takes while flying the plane itself
  • And so much more.

If the activity is occurring on a system that, at some point, has access to the internet, then you can likely track any aspect of that activity with xAPI. 

Finally, xAPI data is interoperable. Just as you can upload a SCORM package to any LMS, you can send xAPI data to and from any LRS. 

This means that, when using xAPI, you are not locked to any specific software. You can bring your data with you to new tools, platforms, and vendors. It also means that xAPI-enabled tools know how to deal with data they receive from other xAPI-enabled tools.

In essence, xAPI gives you one language and set of rules that you can use to track and report on all of the human learning and performance experiences that occur.

xAPI Examples

Let’s consider some of the different ways that you can use xAPI:

  • An adaptive eLearning assessment shows you questions based on your performance in other eLearning modules AND on the job.
  • An L&D team analyzes employee learning and performance data to determine the effectiveness of their learning programs.
  • An xAPI-enabled chatbot looks at someone’s past learning data to recommend the next learning resource that the person should access.
  • A company tracks learning experiences without an LMS by sending custom xAPI statements from their website, learning experiences, and learning resources.
  • A talent manager looks at a dashboard showing which employees would be good fits for which positions based on their learning and on-the-job performance data.
  • An eLearning team uses xAPI to track specific behaviors in their eLearning offerings, and then they use this data to improve the user experience.

As you can see, xAPI is often used to track and analyze human learning and performance. xAPI data can help designers create adaptive learning experiences and make data-driven improvements to their learning offerings.

This starts with collecting the right data, and later in this tutorial, I’ll show you exactly how to collect the data that you need.

xAPI vs. SCORM

The main learning specification that you’ve probably heard of is SCORM. 

SCORM deals with how eLearning packages are hosted on an LMS, as well as how the eLearning package and LMS communicate with one another. As the industry has recognized for some time now, most learning occurs outside the LMS — it occurs on the open web, in mobile apps, in learning games, on discussion boards, and more.

Also, even though SCORM is intended to track eLearning on an LMS, it does not give you much information. It can only track basic completion and quiz score data. (See this slide-level analytics article to learn more about the detailed eLearning activity that you can track with xAPI.)

In other words, SCORM makes it easy to host eLearning courses on multiple LMSs, but it doesn’t help you collect specific, nuanced data from a wide array of learning and performance experiences. This is where xAPI comes in.

xAPI Profiles

xAPI Profiles, previously known as recipes, define how xAPI should be implemented within an organization or industry’s unique context.

This additional structure increases interoperability (making it easier to analyze data from different tools or organizations) by providing a clear set of rules for how to implement xAPI for a given use case.

Organizations may create their own xAPI profiles if they are doing a large-scale rollout. In addition to this, they can draw on public xAPI profiles. The public xAPI profiles include the video profile, serious games profile, open badges profile, and more.

The ADL is rolling out an xAPI Profile Server later this year to make it easier to find profile information and use profiles in xAPI projects.

WIth all of this being said, you do not have to use a profile to use xAPI. You can begin conducting xAPI experiments at your organization without an xAPI profile, but you should carefully consider the statements that you will use to track human learning and performance.

cmi5

Perhaps the most notable and “game-changing” xAPI profile is cmi5. This is the xAPI profile that replaces SCORM completely.

Cmi5 stands for “computer managed instruction,” and it defines how LMSs should communicate with xAPI eLearning content. Whereas xAPI as a whole is too broad and flexible to replace SCORM, cmi5 as an xAPI profile is exactly what’s replacing SCORM. It’s also often referred to as “xAPI with rules.”

Cmi5 outlines how ten specific xAPI verbs should be used to define human activity on the LMS and in eLearning modules. Furthermore, it defines how the LMS and eLearning must communicate with one another, and this leads to some amazing new possibilities.

Due to these benefits and the straightforward use case for xAPI, the US Department of Defense is in the process of adopting cmi5. When this transition from SCORM to cmi5 by the DoD is official, we expect that industry adoption will increase rapidly (especially since this is exactly what happened with SCORM). 

How to Use xAPI

When it comes to using xAPI, there are a few options. We can use the out-of-the-box reporting capabilities from xAPI-enabled tools, or we can send custom xAPI statements from any learning experience we please.

xAPI Tools

Articulate Storyline and Adobe Captivate are the most popular eLearning authoring tools, and they both include out-of-the-box xAPI reporting.

However, they give you little control over which statements are sent. You get data about which slides people view, how they score on the assessments, and whether people complete the course, but to collect this data, you must upload the eLearning project to an LMS.

Important note: Articulate announced that they are working to add a custom xAPI interface to the tool some time in 2021. I expect that this will still require you to upload the eLearning project as an xAPI package to an LMS, but it should make it much easier to collect the data you want from Storyline projects.

Custom xAPI

When you want to take the xAPI statements into your own hands, it’s time to use custom xAPI. This lets you use xAPI to its full potential and track learning wherever it occurs, not just in an eLearning course on an LMS.

The most popular way to send custom xAPI statements from the web is with JavaScript. In the following sections, we’ll build an xAPI statement together and then send it with the Fetch API.

How to Create an xAPI Statement

Before we send an xAPI statement, we must create it. This section will teach you more about what exactly an xAPI statement is, as well as how to structure and define each part of an xAPI statement.

Essentially, this section is about how to “speak the language” of xAPI. These requirements and options are what set xAPI apart from any other API, and it’s why, by adhering to them, we can communicate more fluently across learning programs and platforms.

Depending on your previous experience with JavaScript, you may feel overwhelmed by all of the new concepts and information to come. Feel free to bookmark this page and refer to it at different points in your xAPI journey.

To follow along, I suggest that you create an “xapi.js” file with your code editor of choice. If you do not have a code editor, then I suggest you download Visual Studio Code.

Technical Foundations

There are a few technical concepts that you should familiarize yourself with before moving deeper into this section.

JSON

xAPI Statements are JSON objects. So, to understand and use xAPI, you should know how to make sense of a JSON object.

JSON stands for JavaScript Object Notation, and it’s a way to format data. JSON is not unique to xAPI — it is the data format used for most web communication today.

JSON objects are composed of key:value pairs. For example:

{% c-block language="js" %}
{
  "name": "Devlin Peck"
}
{% c-block-end %}

In this example, "name" is the key and "Devlin Peck" is the value. The key and value are separated by a colon.

Also notice that we created the object with curly brackets. An opening curly bracket to start the object and a closing curly bracket to close it.

To add a new key:value pair to the object, we add a comma between the two pairs.

{% c-block language="js" %}
{
  "name": "Devlin Peck",
  "title": "eLearning developer"
}
{% c-block-end %}

We can also nest objects within objects. In the example below, the "name" key is set equal to an object as its value. 

{% c-block language="js" %}
{
  "name": {
      "firstName": "Devlin",
      "lastName": "Peck"
  },
  "title": "eLearning developer"
}
{% c-block-end %}

Notice that in the examples above, all of the keys include double quotes around them. This is necessary to create a valid JSON object.

However, to create a regular JavaScript object, the double quotes are not required around key names. Going forward, we will exclude those quotes. 

When it comes time to send the xAPI statement, we will convert the JavaScript object into JSON format. I will show you how to do this later in the tutorial.

Parents, Children, and Siblings

This concept refers to how objects are related to one another. 

In the example from the last section, “firstName” and “lastName” are children of the “name” property, and the “name” property is a parent of the “firstName” and “lastName” properties. “firstName” and “lastName” are siblings.

{% c-block language="js" %}
{
  "name": {
      "firstName": "Devlin",
      "lastName": "Peck"
  }
}
{% c-block-end %}

I use indentation to help make these relationships more clear, but you will see me use this language throughout this tutorial. This language is also not unique to xAPI. It is a common way to refer to relationships within data structures.

JavaScript Data Types

If you’re completely new to JavaScript, then it will help to get a handle on a few of the different possible data types.

  • Strings contain any combination of letters, numbers, or characters, and they are enclosed by single or double quotes.
  • Numbers represent numbers, as the name implies. Numbers do not need to be surrounded by quotes.
  • Booleans are equal to either true or false, and they are not enclosed by quotes.
  • Arrays hold a list of data, and they are enclosed by square brackets.
  • Objects contain key:value pairs and they’re surrounded by curly brackets.

To create a variable, we use either the “let” or “const” keyword. We use the “let” keyword when we know that we will change the variable’s value later in the code, and we use the “const” keyword when the variable’s value will not change.

{% c-block language="js" %}
let changingString = "I am a string that you can change."
changingString = "Now I am equal to a different string!"

const number = 22
const boolean = true
const array = ["Apples", "Oranges", "Bananas"]
const object = {
  stringKey: "I am a string",
  numberKey: 15
}
{% c-block-end %}

We can also use a variable’s value in JavaScript by entering its name without quotes.

{% c-block language="js" %}
let total = 0
const firstNumber = 2
const secondNumber = 5
total = firstNumber + secondNumber
{% c-block-end %}

In the example above, we set “total” equal to firstNumber + secondNumber, which returns the value of 7.

IRIs

Internationalized Resource Identifiers, or IRIs, are another important concept when it comes to xAPI. If you have heard of URIs, then these are nearly identical...the only difference is that IRIs can contain non-Latin characters.

If you’re completely new to IRIs, then the best thing to compare it to is a URL. https://www.devlinpeck.com is a valid URL because it resolves to a real website...specifically, the site that you’re on right now. 

IRIs for xAPI look just like URLs, but they do not need to resolve to real websites. For example, https://www.devlinpeck.com/iri/iri-lesson-101 is not a valid URL because it does not resolve to a real webpage, but it is a valid IRI.

So, to keep things simple, IRIs are URLs that do not need to resolve to real webpages.

That being said, if you’re creating a new IRI, then you should use a domain that you or your organization controls. We’ll discuss when and why you may want to do this later in the tutorial.

The Actor Object

With the technical foundations out of the way, it’s time to start building the xAPI statement.

The first part of any xAPI statement is the actor object. This object tells us who took the action, and it’s required for an xAPI statement to be considered valid.

Let’s start by creating the framework for the xAPI statement itself, as well as the actor object:

{% c-block language="js" %}
{
  actor: {

  }
}
{% c-block-end %}

Now we have a few ways to identify the actor. If it’s an individual, then we can identify them via an email address or an account. If it’s a group, then we can either name the group or keep it anonymous.

Identifying an Actor via Email

If we’re identifying an actor via their email address, then the first thing we should do is set the actor’s objectType property to “Agent”. This is because we’re referring to a single actor.

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent"
  }
}
{% c-block-end %}

Next, we can include the agent’s email address. The simplest way to do this is with the “mbox” property. When setting the value, we must precede the email address with “mailto:”, just as if we were creating an email link on a webpage.

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent",
    mbox: "mailto:devlinpeck@gmail.com"
  }
}
{% c-block-end %}

Don’t forget to include a comma between the two key:value pairs (in this case, right after “Agent”).

Alternatively, you can encrypt the email address and include it as a SHA-1 hash.

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent",
    mbox: "485ADCCDFC91FD2E7DBBE51F1B844A160364298A"
  }
}
{% c-block-end %}

If you have the actor’s name, then you can also include that in the actor object.

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent",
    mbox: "mailto:devlinpeck@gmail.com",
    name: "Devlin Peck"
  }
}
{% c-block-end %}

The example above is the most common way to represent the Agent in the actor object.

Identifying an Actor via Account

In case you do not have the actor’s email address and you’re collecting statements associated with a specific account, then you can identify them via that account.

Since we’re identifying a single actor, we should first set the objectType to “Agent”.

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent"
  }
}
{% c-block-end %}

Next, we can create a new “account” object where we will hold the account’s homepage and name.

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent".
    account: {
      homePage: "https://www.linkedin.com/",
      name: "devlinpeck"
    }
  }
}
{% c-block-end %}

This new account object identifies me via my LinkedIn account. It sets the homePage property equal to the homepage that the account is associated with, then it includes my username in the “name” property.

We can still give me a name by adding the name property as a child to the actor object, like so:

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent",
    account: {
      homePage: "https://www.linkedin.com/",
      name: "devlinpeck"
    },
    name: "Devlin Peck"
  }
}
{% c-block-end %}

Identifying an Actor via OpenID

Finally, we can identify a single actor via their OpenID. If you’re not already familiar with OpenID, then you do not need to worry about this option.

However, if this approach makes sense for you or your organization, then you can identify the actor with the “openid” property, like so:

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent",
    openid: "https://www.devlinpeck.com/users/devlinpeck",
    name: "Devlin Peck"
  }
}
{% c-block-end %}

Note that when you identify an actor, you should only use one of the unique identification methods that we discussed. For example, you cannot identify someone via their email address and their openID. You must choose one identifier for any given xAPI statement.

Identifying Actors via an Identified Group

Sometimes, a group of agents take action. You can identify this group within the actor object.

The first thing you must do to identify a group is set the objectType property to “Group”, like so:

{% c-block language="js" %}
{
  actor: {
    objectType: "Group"
  }
}
{% c-block-end %}

If the group is “identified,”then we can identify the group using the same methods that we used to identify a single actor (email address, account, etc.). For example:

{% c-block language="js" %}
{
  actor: {
    objectType: "Group",
    mbox: "mailto:support@peck.consulting",
    name: "Peck Consulting Support Team"
  }
}
{% c-block-end %}

We can also identify individual actors that are associated with a group. We do this with the “member” property, which includes an array of actors.

{% c-block language="js" %}
{
  actor: {
    objectType: "Group",
    mbox: "mailto:support@peck.consulting",
    name: "Peck Consulting Support Team",
    member: [
      {
        mbox: "mailto:devlin@peck.consulting",
        name: "Devlin Peck"
      },
      {
        mbox: "mailto:taylor@peck.consulting",
        name: "Taylor Villucci Peck"
      }
    ]
  }
}
{% c-block-end %}

This list of members does not need to be exhaustive. It’s up to you which members you include here, if any.

Identifying Actors via an Anonymous Group

If you would like to identify a group that does not have a unique identifier associated with it (like an account, email address, etc.), then you can identify the group with a list of members and an optional name. That approach would look like this:

{% c-block language="js" %}
{
  actor: {
    objectType: "Group",
    name: "Support Team",
    member: [
      {
        mbox: "mailto:devlin@peck.consulting",
        name: "Devlin Peck"
      },
      {
        mbox: "mailto:taylor@peck.consulting",
        name: "Taylor Villucci Peck"
      }
    ]
  }
}
{% c-block-end %}

The list of members is required when using anonymous groups, and the member list can only include individual agents — not other groups.

The Verb Object

Now that you know how to create an actor object, it’s time to create the required verb object. 

The verb tells us the action that took place, or, more specifically, the action that the actor took. 

Let’s create the verb object now:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {

  }
}
{% c-block-end %}

Since your actor object will look different depending on how you’re identifying the actor, we’ll leave those three periods as a placeholder.

Also, don’t forget to include a comma between the actor object and the verb object. Remember, we need commas between the key:value pairs.

Next, we need to add the required “id” property. This property is an IRI that uniquely identifies a given verb. For example:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    id: "http://adlnet.gov/expapi/verbs/answered"
  }
}
{% c-block-end %}

The reason that we use an IRI to identify a verb is because the verb itself can have different meanings. The classic example given in the xAPI specification itself is with the verb “fired.” Fired can refer to firing a kiln, firing an employee, or firing a weapon.

By providing a unique IRI for each verb, we can learn more about the real meaning behind the verb. The IRIs are often defined in industry profiles, organization-specific profiles, or on the xAPI Vocab Server.

One of the most important considerations when it comes to using verb IDs is to not reinvent the wheel. The community has already agreed upon certain verb definitions, so you should check the vocab server verb list first.

If you go to that verb list and look for the verb "answered," you’ll see the IRI that I used above, as well as the definition for that verb ID: “Indicates the actor replied to a question, where the object is generally an activity representing the question.”

If you do not find the verb that you’d like to use (or an equivalent verb) on the vocab server or in any profiles that you’re using, then you can consider creating a new, unique IRI to refer to that verb.

When creating IRIs, you should always use a root URL that your organization owns or controls. For example, if I were to create an IRI for a verb called flying, I would do so like this:

{% c-block language="js" %}
"https://www.devlinpeck.com/verbs/flying"
{% c-block-end %} 

Since this is an IRI, it does not need to resolve to a real webpage. However, since I own the domain, I would be able to create a webpage for it that includes the verb’s definition if I were to choose to do so. 

Using a domain that you control also ensures that no other organizations or people will create a verb with the same IRI but a different meaning.

The next part of the verb object is a display property with the plain-text name of the verb. The name is included as a language map object so that you know which language the name is in. For example:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    id: "http://adlnet.gov/expapi/verbs/answered",
    display: {
      "en-US": "answered"
    }
  }
}
{% c-block-end %}

You can add additional languages to this display property by adding additional key:value pairs:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    id: "http://adlnet.gov/expapi/verbs/answered",
    display: {
      "en-US": "answered",
      "es": "respondido"
    }
  }
}
{% c-block-end %}

If you’re wondering how to identify the correct language codes to use for the language map, you can use an ISO 639 language code combined with an ISO 3166 country code (with a hyphen in between the two).

And that’s all there is to it for the verb object! The verb ID is required, and the display property is highly recommended.

The “Object” Object

Next up we have the “object” object. This object tells us what the actor is acting upon. It’s the third and final object that’s required for an xAPI statement to be considered valid.

So, let’s create the object:.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {

  }
}
{% c-block-end %}

The “object” object has an objectType property that tells us (you guessed it) the object’s type. The type can be either an Activity, Agent, Group, SubStatement, or StatementRef. 

The “Activity” objectType

For most xAPI statements, the objectType is “Activity”. If you’re new to xAPI, then this is honestly the only objectType that you should concern yourself with. Don’t worry if you feel confused by the objectTypes that we’ll discuss in the following sections.

We can set our objectType to “Activity”, but if we leave this out, then the statement will assume that the objectType is “Activity” by default.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "Activity"
  }
}
{% c-block-end %}

When the object is an activity, we must include an “id” property. Similarly to the verb object’s ID, the ID must be an IRI.

However, there is not a library of activity IDs for you to pull from. You will need to create an IRI for the specific activity in question.

If the user is accessing a certain webpage, then the activity ID may be the URL for that webpage. 

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "Activity",
    id: "https://www.devlinpeck.com/tutorials/send-xapi-statements"
  }
}
{% c-block-end %}

However, if the user is viewing a resource within an eLearning project, then you’ll need to create an IRI for that resource. Here are some pointers for doing so:

  1. Use a root URL that you or your organization owns or controls
  2. Add “folders” to the URL that make sense semantically. For example, “https://www.devlinpeck.com/elearning/xapi-101/xapi-checklist.” By looking at this IRI, we know that this is an eLearning project called xAPI 101 with a resource named xAPI Checklist.
  3. Ensure that your approach to creating IRIs is standardized and communicated to other people who will work with xAPI data. For example, you do not want someone on a different team to refer to a resource in their course with “https://www.devlinpeck.com/training/resources/elearning-checklist.” You want to keep things consistent.

If you’re getting started with xAPI and you just want to experiment with it, then you shouldn’t spend too much time creating IRIs. As long as you follow the general guidelines above, then you will be good to go.

However, for large-scale xAPI initiatives, standardization becomes more important. When your data is consistent, it becomes much easier to analyze and gain insights.

xAPI Activity Description

Let’s move on from the ID property. When the object type is an activity, we can also include a “definition” property.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "Activity",
    id: "https://www.devlinpeck.com/tutorials/send-xapi-statements",
    definition: {

    }
  }
}
{% c-block-end %}

This property is an object that can contain multiple properties within, including the name of the activity, a description of the activity, the type of activity that it is, and more.

Both the name and description properties are language maps, similar to the verb’s display property in the verb object.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "Activity",
    id: "https://www.devlinpeck.com/tutorials/send-xapi-statements",
    definition: {
      name: {
        "en-US": "How to Send Custom xAPI Statements"
      },
      description: {
        "en-US": "Tutorial on devlinpeck.com about how to send custom xAPI statements."
      }
    }
  }
}
{% c-block-end %}

The definition’s “type” property is an IRI. This helps us make sense of the type of activity that we’re dealing with. 

Similar to the verb’s ID, we can see a list of activity types that the community has already defined on the xAPI Vocab Server

Since this tutorial serves as a resource, I will opt for the “resource” activity type. If you do not find an activity type IRI that fits your activity, then you can create one using the guidelines we’ve already discussed.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "Activity",
    id: "https://www.devlinpeck.com/tutorials/send-xapi-statements",
    definition: {
      name: {
        "en-US": "How to Send Custom xAPI Statements"
      },
      description: {
        "en-US": "Tutorial on devlinpeck.com about how to send custom xAPI statements."
      },
      type: "http://id.tincanapi.com/activitytype/resource"
    }
  }
}
{% c-block-end %}

The activity definition also accepts a "moreInfo" property. This optional property would include a link where you could go to learn more about the activity. 

It can also include a set of interaction properties. These properties are “legacy” properties that help make xAPI play nicely with SCORM 2004. You likely will not need to use these when sending custom xAPI, but in case you do, you can learn more about them in the official xAPI specification.

Finally, the activity definition can include an “extensions” property, which we will explore later in this tutorial.

The “Agent” and “Group” objectType

Sometimes, the actor will act upon another agent or group. For example, “Devlin promoted Susie” or “Devlin disbanded the Support Team.” 

When you need to create xAPI statements like this, you must set the objectType equal to “Agent” or “Group”. Then, you include the agent or group’s identifier, which we discussed in the Actor section of this tutorial.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "Agent",
    mbox: "mailto:susie@peck.consulting"
  }
}
{% c-block-end %}

The “StatementRef” objectType

There may be times when someone is acting on another statement that already exists. The xAPI spec gives the examples of commenting on a statement and voiding a statement. 

In those cases, the object would be the actual statement that you’re commenting on or voiding. You can reference the statement via the statement’s unique ID. 

We have not yet discussed the statement’s ID, but it is a unique string of characters that identifies the statement. You can generate this ID yourself, or you can let the LRS automatically generate this unique ID for you.

Here’s how you would use a statement reference as your object:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "StatementRef",
    id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0"
  }
}
{% c-block-end %}

The “SubStatement” objectType

Substatements are a type of object that signifies future intent. 

For example, if I wanted to send an xAPI statement saying that I planned to write this tutorial, then the substatement would include the entire statement associated with writing this article. 

Consider the full example below, and remember — if you’re feeling confused, don’t worry. This type of statement is not used often.

{% c-block language="js" %}
{
  actor: {
    objectType: "Agent",
    mbox: "mailto:devlinpeck@gmail.com",
    name: "Devlin Peck"
  },
  verb: {
    id: "https://www.devlinpeck.com/xapi/verbs/planned",
    display: {
      "en-US": "planned"
    }
  },
  object: {
    objectType: "SubStatement",
    actor: {
      objectType: "Agent",
      mbox: "mailto:devlinpeck@gmail.com",
      name: "Devlin Peck"
    },
    verb: {
      id: "https://www.devlinpeck.com/xapi/verbs/wrote",
      display: {
        "en-US": "wrote"
      }
    },
    object: {
      objectType: "Activity",
      id: "https://www.devlinpeck.com/tutorials/send-xapi-statements",
      definition: {
        name: {
          "en-US": "How to Send Custom xAPI Statements"
        },
        description: {
          "en-US": "Tutorial on devlinpeck.com about how to send custom xAPI statements."
        },
        type: "http://id.tincanapi.com/activitytype/resource"
      }
    }
  }
}
{% c-block-end %}

The Result Object

The result object is an optional object that we can include in an xAPI statement. It tells us about the user’s response to a given question, their score on an assessment, and more.

First, let’s create the result object.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  result: {

  }
}
{% c-block-end %}

The result object may contain information about whether or not the activity was attempted successfully, as well as information about whether or not it was completed successfully.

You can include this information via the “success” and “completion” properties, respectively. The values should be either true or false (also known as Booleans). Boolean values do not have quotes around them, as you’ll see below.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  result: {
    success: true,
    completion: false
  }
}
{% c-block-end %}

You can also include the user’s response to a question in the result object. This may be a multiple-choice selection or an open-text response. You include it via the “response” property, like so:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  result: {
    success: true,
    completion: false,
    response: "This is my response to a text-entry question."
  }
}
{% c-block-end %}

We can also include duration information in the result object. This tells us how long someone spends on a given slide, resource, course, or activity.

To manage the timer, you need to use JavaScript. This is beyond the scope of this article but you can learn more about it in this xAPI + Storyline tutorial.

The format for the duration property is also quite specific. It must adhere to ISO 8601 Duration format. Check it out in this example:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  result: {
    success: true,
    completion: false,
    response: "This is my response to a text-entry question.",
    duration: "PT2H33M18S"
  }
}
{% c-block-end %}

This duration tells us that the statement occurred over a period of time lasting 2 hours, 33 minutes, and 18 seconds.

Also, since the result object is optional, all of the different properties that may be included in the result object are also optional.

The Result’s Score Object

The result object can also include the score object, which is an object that can contain multiple properties within itself. Let’s take a look at this object:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  result: {
    success: true,
    completion: false,
    response: "This is my response to a text-entry question.",
    duration: "PT2H33M18S",
    score: {
      scaled: 0.8,
      raw: 8,
      min: 0,
      max: 10
    }
  }
}
{% c-block-end %}

These optional properties tell us what the minimum score is for the assessment, the maximum score, the user’s current “raw” score, and the scaled score (which is the raw score divided by the max score).

Finally, the result object may include an extensions property. We will cover extensions later in this tutorial.

The Context Object

The last main part of an xAPI statement that we’ll discuss is the context object. This object (and all of its properties) are optional.

As the name implies, the context object tells us more about the context in which the activity took place.

Registration

The registration property is one of the most important properties on the context object. This property helps you tie a group of statements to a given registration, which is often interpreted as an “attempt.”

For example, imagine that someone is completing an eLearning course. We would want all of their statements — about the slides they’re viewing, the questions they’re answering, the quizzes they’re passing, and more — to be associated with the same registration. 

This way, when we look at the xAPI data, we can determine how many “attempts” there are on a given learning object by looking at the number of unique registrations.

The only downside with this is that it’s up to you, the developer, to decide what counts as an attempt. The two most common ways to do this are:

  1. Every time someone accesses a learning experience, it’s considered an attempt (or unique registration). So if I complete the first half of the learning experience in one sitting, then those statements will hold one registration ID. If I open the learning object again later in the day, the new statements will have a different registration ID.
  2. The attempt is tied to completion, which is often tied to a final assessment. Therefore, every time someone accesses the learning experience, it will have the same or registration ID until they complete the experience or pass or fail an assessment. This way, you can look at how many different registration IDs someone has associated with a given activity to determine how many times they attempted that activity.

The option that you choose will depend on what makes the most sense for you and your team. Regarding the value of this property, it should be a universally unique identifier (or UUID). For example:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  context: {
    registration: "123e4567-e89b-12d3-a456-426614174000"
  }
}
{% c-block-end %}

Instructor and Team

The instructor and team properties include information about, you guessed it, the instructor and team associated with a given statement.

For example, if a student is completing an assessment, you may want to include a note about who the instructor is. Then, when it comes time to analyze the data, you can look at how any given instructor’s students are performing.

The instructor can either be an individual agent or a group.

Likewise, you can include information about the team that the statement relates to. If a customer support agent completes their training program, then we can include their team information on that statement. 

Then, when we look at the data, we can see how many people on each team have completed their training program.

The team property must be equal to a group.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  context: {
    registration: "123e4567-e89b-12d3-a456-426614174000",
    instructor: {
      objectType: "Agent",
      openid: "https://www.devlinpeck.com/users/devlinpeck",
      name: "Devlin Peck"
    },
    team: {
      objectType: "Group",
      mbox: "mailto:support@peck.consulting",
      name: "Peck Consulting Support Team"
    }
  }
}
{% c-block-end %}

Revision and Platform

The revision property tells you about any revisions that were made to the activity associated with the statement, and the platform property tells you more about where the activity took place.

Both of these properties hold open-text values (or Strings), and they can only be included when the statement’s object is an activity.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  context: {
    revision: "Fixed typo in activity IRI",
    platform: "Devlin Peck’s website"
  }
}
{% c-block-end %}

Language

The language property tells us the primary language that the experience occurred in. We define it using the first part of the language maps that we’ve discussed in earlier sections, like so: 

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  context: {
    language: "en-US"
  }
}
{% c-block-end %}

Statement

Sometimes, you may want to give your statement additional context by relating it to another statement. You do this with the statement property.

For example, remember earlier when we used a substatement as the statement’s object? We can use that to signify future intent. Then, when we actually execute that statement, we can refer to the statement where we planned that event with the context's statement property.

To refer to the statement, you would include an object that contains both the statement’s ID and the “StatementRef” objectType.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  context: {
    revision: "Fixed typo in activity IRI",
    platform: "Devlin Peck’s website",
    language: "en-US",
    statement: {
      objectType: "StatementRef",
      id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0"
    }
  }
}
{% c-block-end %}

contextActivities (parent, grouping, category, and other)

Along with the registration, the contextActivities property is one of the most widely used parts of the context object. This property tells us how this statement’s activity relates to other activities. 

To do this, the contextActivities property can include four properties of its own: 

  • Parent. This property tells us which activity is situated as a parent to the activity in this statement. For example, if this statement’s activity is a quiz question, then the parent would be the quiz itself.
  • Grouping. This property tells us which other activities are closely related to this statement’s activity, such as siblings or more distant parent activities. For example, if this statement’s activity is a quiz question, then we may include the overall course that it’s included in via the grouping property.
  • Category. This property tells us whether the statement was created in adherence to any specific xAPI profile.
  • Other. For more loosely-related activities that do not match any of the other properties, you can include them via the “other” property.

The only other thing to know about the contextActivities properties are that their values should be arrays, and within those arrays you should include activity objects. 

Here is a context object that includes everything we’ve discussed so far:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  context: {
    registration: "123e4567-e89b-12d3-a456-426614174000",
    instructor: {
      objectType: "Agent",
      openid: "https://www.devlinpeck.com/users/devlinpeck",
      name: "Devlin Peck"
    },
    team: {
      objectType: "Group",
      mbox: "mailto:support@peck.consulting",
      name: "Peck Consulting Support Team"
    },
    statement: {
      objectType: "StatementRef",
      id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0"
    },
    contextActivities: {
      parent: [
        {
          id: "https://www.devlinpeck.com/quizzes/xapi-quiz"
        }
      ],
      grouping: [
        {
          id: "https://www.devlinpeck.com/courses/xapi-101"
        }
      ],
      category: [
        {
          id: "https://www.devlinpeck.com/profiles/sample-quiz-profile"
        }
      ],
      other: [
        {
          id: "https://www.devlinpeck.com/xapi"
        }
      ]
    }
  }
}
{% c-block-end %}

The context object can also include an extensions property, which we’ll discuss next.

xAPI Extensions

When you need to include data on an xAPI statement that doesn’t fit in any of the predefined properties, then you can use the extensions property.

The extensions object includes key:value pairs where the key is an IRI and the value is anything that you’d like. As you can see, this makes the xAPI specification extremely flexible.

For example, we could create an extension that states which camera someone used to record a video.
{% c-block language="js" %}
{
  "https://www.devlinpeck.com/equipment/camera": "Canon EOS M50"
}
{% c-block-end %}

Notice that I created the IRI with the domain that I own and control, just as we’ve done for other IRIs in this section. With extensions, you can create IRIs as needed to include whatever data you’d like.

If you’re following a specific xAPI profile, then it may tell you which extensions to use and how to use them.

The goal here is to be as consistent as possible with your extension use, both with how you define the extension IRIs and how you format their values.

So, where exactly do we include extensions on an xAPI statement? There are three possible locations: the activity’s definition, the result, and the context. Let’s consider each of them.

The Activity Definition’s Extensions

We discussed the activity definition property earlier, which is a property of the “object” object. It includes the activity’s name, description, type, and more.

However, as we mentioned earlier, it also can include an extension property. This is where you would include additional information about the activity itself. 

Let’s add an extension to the activity we created earlier that says when the resource was published:

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    objectType: "Activity",
    id: "https://www.devlinpeck.com/tutorials/send-xapi-statements",
    definition: {
      name: {
        "en-US": "How to Send Custom xAPI Statements"
      },
      description: {
        "en-US": "Tutorial on devlinpeck.com about how to send custom xAPI statements."
      },
      type: "http://id.tincanapi.com/activitytype/resource",
      extensions: {
        "https://www.devlinpeck.com/extensions/date-published": "February 7th, 2021"
      }
    }
  }
}
{% c-block-end %}

The Result’s Extensions

We can also add extensions to the result object. This is where we would include additional data about the user’s score or the statement’s outcome.

For example, we could add an extension that says how many times the user has attempted a given quiz.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  result: {
    success: true,
    completion: false,
    response: "This is my response to a text-entry question.",
    duration: "PT2H33M18S",
    score: {
      scaled: 0.8,
      raw: 8,
      min: 0,
      max: 10
    },
    extensions: {
      "https://www.devlinpeck.com/extensions/quiz-attempt": 2
    }
  }
}
{% c-block-end %}

The Context’s Extensions

Finally, we can add extensions to the context object. For the example I gave earlier about including information about the equipment used to record a learning experience, we could include that information as a context extension.

{% c-block language="js" %}
{
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  context: {
    registration: "123e4567-e89b-12d3-a456-426614174000",
    instructor: {
      objectType: "Agent",
      openid: "https://www.devlinpeck.com/users/devlinpeck",
      name: "Devlin Peck"
    },
    extensions: {
      "https://www.devlinpeck.com/equipment/camera": "Canon EOS M50"
    }
  }
}
{% c-block-end %}

Additional xAPI Statement Properties

There are several other properties that we can add to an xAPI statement that we have not yet discussed. Let’s explore them now.

Statement ID

Every xAPI statement includes a statement ID. You can provide this unique ID yourself, but in most cases, the LRS will provide it for you.

{% c-block language="js" %}
{
  id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0",
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  }
}
{% c-block-end %}

Timestamp and Stored

The xAPI statement also includes a timestamp and stored property. The timestamp tells us when the xAPI statement was created, and the stored property tells us when it was stored in the LRS.

If you do not provide the statement a timestamp, then the LRS will set the timestamp equal to the stored property when it receives the statement.

The timestamp and stored properties must be formatted according to the ISO 8601 Timestamp format.

{% c-block language="js" %}
{
  id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0",
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  timestamp: "2020-09-04T23:11:32.633Z"
  stored: "2020-09-04T23:11:32.633Z"
}
{% c-block-end %}

Version

You may want to specify the version of xAPI that you used to create the statement.

This version must start with "1.0", and it is often "1.0.0".

{% c-block language="js" %}
{
  id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0",
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  },
  timestamp: "2020-09-04T23:11:32.633Z"
  stored: "2020-09-04T23:11:32.633Z",
  version: "1.0.0"
}
{% c-block-end %}

Attachments

xAPI statements can also include attachments, such as PDFs, videos, images, certificates of completion, and more. Since this is a more technical topic and niche use-case (and this section on xAPI statements is already massive), we will not dive into attachments here.

You can learn more about attachments in the xAPI Specification itself.

Full xAPI Statement Example

If you’ve followed along with this entire section, then you now have a pretty good handle on everything that makes up an xAPI statement.

To get started sending statements, you really only need to worry about the actor, verb, object, and sometimes results objects. However, if you’re going to lead wider-scale xAPI initiatives, then it’s a good idea to have a solid grasp on every part of an xAPI statement.

First, let’s take a look at one of the simplest xAPI statements you can send (with only the required properties included):

{% c-block language="js" %}
{
  actor: {
    mbox: "mailto:devlinpeck@gmail.com"
  },
  verb: {
    id: "http://adlnet.gov/expapi/verbs/answered"
  },
  object: {
    id: "https://www.devlinpeck.com/quiz/send-xapi-statements"
  }
}
{% c-block-end %}

Now let’s consider an xAPI statement that has most of the possible properties included:

{% c-block language="js" %}
{
  id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0",
  actor: {
    objectType: "Agent",
    mbox: "mailto:devlinpeck@gmail.com",
    name: "Devlin Peck"
  },
  verb: {
    id: "http://adlnet.gov/expapi/verbs/answered",
    display: {
      "en-US": "answered",
      "es": "respondido"
    }
  },
  object: {
    objectType: "Activity",
    id: "https://www.devlinpeck.com/quiz/send-xapi-statements",
    definition: {
      name: {
        "en-US": "How to Send Custom xAPI Statements"
      },
      description: {
        "en-US": "Tutorial on devlinpeck.com about how to send custom xAPI statements."
      },
      type: "http://adlnet.gov/expapi/activities/assessment"
    }
  },
  result: {
    success: true,
    completion: false,
    response: "This is my response to a text-entry question.",
    duration: "PT2H33M18S",
    score: {
      scaled: 0.8,
      raw: 8,
      min: 0,
      max: 10
    }
  },
  context: {
    registration: "123e4567-e89b-12d3-a456-426614174000",
    instructor: {
      objectType: "Agent",
      openid: "https://www.devlinpeck.com/users/devlinpeck",
      name: "Devlin Peck"
    },
    team: {
      objectType: "Group",
      mbox: "mailto:support@peck.consulting",
      name: "Peck Consulting Support Team"
    },
    statement: {
      objectType: "StatementRef",
      id: "8f87ccde-bb56-4c2e-ab83-44982ef22df0"
    },
    contextActivities: {
      parent: [
        {
          id: "https://www.devlinpeck.com/quizzes/xapi-quiz"
        }
      ],
      grouping: [
        {
          id: "https://www.devlinpeck.com/courses/xapi-101"
        }
      ],
      category: [
        {
          id: "https://www.devlinpeck.com/profiles/sample-quiz-profile"
        }
      ],
      other: [
        {
          id: "https://www.devlinpeck.com/xapi"
        }
      ]
    }
  },
  timestamp: "2020-09-04T23:11:32.633Z",
  version: "1.0.0"
}
{% c-block-end %}

How to Send an xAPI Statement

Once you’ve generated the xAPI statement object, it’s time to send that statement to an LRS. Let’s take a look at how we can do this with pure JavaScript. No external wrappers or libraries are necessary.

Create the Statement Variable

First, we need to store the xAPI statement that we’ve created in a JavaScript variable. We can create a “statement” variable like this:

{% c-block language="js" %}
const statement = {
  actor: {
    ...
  },
  verb: {
    ...
  },
  object: {
    ...
  }
}
{% c-block-end %}

By adding “const statement =” before we define the statement object, we assign the entire object to a variable titled “statement”. The “const” keyword tells us that the variable will not be changed once it’s defined.

Set up Basic Authentication

Next, we need to get the key and secret from the LRS. The key and secret are used to authenticate the statements that we’ll send.

If you do not know how to access your LRS key and secret, then you should consult your LRS's documentation.

Once you’ve located your key and secret, create an “auth” variable above your “statement” variable. Set the variable equal to the code below, but replace yourKey and yourSecret with the values from your LRS.

{% c-block language="js" %}
const auth = “Basic “ + btoa(“yourKey:yourSecret”)

const statement = {
  ...
}
{% c-block-end %}

Therefore, if my key is h98h19hn and my secret is j01891hd, then the variable should look like this: 

{% c-block language="js" %}
const auth = "Basic " + btoa("h98h19hn:j01891hd")

const statement = {
  ...
}
{% c-block-end %}

Also, make sure that you do not forget the space after “Basic”. 

This variable sets up a basic authentication mechanism that you can use to authenticate the statements with your LRS. To ensure that your data is secure, I suggest that you use an LRS endpoint with write-only permissions. If you do not know how to do this, then you should consult your LRS documentation.

Send the xAPI Statement

Now that we've created the statement object and the authentication variable, we can use the Fetch API to send the xAPI statement. 

We can start our fetch request using the “fetch” keyword, and we’ll do this beneath our statement object: 

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const statement = {
  ...
}

fetch()
{% c-block-end %}

This fetch function takes two arguments. The first argument is the URL where we’d like to send a request. In our case, it will be the statements endpoint for our LRS. 

First, locate your LRS’s endpoint (use your LRS documentation if needed). Next, add “statements” after the trailing slash. Enclose this in quotes within the fetch function’s parentheses, like so:

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const statement = {
  ...
}

fetch("https://devlins-test-lrs.lrs.io/xapi/statements")
{% c-block-end %}

Make sure that you do not forget to add “statements” after the final slash. If you forget to specify this, then your statement will likely not get delivered successfully.

Next, we need to add a comma after the endpoint and create a new object. This object, which is the second argument for the fetch function, sets the options for our request:

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const statement = {
  ...
}

fetch("https://devlins-test-lrs.lrs.io/xapi/statements", {

})
{% c-block-end %}

Now we’ll set the “method” property to “POST” since we’re sending our statement via an HTTP POST request.

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const statement = {
  ...
}

fetch("https://devlins-test-lrs.lrs.io/xapi/statements", {
  method: "POST"
})
{% c-block-end %}

Next, we can add the “headers” property. This property is an object that includes all of our headers for the request. 

We need to add the “Content-Type” header to specify that we’re sending JSON data, and we need to add the “Authorization” header and set it equal to the auth variable that we created earlier.

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const statement = {
  ...
}

fetch("https://devlins-test-lrs.lrs.io/xapi/statements", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": auth
  }
})
{% c-block-end %}

The last thing we need to add in this options object is the body of the request. This is where we include the xAPI statement variable that we created. We can do this with the “body” property, and we set it equal to a JSON version of our xAPI statement.

The JSON.stringify method will convert our statement object into a JSON string, which is exactly what we need.

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const statement = {
  ...
}

fetch("https://devlins-test-lrs.lrs.io/xapi/statements", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": auth
  },
  body: JSON.stringify(statement)
})
{% c-block-end %}

Next, we can chain a “catch” method onto our fetch to catch any errors. This method takes a function as its argument, and that function takes the error as its argument.

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const statement = {
  ...
}

fetch("https://devlins-test-lrs.lrs.io/xapi/statements", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": auth
  },
  body: JSON.stringify(statement)
}).catch(error => console.error(error.message))
{% c-block-end %}

And there you have it! When you execute this JavaScript with a valid xAPI statement, it will send as desired to your LRS. In case it does not send to your LRS, you can check your console (F12) to see what errors occurred.

Create a Reusable Function

In case you’d like to send many xAPI statements from a single web page or project, you can create a reusable function.

I would recommend creating a separate xapi.js file, then creating a “sendStatement” function after defining the auth variable.

You can do so like this:

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const sendStatement = () => {
  const statement = {
    ...
  }

  fetch("https://devlins-test-lrs.lrs.io/xapi/statements", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": auth
    },
    body: JSON.stringify(statement)
  }).catch(error => console.error(error.message))
}
{% c-block-end %}

Now you can add parameters to the sendStatement function and use those placeholders in your statement. For example, if you wanted to manually set the actor’s mbox, the verb ID, and the object ID every time you execute the function, you set the function up like this:

{% c-block language="js" %}
const auth = "Basic " + btoa("yourKey:yourSecret")

const sendStatement = (actorMbox, verbId, objectId) => {
  const statement = {
    actor: {
      mbox: "mailto:" + actorMbox
    },
    verb: {
      id: verbId
    },
    object: {
      id: objectId
    }
  }

  fetch("https://devlins-test-lrs.lrs.io/xapi/statements", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": auth
    },
    body: JSON.stringify(statement)
  }).catch(error => console.error(error.message))
}
{% c-block-end %}

Now, when you want to execute this code from your project, you would first need to link to the xapi.js file from your project’s HTML file. Next, you would execute the JavaScript function like this:

{% c-block language="js" %}
sendStatement("devlinpeck@gmail.com", "http://adlnet.gov/expapi/verbs/answered", "https://www.devlinpeck.com/quiz/send-xapi-statements")
{% c-block-end %}

Notice that we pass in the email address, verb ID, and object ID as arguments to the function. This makes it easy for us to reuse the code without having to write it all again from scratch.

You can add parameters as needed to make this function work for you. You can also create separate functions for each verb, as discussed in the Managing Your xAPI Project tutorial.

Conclusion

If you’ve made it this far, then you should have a solid understanding of how to create and send xAPI statements. To make these statements more dynamic, you will need to use JavaScript to collect the data you’re looking for and include it as a variable in the statement.

xAPI statements must be structured in a very specific way, but the specification is also flexible so that you can mold your statements to work with your use-case. If you can adhere to profiles or industry-accepted definitions, then your xAPI statements will be more interoperable.

If you have any questions about your implementation, then you can join my Slack community and ask away. If you have a budget and would like me to help you implement xAPI personally, then feel free to contact me.

Featured Tutorials

View all tutorials