Verify Credentials
Sign a credential locally, verify it, and understand tampered, expired, and revoked results.
Verification checks the signature, detects changes to signed data, checks expiry, and checks revocation or suspension when the credential carries a supported credentialStatus. You will generate a real signed Open Badges 3.0 credential locally, then try two failing cases. No account or API token is needed for these three examples.
The one-line version
npx @learncard/cli verify credential.jsonPrints one line per check (✓ proof, ✓ expiration, ✗ status: Status: Revoked) and exits non-zero if any fail. Pipe JSON on stdin with -; --json for the raw result. No account or seed needed.
Install
In a new project folder, install the SDK:
npm init -y
npm install @learncard/initVerify your first credential
Save this as issue-and-verify.mjs. It generates a disposable issuer key, signs a badge awarded to that same identity, and verifies it with a separate, seedless SDK instance. Nothing is sent or stored on the LearnCard Network.
import { randomBytes, randomUUID } from 'node:crypto';
import { initLearnCard } from '@learncard/init';
// A fresh, disposable issuer for this tutorial; never print its seed.
const issuer = await initLearnCard({ seed: randomBytes(32).toString('hex') });
const credential = await issuer.invoke.issueCredential({
'@context': [
'https://www.w3.org/ns/credentials/v2',
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
],
type: ['VerifiableCredential', 'OpenBadgeCredential'],
issuer: issuer.id.did(),
validFrom: new Date().toISOString(),
name: 'Verification Complete',
credentialSubject: {
id: issuer.id.did(),
type: ['AchievementSubject'],
achievement: {
id: `urn:uuid:${randomUUID()}`,
type: ['Achievement'],
name: 'Verification Complete',
description: 'Verified a signed credential with LearnCard.',
criteria: { narrative: 'Ran the verification tutorial.' },
},
},
});
// Verification needs neither the issuer's seed nor a network profile.
const verifier = await initLearnCard();
const result = await verifier.invoke.verifyCredential(credential);
console.log(
result.errors.length
? `Invalid: ${result.errors.join('; ')}`
: `Valid: ${result.checks.join(', ')}`
);
console.log(JSON.stringify(result, null, 2));Run it:
What you should see
Captured output:
checks lists successful checks; it is not an overall verdict. Reject verification failures in errors, and review warnings before accepting a credential. This badge has no expiry and no status entry: expiration succeeds, but there is no revocation check to report. If present, credentialSchema also adds a schema check.
The default verifyCredential(credential, options?) result is an object with checks, warnings, errors, and optional status. For display, verifyCredential(credential, {}, true) returns a prettified array instead: success entries use message, failures use details, and warnings have status Error. Use the default object for application decisions.
Tampered
Save this independent example as tampered.mjs. It changes an achievement name after signing, retaining the original proof.
What you should see
Captured output:
Passing the expiration check cannot rescue a broken signature. Do not edit a signed credential; ask its issuer for a replacement.
Expired
Save this as expired.mjs. The issuer signs validUntil in the past, so this tests expiry rather than tampering.
What you should see
Captured output:
The signature still passes. For these VC 2.0 credentials, use validFrom / validUntil; VC 1.1 credentials use issuanceDate / expirationDate.
Revoked (status list)
This step requires a network-issued credential with a credentialStatus entry, not one of the local badges above. A status list lets the issuer revoke a credential without changing its signed contents.
Issue and deliver a VC 2.0 badge through the network's Boost issuance flow, which allocates a
BitstringStatusListEntrybefore signing. Keep the delivered credential, template URI, and recipient profile ID. A pre-signed credential without a status entry does not gain one just by being sent.Follow Revoke or Update a Credential: the issuer calls
revokeBoostRecipient(templateUri, recipientProfileId)(or passes a specific credential URI as the third argument). This flips the associated revocation bit.Verify the same delivered credential again with default options. Verification fetches the status list referenced by
credentialStatus; do not overridechecksto request onlyproof.
The following is a shape excerpt from VerificationCheck.status, not captured output. The local examples do not create or revoke a network credential:
Entries can also include statusListCredential and statusListIndex. A set revocation bit means revoked; a set suspension bit means suspended. Check errors and the structured status entries, not a guessed error string. Missing status data is not evidence that a credential is unrevoked, and an unreachable status list is not a successful check. See Credential Status and Bitstring Status Lists.
Who verifies what
Signature:
verifyCredentialchecks the proof and signed data; review warnings about whether the named issuer authorized the signing key.Issuer trust: your application decides which issuers to accept, using a separate Trust Registry lookup or allowlist.
verifyCredentialdoes not automatically check trusted issuers.Presenter binding: request a signed Verifiable Presentation, use
verifyPresentationwith your fresh challenge and expected domain, verify the enclosed credentials, and checkholder === credentialSubject.id. This establishes control of that subject identity, not a person's real-world identity; receiving credential JSON alone does not.
Request a credential from a LearnCard user
Use Create a ConsentFlow for the full, runnable flow. The requester calls createContract with read access to the required credential categories (for example, Achievement), then sends the user to https://learncard.app/consent-flow with URL-encoded uri (the returned contract URI) and returnTo parameters.
After consent, the requester calls getConsentFlowDataForDid(userDid) and filters the paginated results by contractUri. Retrieve and verify the shared credentials before using their claims. Consent grants access; it does not establish signature validity, issuer trust, or presenter binding.
For other wallets and protocols, see Interoperate with LearnCard.
Troubleshooting
signature error: Verification equation was not satisfied
Reject the credential: signed data or its proof changed. Request the original or a newly issued copy.
expiration error: Credential is no longer valid
The VC 2.0 validUntil is past. Request a current credential.
expiration error: Credential is expired
The VC 1.1 expirationDate is past. Request a current credential.
expiration error: Credential is not valid yet
Check validFrom and your system clock. Wait until the validity period begins.
Issuer authorization was not checked because the credential issuer is not a DID
A valid signature did not establish authorization by the named URL issuer. Resolve that authorization separately; do not silently accept the warning.
The first two errors above were captured from these examples; the remaining messages are defined by the SDK. Resolver or status-list failures may vary by environment. Treat them as unresolved verification, not permission to skip a check.
Next steps
Create a ConsentFlow to request credentials with consent.
Revoke or Update a Credential to try the network status-list flow.
Trust Registries to define which issuers your service accepts.
Last updated
Was this helpful?