Connect a User's LearnCard to Your Platform
Link a learner's LearnCard once, then read their data and issue to them automatically — with their permission.
A learner links their LearnCard to your platform once and chooses what you may read and write. LearnCard calls this a ConsentFlow. This tutorial asks for an optional name and permission to send achievements.
Do you need this, or just send()?
Just awarding a badge? If you know the user's email, use
send()— it's simpler and requires no setup for the user.Ongoing relationship? Use this pattern. The user links their LearnCard once and consents to what you may read/write. You then issue automatically (every lesson, every level) with no emails or claim links — this is what AI tutors, LMSs, and games need.
Guardian approval for a single sent credential is a different, simpler thing: use options.guardianEmail on send(). See Guardian-gated credentials.
The one-line version
npx @learncard/cli consent-contractCreates the contract (or reuses the one in .env), prints the link users visit to connect, and writes the four scripts this page walks through — create-contract.mjs, consent-callback.mjs, read-user-data.mjs, issue-through-contract.mjs — into your folder. Then node --env-file=.env consent-callback.mjs and you're at step 3.
The rest of this page is the same flow in your own code.
Prerequisites
The
.envfrom the Quickstart (SECURE_SEED,PROFILE_ID) — or runnpx @learncard/cli init.RETURN_TOin that.env: the public HTTPS URL that reaches this example's/consent-callbackon port 3000 (a tunnel like ngrok is fine for development).consent-contractwrites it for you.A second LearnCard account (a phone, or a different browser profile) to play the user who consents.
npm install @learncard/init, and the five.mjsfiles below in one folder —consent-contractwrites all of them.
Never commit .env or put the seed in browser code. Disable callback query-string logging in your proxy, analytics, and application: the presentation in the URL is sensitive.
1. Create the contract
Save as create-contract.mjs. It creates your service profile if absent, then a new contract. Errors are not treated as successful profile creation.
Run node --env-file=.env create-contract.mjs. Save the returned contractUri as CONTRACT_URI in .env. Keep the same seed for every step. Each run creates a new contract; reuse the saved URI rather than rerunning setup.
2. Send the user to consent
Open the printed consentUrl with your test account, or use that exact URL as your site's consent link. Its form is https://learncard.app/consent-flow?uri=<encoded contractUri>&returnTo=<encoded URL>; the script encodes both values.
returnTo overrides the contract's redirectUrl. LearnCard accepts HTTP or HTTPS; this example requires HTTPS. The learner can omit their name but must allow achievement issuance.
3. Handle the redirect
Save as consent-callback.mjs. Run node --env-file=.env consent-callback.mjs before accepting the contract.
The redirect includes did (a convenience identifier) and vp (a signed JSON Web Token presentation). Never trust did alone. Verify vp server-side, then read its holder and match its contract URI. The SDK's JWT payload has iss and vp.holder; this example requires them to agree.
Declining does not produce a redirect with a presentation. If the learner returns without vp, treat the attempt as denied or abandoned—not successful consent. Closing the consent page produces no callback at all.
This endpoint confirms consent; it is not a sign-in or account-linking endpoint. The ordinary redirect has no application-bound signed nonce, so a valid presentation can be replayed. Do not create sessions, bind local accounts, expose personal data, or issue credentials on this GET. A production account-linking flow needs separately authenticated, session-bound proof and replay protection. A valid signature also does not prove consent is still active: check the network before each use.
4. Read that user's data
Save as read-user-data.mjs. The callback uses the verified userDid directly. For a server-side command-line test, copy the vp value from your own test callback into CONSENT_VP in the private .env file, then run node --env-file=.env read-user-data.mjs.
The function checks live consent before reading and again before returning, follows every page, and keeps only records for contractUri. Each record has personal (a string map) and credentials (an array of { category, uri }), not nested category arrays. An omitted optional name is not evidence of missing consent.
getConsentFlowData(contractUri) returns all users' data without an identity field. Do not use it for per-user lookup. No matching records means no accessible data for this contract; deny access rather than guessing which user a record belongs to. Network errors also fail closed.
5. Issue a credential through the contract
Save as issue-through-contract.mjs. Run node --env-file=.env issue-through-contract.mjs once after completing the tutorial.
boostUri is the actual value returned by createBoost, never a fabricated identifier. The network enforces current write permission for its Achievement category, including withdrawal between the read and write calls. Running this command again creates another template and credential; production issuance needs your own idempotency control and an authorized completion event.
6. Handle withdrawal
The learner withdraws in LearnCard; the underlying call is withdrawConsent(termsUri), where termsUri comes from their consent record—not the contract URI. Your issuer cannot withdraw on their behalf.
Save as handle-withdrawal.mjs. Import refreshAccess into server jobs and call it with the same verified userDid and contractUri before using cached data. This example removes the cached entry first, even if a subsequent network call fails.
The shared read function uses getProfile(userDid) to resolve the identifier to a profile, then passes its profileId to verifyConsent. A hidden or unavailable profile denies access here. After withdrawal, verifyConsent returns false and the guarded read returns no records. Do not rely on record presence alone: the current per-user data query can retain withdrawn terms. Stop pending issuance and delete any persistent copies too. In a production cache, run these checks periodically as well as before use; do not keep personal data indefinitely when a learner stops visiting.
What you should see
Setup prints JSON containing a real
contractUriandconsentUrl.Accepting consent returns Consent verified. Active access confirmed.
The read command prints Active consent: 1 record(s). for this single-contract example, even if the optional name was omitted.
Issuance prints Issued: followed by the credential URI. The learner can retrieve it through their contract.
After withdrawal, reading prints No active consent. Issuance stops with No active consent. Refreshing access returns
falseand removes the cached entry.
Troubleshooting
Could not find contract
Use the exact saved CONTRACT_URI and the same network.
Invalid Terms for Contract
Accept all required permissions; terms must match the contract.
You've already consented to this contract!
Use the existing consent record instead of consenting twice.
Could not find profile for did
Confirm the verified holder has a profile on this network.
Target profile has not consented to this contract
Obtain active consent before writing; stop after withdrawal.
Target profile has not consented to receive boosts in the Achievement category
Obtain write permission for Achievement.
Profile does not have permissions to issue boost
Use a template owned by, or issuable by, your service.
Wrong consent contract
The verified presentation belongs to another contract. Start the correct flow.
Unable to confirm consent. Try again.
Check server connectivity and restart consent. Never fall back to the did parameter.
Next steps
See a runnable game example: Gashapon Game Corner
Issue on Consent sends credentials when consent is accepted, using a registered signing authority.
User consent and terms covers permission updates and withdrawal.
Reading & Writing Consented Data explains data access beyond this example.
Last updated
Was this helpful?