Start an Issuance
This is the integration itself: the calls your application makes to issue a credential, and what it does with the response. It covers steps 5, 6, 7 and 9 of the issuance flow — the parts that are yours to build. Steps 1 to 4 are your existing login, unchanged, and step 8 is handled for you.
Requirements
- A running credential issuance service. See Install the issuance service.
- An API key, sent as a bearer token on every call below. See API keys.
- At least one credential configuration. Nothing can be issued until you have declared what you are able to issue.
Start a session
Send a POST to /issuance/oid4vci/new-session with the data that goes into the credential:
curl -X POST 'https://issuer.example.org/issuance/oid4vci/new-session' \
-H 'Authorization: Bearer <your API key>' \
-H 'Content-Type: application/json' \
-d '{
"credential_dataset": {
"credentialSubject": {
"first_name": "Jens",
"last_name": "Jensen",
"id_number": 12345678
}
},
"credential_configurations": ["5016c96c-0194-4200-9f57-13d4c8fbb06a"]
}'
credential_dataset must contain a credentialSubject key, with the claims nested underneath it. The claim names
have to match the paths declared in the credential configuration you are issuing against.
| Field | Required | What it does |
|---|---|---|
credential_dataset |
Yes | The claims to put in the credential, nested under credentialSubject. |
credential_configurations |
No, but you should send it | Which configurations to offer. Omitted, the wallet is offered every configuration the service has, which is rarely what you want. |
pre-authorized_code |
Only with an external OAuth server | Obtained from your own authorization server beforehand. Must be omitted if the issuance service is acting as the authorization server. See OAuth 2.0 integration. |
valid_from |
No | When the credential becomes valid. Defaults to the time of issuance. |
valid_until |
No | When it expires. Defaults to one year after issuance. |
Set valid_until deliberately
Both valid_from and valid_until are optional, and omitting valid_until gives the credential a year of
validity. A year is rarely the right answer: a credential that outlives the fact it asserts — an enrolment
that ends in June, a membership renewed each quarter — keeps verifying long after it should. Set
valid_until to the point the underlying fact expires.
Expiry is not a substitute for revocation. Expiry handles the end you can predict at issuance time; revocation handles the end you cannot.
Read the response
{
"session_id": "7d4633b273f000ca2243e258863975907756b3afc29783c7777cf47839264722",
"credential_offer_uri": "haip-vci://?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2Fissuance%2Foid4vci%2Fcredential-offer%2F7d4633b2..."
}
-
credential_offer_uriis what the wallet needs. Get it in front of the holder.It is a deep link, not a web address: the scheme is what makes a wallet open it, and the URL nested inside under a query parameter of the same name is where the wallet then fetches the offer from. Pass the whole value through unchanged — do not unwrap it, and do not build it yourself.
-
session_ididentifies this issuance for the rest of its life. Keep it: it appears in the result pushed to you, and it is what you pass to the status endpoint.
Get the credential offer to the wallet
How you do this depends on where the holder is:
- On a desktop browser, render the
credential_offer_urias a QR code for the holder to scan with their phone. - On the same device as the wallet, present it as a link so tapping it opens the wallet directly.
The scheme that determines which app opens the offer comes from credentialOfferPrefix in the service
configuration; see the configuration reference.
The wallet has to reach the service
The offer URI is built from the issuer's baseUrl, and the wallet then makes several further calls to that
address. A phone cannot resolve localhost, so for a real wallet to complete the flow baseUrl must be an
address that phone can reach. This is the most common reason a QR code scans and then nothing happens.
What happens next, without you
The wallet fetches the offer, reads the issuer metadata, obtains an access token, requests the credential, and stores it. That is several round trips and the issuance service handles all of them.
You do not implement any of this. If you want to see what goes over the wire, it is in OID4VCI issuance protocol.
Receive the result
When the credential has been delivered, the issuance service POSTs a copy to the
sessionCallback.targetUrl you configured, with your bearerToken in the Authorization header:
{
"session_id": "7d4633b273f000ca2243e258863975907756b3afc29783c7777cf47839264722",
"issuance_successful": true,
"credential_response": {
"credentials": [
{ "credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9…" }
]
},
"revocation_uri": "https://issuer.example.org/credential-status/revoke/7d4633b2…"
}
Match it to the interaction you started using session_id, and keep that id — it is what you will need to
revoke this credential, and revocation_uri is simply the address built from it. Setting this
up, and what the credential contains, is covered in
Receive the issued credential.
Checking on a session
If you need to know where a session stands — a holder who never scanned the code, a timeout in your own UI — poll:
curl 'https://issuer.example.org/issuance/oid4vci/status/7d4633b2...' \
-H 'Authorization: Bearer <your API key>'
{
"status": "ISSUANCE_SUCCEEDED",
"error_message": null
}
status |
What it means |
|---|---|
ISSUANCE_STARTED |
The session exists and is waiting for the wallet. |
ISSUANCE_SUCCEEDED |
The credential was issued and handed to the wallet, after your callback accepted it if one is set. |
ISSUANCE_FAILED |
The credential could not be issued. error_message says why — read it before anything else. |
DELIVERY_REJECTED |
The credential was signed, but your callback answered with a non-successful HTTP response. |
DELIVERY_FAILED |
The credential was signed, but your callback could not be reached at all. |
In both delivery statuses the wallet is refused the credential, so the holder does not receive it:
ISSUANCE_SUCCEEDED is the only status in which they do. See
Receive the issued credential.
Use the callback as your primary path and this endpoint for reconciliation.
What's Next
- Receive the issued credential: Configure the callback in full.
- Credential configurations: Change what you issue.
- Revocation: Invalidate a credential after issuing it.
GET /openapion your running issuance service: every endpoint and field.