SDK docs · sdk/08

Credentials and holders

How a credential comes to exist, who holds it and when it expires: issuance over OpenID4VCI, the headless holder library instead of a wallet, witness refresh, and where identity evidence lives.

The lifecycle, in four movements

A presentation is only as good as the credential behind it. This page is the other side of that exchange: how a credential comes to exist, who holds it, and what the holder component does and refuses to do. Four properties carry the rest of it:

  1. The grade is the issuer’s, not the holder’s. The offer carries the grade and date of birth the approved identity check produced; a holder cannot ask for a grade.
  2. The issuer never sees the credential in the clear. The holder sends a blinded request bound to a one-shot nonce from the token endpoint; the issuer blind-signs, the holder unblinds locally, and the link secret stays inside the holder library.
  3. A credential is not usable the moment it is signed. It becomes usable when a finalized status root commits the accumulator it lives in; until then the witness endpoint says so, and an honest holder does not present.
  4. Holders do not accumulate on chain. The chain carries the issuer’s periodic status root — never a credential id, serial, presentation or holder identifier.

The issuance surface has no published contract

There is no OpenAPI document for issuance. The operator-facing side of the issuer service has one; the holder-facing endpoints are specified by their implementation and by this page, and by nothing else. So a third-party wallet has nothing stable to build against today, and the intended integration is the library below, not a wallet. The shape follows OpenID4VCI with a pre-authorized code grant; the credential format and proof type are MintID’s own.

EndpointCalled byWhat it does
GET /.well-known/openid-credential-issueranyoneissuer metadata: the credential configuration, the issuer’s current definition, and where its accumulator is anchored
POST /v1/credential-offersthe issuer’s operatorturns one approved verification session into an offer with a pre-authorized code
POST /v1/tokenthe holderredeems that code once for an access token and a nonce
POST /v1/credentialthe holderfulfils a blinded issuance request bound to that nonce
GET /v1/credentials/{id}/witnessthe holderthe usability gate plus fresh witness material

Operator routes are authenticated; holder routes are reached with the one-shot code and token, and one token yields at most one credential.

There is no wallet, on purpose

The holder is a headless library you embed, running in your own infrastructure next to the keys you already custody. MintID ships no consumer wallet, browser key store, hosted key or mobile app, and has none planned: a wallet front-end is outside the specification, and a dashboard that could read your key would be custody in fact. Nothing here prevents a credential from one day being presented from a wallet somebody else builds — the transports are the standard ones — but that is not a surface MintID offers you today. Two bindings exist — Python and Go (the Rust library as a child process over JSON lines: no cgo, no hand-written FFI); no TypeScript binding yet.

OperationWhat it doesWhat it guarantees
issuethe holder half of issuance: metadata, token, blinded request, unblindingthe issuer learns a commitment, never the link secret
refresh_witnessadopts the issuer’s current definition, this credential’s membership witness, and the finalized root anchoring themself-checked first: material that does not verify is refused and the holder keeps its previous state
presentanswers a verifier’s challenge with one bound, 10-second envelopethe binding is derived by the library from the challenge itself; if the credential cannot satisfy the policy the call fails and nothing is sent
self_revokenot implemented. It refuses and changes nothing; holder-initiated revocation is a scheduled, audit-gated milestone

There is no export. The one serialization is a custody encoding holding the link secret: encrypt it under your own key, keep it where your payment key lives. The holder’s view of itself reports the issuer, grade, validity instants, credential id and current root — never the secret, never the date of birth. A lost key is recoverable by nobody: you re-verify and get a fresh credential, unlinkable to the old one.

When a credential expires

A credential expires exactly when the verification behind it does: 12 months after approval for grades 1 and 2, 6 months for grade 3, 3 months for grade 4 — however late in that period the credential was issued. There is no grace period and no extension. Once the verification has expired, every issuance step is refused, including an offer made just before it. The expiry is inside the credential and proven at every presentation; past it, the credential no longer satisfies an unexpired policy, and the way back is a new verification.

Witness refreshes, and what a stale one costs

Non-revocation is proven against the accumulator the issuer’s latest finalized status root commits, and roots move on a short heartbeat.

  • Cadence: size it against how often roots are published, not how long one stays accepted. In practice: refresh right before presenting.
  • Stale fails closed. A witness that no longer matches the anchored accumulator cannot build a valid presentation; one built against a superseded root is rejected.
  • The refusal names the failed checkstatus_root_stale, not a generic failure. Refresh once and retry, never loop (reason codes).
  • Revocation lands at the next root. Until it finalizes, the previously anchored state still verifies: cadence, not discretion.

Where identity evidence lives

With the verification provider, and nowhere else. The boundary is narrow and provider-neutral: a driver opens a case, reports its state, and returns an authenticated verdict enumerating the checks it performed. The issuer keeps that verdict, an opaque handle to evidence in its own vault, a grade, timestamps and a status; raw evidence reaches neither MintID nor the chain. The provider does not set the grade: a driver that returned one is rejected, because grading is the issuer’s decision over enumerated checks, bounded by what its admission permits.

Assurance grades

GradeWhat the issuer asserts
A1basic binding of the subject to the credential
A2document verification plus a liveness / face-match step
A3A2 plus enhanced due diligence with periodic review
A4highest assurance, under stricter controls

Read these honestly. A grade measures the strength of the check, not its recency; freshness is a separate dimension, carried beside the grade and never folded into it. Note what follows from that: a presentation today proves a grade threshold, and there is no freshness predicate — if recency matters to your decision, it is not something a proof will tell you. An issuer may use only the grades its admission permits, and a presentation proves a threshold — “at least A2” — never the exact grade or the attributes. MintID claims conformance to no external assurance framework: these four are its own vocabulary, and the reasoning behind them is in the assurance vocabulary record.

Current status

The proof core that issues and presents these credentials is built and in production. Holder-initiated revocation is the part not yet implemented, as the operations table says. The verifier side of the exchange starts at the integration overview; what you owe before any code is in getting started, and roots and registry state are in substrate reads.