Verifying a credential
Verification needs no account and no wallet. A verifier enters a credential id, follows a link or scans a QR code, and the browser reads the chain directly through a public RPC endpoint. The page then shows what the chain says about that id, checks the issuer's signature independently, and offers further checks on files the holder hands over.
Who takes part
| Party | Role |
|---|---|
| Verifier | Anyone; no wallet needed |
| Verifier's browser | Reads the registries over RPC, hashes files locally |
CredentialRegistry | The anchor (issuer, hashes, type, dates) and the CredentialSigned event |
StatusRegistry | Status, reason, replacement and validity |
IssuerRegistry | The issuer key in charge (after a key retirement) |
RecordTypeRegistry | The name of an issuer-defined record type |
Where it starts
| Entry | How |
|---|---|
| Home page | Field Credential id or verification link ("No wallet or account needed. Anyone can check a credential."). It accepts a bare id or a pasted /verify?id= link and opens /verify?id=.... A wrong value gives "That is not a credential id. An id is 0x followed by 64 hex characters, or paste the verification link you were sent." |
| Verify page | /verify, header Verification, title Verify a credential, field Credential id ("The id is 0x followed by 64 characters. It is printed under the QR code of every credential.") |
| Link or QR | https://<site>/verify?id=<credential id>, from the issued card, the holder's Show QR, or any copy. The link carries only the id, which is a digest and holds no personal data. |
| Holder or issuer lists | Verify on each row of the student page and the dashboard |
The lookup
lookupCredential (src/app/verify/credential-lookup.ts) first checks the id format, then asks the CredentialLocator (src/services/domain/credential-locator.ts), which tries the active contract set and then the other listed sets. Errors never become a negative verdict:
| State | Message |
|---|---|
invalid-id | "This is not a valid credential id. It must be a 0x-prefixed 32-byte hex value." |
not-found | Credential not found: "No credential with this id exists on this network. Check the id and try again." |
lookup-failed | "Could not reach the network, so the credential was not checked. This is not a verification result. Please try again." |
unconfigured | "Verification service not configured. Please try again later." |
not-deployed | "The v0.3 credential registry is not deployed on this network." |
The credential sheet
A found credential is shown as a sheet: the seal drawn from the id, the id, Academic credential, the record type name (custom types marked issuer-defined), "Issued" with the date and, when set, "valid until". Then a list of checks, the facts Issuer and Document hash, and notes.
The checks
| Line | Source | Possible texts |
|---|---|---|
| Credential found on-chain | The anchor exists | Always shown when found |
| Issuer signature | CredentialService.checkSignature: rebuilds the signed fields from the anchor and the CredentialSigned event (nonce, signature), checks the digest equals the id, then verifies as an EOA signature or through ERC-1271 | "Issuer signature valid", "Issuer signature INVALID", "The issuer signature could not be found for this credential. This is not a verification result.", "The issuer signature could not be checked right now. This is not a verification result." |
| Status | StatusService.getStatus: statusOf and isValid | "Status: Active"; "Status: SUSPENDED", "REVOKED" or "SUPERSEDED" with the reason label and date, plus "(registry administrator hold)" when the admin placed it; "Active, but not currently valid: the issuer is suspended or the credential has expired."; "The credential status could not be checked right now. This is not a verification result." |
| Replacement | When superseded: "The issuer names a replacement. Open it to check it yourself:" View the replacement credential, and a check of that replacement | "The replacement exists, was issued under the same issuer and is in good standing.", "The replacement exists but is not in good standing (...)", "The named replacement was issued under a different issuer. Do not rely on it as the replacement of this credential.", "The named replacement could not be found in any listed contract set.", "The replacement could not be checked right now." |
The stamp on the seal follows the status: Active, Not valid, Suspended, Revoked, Superseded. An unread status earns no stamp; the page never guesses.
The notes
| Note | When |
|---|---|
| "Contract set: 0.3.0" | Always, naming the set that answered |
| "This credential was issued in an older contract set. It is checked in that set and stays there." | The set is not the active one |
| "The issuer key was retired in favour of 0x.... The credential was issued before the retirement and is judged by the key in charge now." | currentIssuerOf returns another key (sets with key lineage, 0.5.0) |
Issuer tools on the same page
Below the sheet, Manage as issuer ("Issued this credential? Connect the issuing wallet to change its status.") connects a wallet and compares it with the issuer key in charge. The tools are described in Lifecycle.
Further checks
Section Further checks, "Check what the holder gave you". Every file is read in the browser and never uploaded.
| Panel | Input | What it checks | Results |
|---|---|---|---|
| Check a document | Document file | SHA-256 of the file against the anchored document hash (checkDocument) | "Document matches the issued credential" or "Document does NOT match the issued credential" |
| Check holder details | Credential package file | The package belongs to this id, its fields hash to the id and equal the anchor, and the name and student ID prove the on-chain commitments (checkPackage) | "Holder details match the on-chain commitments" with the name, Student ID, Programme, award and Award date; or one of the mismatch messages listed in Credential package |
| Check a verifiable credential file | A VC or Open Badge JSON | See Verifiable credentials | |
| Check a disclosure | A disclosure file and a fresh challenge | See Selective disclosure |
The verifier receives the document and the package from the holder by any channel. The page proves that what they were given is what the issuer signed and anchored.
The offline script
blockchain/scripts/verify-credential.ts checks a package, a VC file or an Open Badge file from the command line:
cd blockchain
npx ts-node scripts/verify-credential.ts credential-0a1b2c3d.json
npx ts-node scripts/verify-credential.ts credential-0a1b2c3d.json --rpc <url> --registry <CredentialRegistry address>
Without --rpc it checks only that the fields hash to the id and that the signature recovers to the issuer address in the file, and says so: "The issuer and registry are NOT verified; pass --rpc and --registry." With both flags it refuses a package signed for another registry or chain, then reports Anchor matches, Issuer registered (using the key in charge where the set has key lineage) and Signature (EOA or ERC-1271). The script does not read the credential's status; use the verify page or the API for that.
Variants
- v0.2 (legacy). With
NEXT_PUBLIC_CONTRACT_VERSIONnotv03,/verifyshows the v0.2 page: a numeric record id, an optional name to compare, and states found, issuer unregistered, not found, invalid, lookup failed and unavailable. The home field then asks for a record number. - Several contract sets. The locator finds a credential in any listed set. A superseded credential may name a replacement in another set; the replacement check looks in every listed set and requires the same issuer key in charge.
- The verification API. The same answers, signed by the server, are available at
/api/v1/credentials/{id}when the API key is configured. See REST API.
What the page does not check
- Issuer registration. The sheet says so: "This page shows that the credential exists on-chain and compares the document hash. It does not yet check whether the issuer is still registered." It shows the issuer address, not the registered name.
- Who presents the credential. Anyone can show a credential id or a document. Holder binding is proved by the package's salts or by a disclosure file signed by the holder.
- Status lag. Reads come from a public RPC endpoint and can lag a fresh transaction by a block or two.
- Off-chain claims. The name, programme and award are proved only when the holder hands over the package or a disclosure.
Code map
| Concern | File |
|---|---|
| Page and states | src/app/verify/page.tsx, CredentialVerifyContent.tsx, credential-lookup.ts, LookupProblem.tsx |
| Sheet and checks | CredentialSheet.tsx, IssuerSignatureStatus.tsx, CredentialStatusBanner.tsx, status-stamp.ts, use-credential-status.ts |
| Notes | SetNote.tsx, IssuerRetiredNote.tsx, ResolvedRecordType.tsx |
| Replacement check | replacement-check.ts, use-replacement-check.ts |
| Further checks | FurtherChecks.tsx, DocumentCheckPanel.tsx, HolderDetailsPanel.tsx, VcCheckPanel.tsx, DisclosurePanel.tsx |
| Services | src/services/domain/credential-locator.ts, credential-service.ts (checkSignature, checkDocument, checkPackage), status-service.ts |
| Home lookup and links | src/components/home/HomeVerifyForm.tsx, lookup-input.ts, src/lib/verification-link.ts |
| Script | blockchain/scripts/verify-credential.ts, blockchain/scripts/lib/verify-credential.ts |
| v0.2 page | src/app/verify/VerifyPageContent.tsx, lookup.ts |
Tests
src/app/verify/CredentialVerifyContent.test.tsx,IssuerSignatureStatus.test.tsx,CredentialStatusBanner.test.tsx,DocumentCheckPanel.test.tsx,HolderDetailsPanel.test.tsx,status-stamp.test.ts,replacement-check.test.ts,use-replacement-check.test.tsx,use-credential-status.test.tsx,ResolvedRecordType.test.tsx,VerifyPageContent.test.tsx,lookup.test.tssrc/services/domain/credential-locator.test.ts,credential-service.test.ts,status-service.test.ts,src/lib/verification-link.test.tsblockchain/test/v03/VerifyCredential.test.ts- E2E:
e2e/journeys/issue-and-verify.journey.ts,revoke-and-reverify.journey.ts,contract-sets.journey.ts,key-retirement.journey.ts