Issuance SDK
Run an issuance flow from a web page and track the offer
An issuance flow is a graph of steps the holder performs before the credential is minted: present a credential, upload a 2D-DOC, pick a record. The issuer decides the order; a client only performs what it is asked. The SDK runs it as an OID4VCI 1.1 authorization challenge through ChallengeClient, and IssuerClient watches the resulting offer.
Run a flow from a browser
import { createChallengeClient, INTERACTION, isInteraction, isOffer } from '@verifiables/eudi-sdk'
const challenge = createChallengeClient({
apiUrl: 'https://api.verifiables.com',
headers: { Authorization: 'Bearer oat_your_publishable_key' },
})
let step = await challenge.start({
issuanceId: 'issuance-id-here',
// What this page can perform. A flow needing anything else answers
// `missing_interaction_type` before any state exists.
capabilities: [INTERACTION.presentation, INTERACTION.select, INTERACTION.twoDDoc],
// A browser cannot hold a credential: the run ends with an offer a wallet scans.
sink: 'credential_offer',
})start answers either with the offer (nothing to do) or with the first interaction. Loop until the offer:
while (isInteraction(step)) {
switch (step.type) {
case INTERACTION.presentation: {
// Cross-device: show `presentation_url` as a QR, the wallet answers the
// verifier, then hand the request back as this step's result.
showQr(step.presentation_url!)
const status = await challenge.pollPresentation(step.request_id!)
if (status.status !== 'valid') throw new Error('Presentation failed')
step = await challenge.answerPresentation(step.auth_session, step.request_id!)
break
}
case INTERACTION.select: {
// Several records matched the presented identity (a company search).
const chosen = await askUser(step.options!) // { id, name, address?, city?, postalCode? }
step = await challenge.answerSelection(step.auth_session, step.token!, chosen.id)
break
}
case INTERACTION.twoDDoc: {
// step.document_type_name names the expected document; the API reads the barcode.
step = await challenge.answerDocumentUpload(step.auth_session, step.token!, file)
break
}
default:
throw new Error(`This page cannot perform ${step.type}`)
}
}
if (isOffer(step)) {
showQr(step.credential_offer_uri) // openid-credential-offer://...
console.log(step.offer_id, step.expires_in)
}Each custom interaction carries a token that must come back with its answer; the SDK adds it as document_token. A failed document read leaves the step open with the same token, so another file can follow.
Errors
Every refusal is a ChallengeError:
import { ChallengeError } from '@verifiables/eudi-sdk'
try {
step = await challenge.start({ ... })
} catch (e) {
if (e instanceof ChallengeError) {
e.status // HTTP status
e.error // 'issuance_expired' | 'issuance_exhausted' | 'missing_interaction_type' | ...
e.description // human-readable, when the issuer gave one
e.missing // interaction types the page lacks, on missing_interaction_type
}
}| Status | error | Meaning |
|---|---|---|
| 404 | invalid_request | No issuance with this id |
| 410 | issuance_expired / issuance_exhausted | The issuance is no longer claimable |
| 400 | missing_interaction_type | The flow needs a step this page did not declare |
| 409 | presentation_pending | Handed back before the wallet answered |
| 422 | no_barcode / wrong_type / document_verification_failed | The uploaded document was refused |
Poll offer status
Track whether the credential has been claimed by the wallet:
import { createIssuerClient } from '@verifiables/eudi-sdk'
const issuer = createIssuerClient({ apiUrl: 'https://api.verifiables.com' })
const status = await issuer.pollOfferStatus(step.offer_id, {
interval: 2000,
timeout: 120000,
})
if (status.status === 'used') {
console.log('Credential issued successfully')
}| Status | Meaning |
|---|---|
pending | Offer created, waiting for wallet |
claimed | Wallet accepted the offer, issuing |
used | Credential issued successfully |
expired | Offer expired before being claimed |
Wallet-driven flows
When the holder has the wallet at hand, the wallet runs the flow itself: it starts the challenge with issuer_state naming the issuance, presents same-device, scans the 2D-DOC with its camera, and exchanges the final code at the token endpoint. That client is @verifiables/wallet-core/challenge; a page has nothing to orchestrate beyond showing the entry point.