Credentials and holders
How a credential is issued over OpenID4VCI, held in a headless holder library, kept current by witness refresh, and retired when its check expires.
The lifecycle, in four movements
A presentation is only as good as the credential behind it. A credential starts from an approved identity check, is blind-signed by the issuer, lives in a holder library you embed, and becomes usable once a finalized status root commits it. Four properties carry the rest:
- 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.
- 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.
- 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 a conforming holder does not present.
- Holders do not accumulate on chain. The chain carries the issuer’s periodic status root — never a credential id, serial, presentation or holder identifier.
Issuance follows OpenID4VCI; the holder library is the stable surface
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 wallet somebody else builds has no stable issuance contract to build against today, and the intended integration is the holder library below. The shape follows OpenID4VCI with a pre-authorized code grant; the credential format and proof type are MintID’s own. The issuer’s routes themselves are part of its integration guide, which is not published here: write to info@mintid.net and say what you are building.
| Step | Called by | What it does |
|---|---|---|
| Issuer metadata | anyone | the credential configuration, the issuer’s current definition, and where its accumulator is anchored |
| Credential offer | the issuer’s operator | turns one approved verification session into an offer with a pre-authorized code |
| Token | the holder | redeems that code once for an access token and a nonce |
| Credential | the holder | fulfils a blinded issuance request bound to that nonce |
| Witness | the holder | the usability gate plus fresh witness material |
Who may call what
Holder routes are reached with the one-shot code and token, and one token yields at most one credential. They carry their own authentication, so every deployment opens them without an operator credential — the same six on the single-node production chain, on the public testnet and on the local stack:
| Route | Open to | Its own authentication |
|---|---|---|
| the issuer's OpenID4VCI metadata | anyone | none (metadata) |
| the token step | the holder | the pre-authorised code, plus the mint presentation for an agent credential; redeemed once |
| the credential step | the holder | the access token of the token step, bound to its nonce |
| a credential's witness read | the holder | the credential id is a 256-bit random handle |
| the delegated-scope directory: its anchors and its delta log (two routes) | anyone | none (public on any issuer) |
A holder that runs in a browser reaches these six from its own origin when the deployment allows that origin: the issuer answers the CORS preflight for a configured list of exact origins (no wildcard), never in credentials mode, and on no other route. Cross-origin browser access is per deployment. A deployment may rate-limit the token and credential steps per client address at its edge; a holder that receives 429 rate_limited waits (Retry-After) and retries.
Every other issuer route — offers, revocation, renewal, roots, the catalogue — and every list is an operator route. Operator routes are authenticated by the deployment in front of the issuer, not by the route itself, so who may call them depends on where the issuer runs:
| Where | The operator routes |
|---|---|
| The single-node production chain, and any deployment of your own behind an authenticating edge | the issuer's operator only, with its credential |
| The public testnet | the offer, agent and revocation routes of the sandbox journey are open to anyone; the rest stays operator only. Their paths and request bodies are in the issuer's integration guide, given on request |
| The local stack | open on your machine: nothing stands in front of the issuer, and you are its operator |
Run an issuer anywhere else only behind an edge that authenticates its operator routes.
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. The library is Rust, with three bindings — Python, Go and TypeScript; the Python and Go bindings drive it as a child process over JSON lines (no cgo, no hand-written FFI).
| Operation | What it does | What it guarantees |
|---|---|---|
issue | the holder half of issuance: metadata, token, blinded request, unblinding | the issuer learns a commitment, never the link secret |
refresh_witness | adopts the issuer’s current definition, this credential’s membership witness, and the finalized root anchoring them | self-checked first: material that does not verify is refused and the holder keeps its previous state |
present | answers a verifier’s challenge with one bound, 10-second envelope | the binding is derived by the library from the challenge itself; if the credential cannot satisfy the policy the call fails and nothing is sent. An agent credential also takes the issuer’s dispute key document and your own chain view of the MintID Foundation’s admission keys: every agent presentation carries an encryption of the credential’s status reference that only the issuer and the Foundation together can open, for a claimed dispute; the holder checks the key before it proves anything and presents nothing without it |
self_revoke | revokes the holder’s own credential through a privacy relay that submits the revocation and pays for it, so the holder needs no account | in production on the single-node production chain since 20 September 2026: neither who the holder is nor which credential it was is revealed; the issuer must remove it within a 60-second grace the chain enforces, and every service refuses it within about four minutes of the chain recording the revocation (245 seconds on the single-node production chain); it is not instant: until the next status commitment carries the revocation, a presentation can still be accepted; its independent audit is a scheduled milestone whose findings are published |
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.
The holder in a browser. The same library also compiles to WebAssembly, behind a TypeScript surface: the package @mintid-net/holder (npm install @mintid-net/holder, on npm from the first public release whose signed manifest lists it; source sdk/holder/, read-only mirror https://gitlab.com/mintid/sdk-holder; its README). The package and the mirror carry the WebAssembly module the release pipeline built, checked against identity-proof-core/holder-wasm/REFERENCE.md5 (verify a release). It is the holder, not a wallet: it has no storage and makes no network call. The page that embeds it passes its own HTTP transport and the source of verifiers' request keys, and keeps the custody record — the same bytes the Python binding writes, loadable by every binding — encrypted under a key of its own. On top of the signature, it checks the challenge's exact origin, profile, deadline and that it answers each challenge once. An agent record presents there too, with the same two dispute inputs as every other binding (disputeKey, admissionKeys; PresentAgent in Go). Proving is slower than native: on a high-end laptop an identity presentation takes about 0.6 seconds in the browser, and about 2.5 seconds with the CPU slowed four times (MintID's proxy for a mid-range laptop). estimateProveMs() measures it on the device before the page asks for an approval; the README carries the measured figures, the measurement method and how to design around the 10-second window.
When a credential expires
A credential expires exactly when the verification behind it does: 12 months after approval for grades A1 and A2, 6 months for grade A3, 3 months for grade A4 (grades are encoded 1–4 on the wire) — 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. A credential and its agents expire exactly when the identity check’s validity ends (12, 6 or 3 months by grade); a subscription at an operator neither extends nor shortens it; an operator may revoke agents it operates for non-payment through its own operator route.
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. An identity credential’s proof is accepted against the issuer’s latest finalised root or against an entry of the issuer’s anchored ring of recent roots that is inside its validity window, was superseded within the verifier’s height-lag policy and is followed by no emergency root; anything older is rejected. An agent proof needs the issuer’s newest root: one built against any superseded root is rejected.
- The refusal names the failed check —
status_root_stale, not a generic failure. Refresh once and retry, never loop (reason codes). - Revocation lands at the next root, and landing is not yet refusal everywhere: a proof against the previous root stays acceptable while that root is inside its validity window, and its holder (an agent’s principal) answers for what is accepted in that interval. Every verifier refuses a credential its issuer revoked by the bound of its path, which is when the revocation takes effect: in under a minute at the reference values (185 seconds on the single-node production chain), or at once through the emergency key.
- A holder’s own revocation runs on the same clock, plus the 60-second grace the chain gives the issuer to remove the credential: every verifier refuses it within about four minutes of the chain recording it (245 seconds on the single-node production chain; 110 seconds at the reference values), and not before the next root carries it, so a presentation can still be accepted until then.
In the first external test round of the AAIF Identity and Trust Working Group, in October 2026 on the public testnet, the first refusal came 33.8 seconds after the chain recorded the switch-off, and one presentation was still accepted at 27.5 seconds. That is one observation; the bound is 245 seconds.
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
| Grade | What the issuer asserts |
|---|---|
| A1 | basic binding of the subject to the credential |
| A2 | document verification plus a liveness / face-match step |
| A3 | A2 plus sanctions and PEP screening at onboarding and at every renewal, its result and the lists’ version recorded, and a second independent evidence (proof of address, or the identity document’s chip read); short of any of these, the verification is graded A2 |
| A4 | highest assurance, under stricter controls |
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. The issuer publishes the band, and it is read on the classical seller rail. 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’s four grades are its own vocabulary, mapped to eIDAS and ISO/IEC 29115 without claiming conformance to either, and the reasoning is in the assurance vocabulary record.
Current status
The proof core that issues and presents these credentials is built and runs on the single-node production chain, in production since 20 September 2026 and operated by Conectia, and integrators use it under commercial terms; Conectia, the chain’s first issuer and verifier, and ZadQ, the first product built on them (on the classical seller rail), are related parties of MintID. An independent audit of its implementation is a scheduled milestone whose findings are published. Holder-initiated revocation has been in production on the single-node production chain since 20 September 2026, through the privacy relay the operations table describes; its independent audit is a scheduled milestone whose findings will be published. 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.