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. |
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…" }
]
}
}
Match it to the interaction you started using session_id. 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": "SUCCEEDED",
"error_message": null
}
status is STARTED, SUCCEEDED or FAILED. On a failure, error_message says what went wrong — read it
before anything else.
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.
- Issuer API reference: Every endpoint and field.