Skip to content

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

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_uri is 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_id identifies 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_uri as 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