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
- Define a verifiable credential — its name, its claims, and how it's signed. This page covers this step.
- Issue one to a specific user, who redeems it into their wallet — see Issuing Verifiable Credentials.
- 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.
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.
| Format | Status |
|---|---|
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/listshow 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.
| Flag | Required | Description |
|---|---|---|
--identifier (alias --vct) | Yes | The 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. |
--name | Yes | Human-readable name. |
--format | Yes | dc+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-alg | Yes | RS256 or ES256. |
--claim | No, repeatable | Three 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. |
--enabled | No | true 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.