Skip to content

xAPI

Statement delivery to an LRS, alongside SCORM rather than instead of it. A course can run both: report to the LMS through ILmsRuntime and send statements through IXApiRuntime, and neither knows about the other.

Folder What lives here
Runtime/ IXApiRuntime, the live implementation, and the stub
Data/ Statement, actor, verb, activity, result, context
Helpers/ XApiVerbs and XApiStatements — the shapes you actually send
Transport/ The queue, the retry policy, and the UnityWebRequest sender
Serialization/ Hand-written JSON, because JsonUtility cannot express a statement

XApiRuntimeFactory sits at the root and builds a runtime from an XApiEndpointConfig.

The one thing to know about the contract

Everything public returns Task<bool>: SendStatementAsync, InitializeAsync, FinishAsync, and every helper in XApiStatements.

Internally delivery is a tri-state — delivered, retryable, or rejected — and that distinction matters a great deal, because a retryable failure must stay queued and a rejected statement must not. But it is deliberately internal. true means "accepted or safely queued"; false means "the LRS refused this and retrying will not help". A course cannot act usefully on the difference between a timeout and a 503 — the queue already handles both — and exposing a third state would make every call site a switch over cases most courses would get wrong.

So the queue sees three outcomes and callers see two. If you need the detail, it is in the console: every refusal is logged with its status code.

Statements a course does not have to build

XApiStatements has the dozen shapes that cover most courses — PassedAsync, AnsweredAsync, SuspendedAsync and so on. They exist because the specification has rules that are easy to break by hand, and an LRS enforces them by rejecting the whole statement:

  • A raw score outside its stated min..max range is a rejection, not a clamped score. XApiScore.Raw clamps and warns instead, because a slightly wrong number on a report beats a lost pass.
  • A registration must be a UUID.
  • A verb needs an id and an activity needs an id; empty strings are checked before send rather than after refusal.