Skip to main content

Credential lifecycle

Nothing in v0.3 is deleted. A credential keeps its anchor forever and its status changes over time in the StatusRegistry. Every change records a reason code, the time, the account that made it and the hash of an optional note, and emits an event, so the history is public and permanent.

States and transitions​

StatusValueMeaningFinal
None0No credential with this id
Active1Issued and standingNo
Suspended2Temporarily not validNo
Revoked3Cancelled for goodYes
Superseded4Replaced by another credential, named in supersededByYes

Valid is more than Active. isValid(id) is true only when the status is Active, the issuer (resolved to the key in charge) is not suspended issuer-wide, and the credential has not expired (validUntil is 0 or block.timestamp <= validUntil). The verify page shows "Active, but not currently valid: the issuer is suspended or the credential has expired." for an Active credential that is not valid.

Who may do what​

The "issuer" below is the key in charge of the credential's issuer: IssuerRegistry.currentIssuerOf(issuer), which follows key retirements on sets that have them (0.5.0). A retired key has no authority over its old credentials; its successor does. On sets without key lineage the issuer is the signing key.

FunctionCallerAllowed fromEffectReason code
suspend(id, reason, noteHash)Issuer, or the registry adminActiveSuspended. adminHold is set when the admin (and not the issuer) suspends.Issuer: 0 to 7 or 1000 and up. Admin: any.
reinstate(id, noteHash)Issuer; the admin when adminHold is setSuspendedActive, hold cleared, reason 0None
revoke(id, reason, noteHash)Issuer onlyActive or Suspended (even under an admin hold)Revoked, hold cleared0 to 7 or 1000 and up
supersede(id, replacementId, noteHash)Issuer onlyActive or Suspended without an admin holdSuperseded, supersededBy set0
supersedeFor(issuer, id, replacementId, noteHash)The CredentialRegistry only, inside issueReplacingAs supersedeAs supersede0
supersedeToRegistry(id, replacementId, successorRegistry, noteHash)Issuer onlyAs supersedeSuperseded, pointing to a credential in another set0
setIssuerSuspended(issuer, true, reason)A guardian (the admin counts as one)AnyAll of that issuer's credentials stop being validAny
setIssuerSuspended(issuer, false, reason)The admin onlyAnyLifts the issuer-wide suspensionAny

Any other transition reverts with InvalidTransition(from, to). A caller without the right gets NotAuthorised; an unknown id gets UnknownCredential.

Status changes are not stopped by the system pause. The pause stops creating (issuing, redeeming invitations, registering record types); an issuer can still revoke during an incident.

Supersede​

supersede (in the same set) requires a replacement that is a different credential, currently Active, and issued under the same key in charge. Otherwise it reverts with ReplacementInvalid ("The replacement must be a different, active credential issued by the same account."). The CredentialSuperseded(id, replacementId, successorRegistry) event names the registry where the replacement lives: this set's own registry for supersede, or another set's for supersedeToRegistry.

supersedeToRegistry cannot check a credential in another contract, so it only requires a non-zero replacement id different from the old one and a successor registry that is not zero and not this set's own. The verify page then checks the named replacement itself (it must exist in a listed set, have the same key in charge and be in good standing). supersedeFor, supersedeToRegistry and issueReplacing exist on sets 0.4.0 and later.

An admin hold blocks supersede: incident response may end in revocation, but the issuer cannot quietly point a held credential at a replacement.

Reasons and notes​

CodeLabel
0Unspecified
1Issued in error
2Data correction
3Fraud or misrepresentation
4Academic misconduct
5Administrative hold
6Issuer key compromise
7Holder request
8 to 999Reserved; the contract refuses them from issuers (InvalidReason) and the app shows "Reserved (n)"
1000 and upIssuer-defined; shown as "Issuer-defined (n)"

A note is free text typed in the UI. Only keccak256 of the trimmed text is sent (noteHash), or the zero hash when there is no note. The text itself is never stored; the issuer keeps it if it matters.

From the dashboard​

The issuer dashboard (/dashboard) lists every credential the issuer key and its retired predecessors issued, in every listed contract set, read from CredentialIssued events with the status of each (in batches of 10). Each row has Verify, Open document, Replace key, Manage access where the access gate applies, Re-issue when supersede is possible, and Revoke.

The dialog Revoke this credential? says "Anyone who checks it will see it as revoked. This cannot be undone." and offers Reason (codes 0 to 7) and an optional Note ("Only a hash of the note is stored on-chain."), confirmed with Revoke credential. The call goes to the status registry of the set that holds the credential.

Re-issue opens the issue form with ?replaces=<id>; see Issuing.

From the verify page​

On /verify?id=..., Manage as issuer connects a wallet and compares it with the key in charge. A different wallet sees "The connected wallet is not the issuer of this credential." The issuer then sees Issuer tools:

StatusButtons
ActiveSuspend, Revoke, Supersede
SuspendedReinstate, Revoke, Supersede
Suspended with an admin holdRevoke only, with "A registry administrator placed this hold, so only the administrator can lift it, and it cannot be superseded."
Revoked or Superseded"This status is final; no further changes are possible."

Each button arms the action and shows Reason, Note (only its hash is stored on-chain) and, for supersede, Replacement credential id; Confirm suspend (and so on) sends the transaction, Cancel disarms. A badly formed replacement id gives "The replacement must be a 0x-prefixed 32-byte credential id." After the transaction the status is read again.

Admin and guardian actions​

There is no screen in the app for an admin hold on one credential or for issuer-wide suspension. The admin (a Safe on Sepolia, see Governance) or a guardian calls suspend or setIssuerSuspended on the contract directly. The app reflects the result: the verify page marks an admin hold, the status line explains an issuer-wide suspension, and the dashboard and holder lists show the badge.

Expiry​

The contract supports validUntil, and isValid turns false after it. The issue form always writes 0 (no expiry), so credentials issued through the app do not expire. A credential issued with an expiry by other tooling would show as Active but not valid after the date.

Errors​

RevertMessage
NotAuthorised"Only the issuer of this credential can do that. Switch to the issuing account."
InvalidTransition"This credential can no longer be changed this way. Its status may already be final, or held by an administrator."
ReplacementInvalid"The replacement must be a different, active credential issued by the same account."
UnknownCredential"This credential was not found."
InvalidReason"That reason code is not allowed."

What changes and what does not​

ChangesNever changes
status, reason, updatedAt, updatedBy, adminHold, supersededByThe anchor, the signature event, the credential id
Events: StatusChanged(id, from, to, reason, by, noteHash), CredentialSuperseded, IssuerSuspensionChangedEarlier events (the full history stays readable)

Packages, VCs and disclosure files carry no status. Every verifier reads it from the chain at the time of checking, and status lists, when configured, mirror it for standard VC tooling.

Code map​

ConcernFile
Contractblockchain/contracts/v03/StatusRegistry.sol, libraries/CredentialTypes.sol
Servicesrc/services/domain/status-service.ts, note-hash.ts
Reasons and actionssrc/lib/status-reasons.ts, src/lib/status-actions.ts, src/lib/status-errors.ts
Dashboardsrc/app/dashboard/IssuerDashboardPage.tsx, IssuedCredentialsTable.tsx, RevokeDialog.tsx, src/services/domain/issued-credentials-service.ts
Verify pagesrc/app/verify/ManageCredential.tsx, CredentialStatusBanner.tsx
Badgessrc/components/credential-status.ts

Tests​

  • blockchain/test/v03/StatusRegistry.test.ts, ExpiryBoundary.test.ts, Revisions.test.ts, KeyLineage.test.ts, PauseClassification.test.ts, and the invariant suite under blockchain/test/invariants
  • src/services/domain/status-service.test.ts, src/lib/status-actions.test.ts, src/lib/status-reasons.test.ts, src/lib/status-errors.test.ts
  • src/app/verify/ManageCredential.test.tsx, src/app/dashboard/RevokeDialog.test.tsx
  • E2E: e2e/journeys/revoke-and-reverify.journey.ts, dashboard-actions.journey.ts, key-retirement.journey.ts, pause-and-restart.journey.ts