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

Send & Issue Credentials

One call sends a credential to an email, phone, or LearnCard profile. Sign it yourself or let LearnCard sign from a template.

~10 min Β· After the Quickstart.

The one-line version

npx @learncard/cli send                    # you sign; writes send.mjs
npx @learncard/cli send --template         # LearnCard signs from a reusable template; writes send-from-template.mjs

Both prompt for the recipient (or take it as an argument: send you@yourdomain.com) and reuse the .env the Quickstart created. Everything below is what those scripts do, and the options send() takes beyond them.

send() in one picture

const result = await learnCard.invoke.send({
    type: 'boost',
    recipient: 'jane@example.com', // email, phone, profile ID, or DID β€” detected automatically
    signedCredential, //             ← you signed it       (or)
    templateUri, //                  ← LearnCard signs from a template you created
    refresh: true, //                ← optional: issue it refreshable (email/phone uses deferred inbox signing)
});

You choose who the recipient is and who signs. Everything else β€” delivery, the claim email, auto-delivery to existing accounts β€” is the same call. Add refresh: true and the recipient can receive in-place updates later; see Issue and Refresh a Managed Credential. Add idempotencyKey to make an SDK-signed or server-signed refreshable send safe to retry (direct REST callers providing signedCredential omit the key and retry the same signed credential and templateUri); see Retrying a refreshable send safely.

Recipient looks like
What happens

jane@example.com

Goes to the Universal Inbox; Jane gets a claim email. If that email is already verified on a LearnCard account, it's delivered straight there.

+15551234567

Same, by SMS. Phone delivery is limited to issuers on the trusted registry.

jane-doe (profile ID)

Delivered directly to that account.

did:key:z6Mk… / did:web:…

Delivered directly to that DID.

You pass
Who signs
Set up

signedCredential

You did, with issueCredential() and your seed

Nothing

templateUri

LearnCard, with your signing authority, from a template you created once

setup-signing, then createBoost (or send --template)

template: { credential, name, category }

Same, creating the template in this call

setup-signing

Template URIs look like lc:network:<host>/trpc:boost:<id>. Always use the value returned by createBoost or send (result.uri) β€” never build one.

You need a network profile before you send β€” createServiceProfile for an organization or bot, createProfile for a person. The Quickstart and learncard init do this for you. Network Profiles.

Examples

Your proof is kept as-is all the way to the recipient. send() also saves the credential as a template (result.uri) so you can send the same badge again with templateUri.

Needs a signing authority β€” one command: npx @learncard/cli setup-signing. Templates can carry {{variables}} filled per send; see Issue at scale.

Include the LearnCard Boost context and BoostCredential type on templates the network signs; it stamps boostId on each issued credential. Why.

Options for email and phone recipients

Email and phone sends stay claimable for 30 days by default. Claims are single-use, and the network can't read the credential after it's claimed. For transcripts, CLRs, or other sensitive learner data, set expiresInDays to the shortest practical window β€” it controls how long the payload can be claimed, not the credential's own validity dates. Details in Universal Inbox.

Guardian approval

The guardian gets an approval email with a 6-digit code; the learner sees a pending notice, then a normal claim button once approved. guardianStatus moves to GUARDIAN_APPROVED or GUARDIAN_REJECTED. If the guardian goes on to create a LearnCard account, they become the learner's manager and every future credential to that learner is gated automatically β€” no guardianEmail needed, approval in-app without a code.

If…
Then

guardianEmail must differ from recipient (self-approval not allowed)

Use a different address for the guardian.

Stuck at AWAITING_GUARDIAN

The guardian hasn't acted. They can reopen the link from their email; the learner can't claim until then.

Learner can't claim after approval

Check guardianStatus β€” GUARDIAN_REJECTED means not claimable. Send again if it was a mistake.

What comes back

refresh is the one part you can't get anywhere else: refreshable credentials are stored encrypted to the recipient, so keep the receipt alongside your own record of the claims if you want to publish updates. It's metadata only β€” no credential content. Full walkthrough: Issue and Refresh a Managed Credential.

inbox.status

Means

claimUrl

PENDING

New recipient. A claim email/SMS went out (unless suppressDelivery); they claim at claimUrl.

yes

ISSUED

That email/phone is already verified on a LearnCard account β€” delivered directly. Nothing to claim.

no

Once someone claims by email, that address is verified on their account, so your next send to it comes back ISSUED.

Over HTTP

send() is also POST https://network.learncard.com/api/send with the same JSON body. Authenticate with Authorization: Bearer <API token> from an auth grant with boosts:write scope β€” npx @learncard/cli token, or Generate API Tokens.

Sign locally, send over HTTP

If you sign credentials yourself but want to deliver them from any language, create a token and POST the signed credential. This script creates the token and writes the request body to request.json:

What you should see

The response is JSON:

inbox.status is PENDING for a new recipient (inbox.claimUrl is where they claim it) or ISSUED if they already use LearnCard (auto-delivered, no claimUrl).

The token has one permission (boosts:write). Store it like a password. Revoke it any time.


Track what happened to a credential

Every send() returns an activityId. The network logs each step against it β€” CREATED when you sent, DELIVERED when it reached an existing account or the claim email went out, CLAIMED when the recipient accepted, and EXPIRED or FAILED if it didn't get there.

Or from the terminal: npx @learncard/cli status <activityId>. No server needed β€” poll this when you want to know if a credential was claimed. Details, filters, and stats: Credential Activity. To be told instead of asking, see Know When a Credential Is Claimed.

Who has a template

For a per-template view β€” everyone who holds a credential issued from it, and whether each one is active, revoked, or suspended:

This is also how you find the credentialUri to revoke.


Issue at scale with templates

Boosts support Mustache-style templating to inject dynamic values at issuance time.

Personalize with {{variables}}

Use {{variableName}} syntax in your credential template:

Provide templateData when sending to fill in the variables:

The resulting credential will have all placeholders replaced.

Issue from a spreadsheet

You can issue credentials in bulk by reading a CSV file.

Note that re-running send for the same recipient and template will re-send the credential (it is not idempotent). To avoid duplicates, use learnCard.invoke.getPaginatedBoostRecipients(boostUri) to reconcile who has already received it before sending.


Next steps

Last updated

Was this helpful?