Skip to main content
Version: Next

Verifiable Credentials

A verifiable credential (VC) is a digital credential a user holds in their own wallet app and can present later, to O2ID or to an entirely different party, as cryptographic proof of a claim about themselves. The party checking it doesn't need to call O2ID each time to confirm it's still valid — it's signed, so it verifies offline against O2ID's public key. That key only needs to be fetched once (from O2ID's JWKS endpoint) and can be cached; verifying any number of credentials afterward needs no further contact with O2ID at all.

This is different from the access tokens and ID tokens O2ID already issues. A token proves "this user is signed in, right now, to this application." A verifiable credential outlives the session: it can be presented weeks later, to a different system entirely, with no further involvement from O2ID.

O2ID issues credentials in SD-JWT VC and JWT-VC format — both signed JSON Web Tokens stored in a compatible wallet app.

Common use cases

  • Employee badges — a digital ID issued when someone joins, checkable by a building's access system or a partner's portal without either calling back to O2ID.
  • Age or identity verification — after a user proves their age once (through a flow), an AgeOver18-style credential lets them reuse that proof anywhere age verification is required, instead of repeating the check each time.
  • Certifications and completions — a "completed onboarding" or "certified operator" credential a user can show to a manager or a regulator.
  • Membership or tier status — a loyalty or subscription tier a partner site can check for a discount, with no API exposed for them to query it directly.

How it works

  1. Define a verifiable credential — its name, its claims, and how it's signed. This page covers this step.
  2. Issue one to a specific user, who redeems it into their wallet — see Issuing Verifiable Credentials.
  3. The user presents it to whoever asks, whenever they choose, with no further involvement from O2ID.

A verifiable credential defined here is a reusable template — issuing one doesn't consume it. Define EmployeeBadge once, then issue it to any number of employees.

note

Managing verifiable credentials requires a role with the verifiablecredentials:read/verifiablecredentials:create/ verifiablecredentials:update/verifiablecredentials:delete scopes — see Managing Roles for how to create and assign roles.

Formats

Pick dc+sd-jwt unless a specific downstream verifier requires the plain W3C encoding instead — selective disclosure is a meaningful privacy benefit for most use cases (an employee badge that discloses department only when actually asked to), and costs nothing when unused.

FormatStatus
dc+sd-jwt (SD-JWT VC)Supported end to end. Supports selective disclosure — the holder can hide individual claims when presenting.
jwt_vc_json (W3C VC Data Model, JWT-encoded)Supported end to end. No selective disclosure — every claim is always visible.
mso_mdoc (ISO 18013-5)Supported. ES256 signing only — RS256 is rejected with 400 (COSE_Sign1 with RSA is spec-legal but unsupported by real mdoc wallets). No selective disclosure via this API; each issued mdoc carries every claim as its own digested IssuerSignedItem. Requires a holder public key at issuance (see Issuing Verifiable Credentials) — mdoc has no bearer-only mode.

Claims

Each claim maps to a key in the eventual credential body:

  • id — server-assigned when the claim is created; get/list show it, and --remove-claim/--update-claim (see Updating) target a claim by this ID.
  • name — must be unique within the verifiable credential.
  • displayName — purely descriptive.
  • selectivelyDisclosable — SD-JWT VC only: whether this claim gets its own selective-disclosure digest rather than always being visible. Ignored by other formats.

--claim on create sets the initial list all at once. On update, claims are edited individually instead — there's no flag that replaces the whole list after creation.

Creating a verifiable credential

o2idctl verifiable-credentials create \
--identifier EmployeeBadge \
--name "Employee Badge" \
--format dc+sd-jwt \
--signing-alg RS256 \
--claim employeeId:"Employee ID" \
--claim department::true

This defines a credential with two claims — employeeId (always visible to whoever the badge is shown to) and department (marked selectively disclosable, so the holder can choose to hide it when presenting the badge). See Issuing Verifiable Credentials for issuing it to a user.

FlagRequiredDescription
--identifier (alias --vct)YesThe credential identifier — becomes SD-JWT VC's vct claim, mdoc's doctype, or a JWT-VC's type array entry on the wire. Must be unique among this tenant's other verifiable credentials.
--nameYesHuman-readable name.
--formatYesdc+sd-jwt (SD-JWT VC), jwt_vc_json (W3C VC Data Model, JWT-encoded), or mso_mdoc (ISO 18013-5 — requires --signing-alg ES256, see Formats).
--signing-algYesRS256 or ES256.
--claimNo, repeatableThree colon-separated fields, always in this order: name[:displayName[:selectivelyDisclosable]]. Leave a field empty to skip it while still setting a later one — e.g. department::true sets name=department, no displayName, and selectivelyDisclosable=true.
--enabledNotrue or false. Defaults to true.

Getting and listing verifiable credentials

o2idctl verifiable-credentials get <id>

Returns a single verifiable credential by ID.

o2idctl verifiable-credentials list

Returns every verifiable credential defined for the current tenant.

Updating a verifiable credential

o2idctl verifiable-credentials update <id> --name "Employee Badge v2" --enabled false

Changes only the fields you pass — every flag is left untouched if you omit it entirely. Setting --enabled false stops new offers from being created against a credential without deleting it, which matters because it also drops that credential out of the issuer metadata document wallets use for discovery.

Claims are edited one at a time, not by resending the whole list (see Claims for how to find a claim's ID):

o2idctl verifiable-credentials update <id> --add-claim location:"Office Location"
o2idctl verifiable-credentials update <id> --remove-claim <claim-id>
o2idctl verifiable-credentials update <id> --update-claim <claim-id>:employeeId:"Emp. ID":true

All three are repeatable and can be combined in one call; existing claims you don't target are left exactly as they are. They apply in the order given, and the whole set is validated together — if any of them fails (an unknown claim ID, a name collision, ...), nothing in the call is applied.

Deleting a verifiable credential

o2idctl verifiable-credentials delete <id>

This is permanent and cannot be undone. It only removes the definition — it has no effect on credentials already issued and sitting in users' wallets. Those are self-contained, signed documents that don't check back with O2ID (see the top of this page), so there's currently no way to invalidate one after the fact; see Known limitations for the lack of a revocation mechanism.

API Reference

See the API Reference for the full request/response schemas.