For the complete documentation index, see llms.txt. This page is also available as Markdown.

Record Learning Activity

After a user connects their LearnCard, log what they do — and let them see it in their Activity Feed.

You've connected a user's LearnCard to your platform — see Connect a User's LearnCard. Now log what they do there. Each activity becomes an xAPI statement stored in the user's own LearnCloud, and shows up in their LearnCard Activity Feed.

~15 min · After Connect a User's LearnCard.

When the user accepted your contract, the vp in the redirect wasn't only proof of who consented. It contains a delegate credential the user issued to your DID granting statementAccess: ['read', 'write']. Send that same vp string as the X-VP header and LearnCloud will accept statements about that user from you.

So: store the vp per user, server-side, encrypted. It is a bearer credential — never log it or expose it to the browser.

Treat the vp like any bearer credential you hold on a user's behalf. Check verifyConsent before each write and delete the stored vp when a user withdraws — the pattern below does both. Today the xAPI endpoint validates the vp itself rather than re-checking live consent, so this check is yours to make.

Save the blocks below together as record-activity.mjs next to the two helper files:

import { initLearnCard } from '@learncard/init';
import { verifyConsentRedirect } from './consent-callback.mjs';
import { readUserData } from './read-user-data.mjs';

const { SECURE_SEED, CONTRACT_URI, CONSENT_VP: vp } = process.env;
if (!SECURE_SEED || !CONTRACT_URI || !vp) {
    throw new Error('Set SECURE_SEED, CONTRACT_URI, and CONSENT_VP');
}
const learnCard = await initLearnCard({ seed: SECURE_SEED, network: true });
const verified = await verifyConsentRedirect(learnCard, vp, CONTRACT_URI);
if (verified.status !== 'verified') throw new Error('Consent denied or abandoned');
const userDid = verified.userDid; // Verified holder, not the redirect's did parameter

const requireConsent = async () => {
    if ((await readUserData(learnCard, userDid, CONTRACT_URI)).length === 0) {
        throw new Error('No active consent');
    }
};

2. Send a statement

The actor is the user (their DID), not you. LearnCloud checks that the delegate credential inside X-VP was issued by that same DID and includes write; the statement is then stored under the user's account.

This example uses the standard completed verb. Replace the sample activity URI with a stable URI owned by your platform, following the activity and verb naming rules.

The proxy copies the signed contractUri into context.extensions['https://learncard.com/xapi/extensions/contractUri'] automatically. You do not need to modify or re-sign the presentation.

3. Read statements back

Use the same vp and actor with a GET request. Delegated reads require read instead of write, with the same issuer/actor and holder matching rules.

The response contains statements and may contain a more pagination link. The query selects the user's activity; the local filter selects this contract. See contract-scoped queries for more detail.

Run node --env-file=.env record-activity.mjs once. Re-running creates another activity record; production writes should come from authorized learning events with your own duplicate prevention, not from the consent callback GET.

4. What the user sees

In LearnCard, open Manage Data Sharing, select your contract, and open xAPI Data Feed. The Activity Feed shows the activity name, verb, timestamp, and any result data, with a raw JSON view for inspection. It filters by the contract extension injected above, so keeping the original consent VP also keeps the activity associated with the right contract.

What you should see

  • The POST returns a successful response containing the stored statement ID(s).

  • The read prints Found this statement: true for the newly stored activity; if other activity fills the first page, follow pagination.

  • The learner sees completed · Lesson 1 in the contract's Activity Feed.

  • After withdrawal, the consent guard stops further reads and writes with No active consent.

Troubleshooting

For authentication failures, see Common causes of 401, including the delegated-access exception to direct actor/holder matching. Do not substitute an API key or Partner Connect identity token for vp.

If a stored statement is missing from the contract feed, check its contract extension against the saved CONTRACT_URI and confirm you used the original consent VP. A missing or invalid actor account is a bad request, not evidence that you need another identity token.

Next steps

Last updated

Was this helpful?