Prerequisites

In the below exercises we will be creating credential offers in different ways. Please read the OID4VP introduction page first that explains the concept of credential offers.

Please make sure to have read the OID4VP introduction introduction first!
These exercises assume you have installed a wallet that is capable of OID4VCI and compatible in specification version with our Issuer software. If in doubt we suggest to use the Sphereon Wallet from the Android/iOS stores
We suggest to use a tool like Postman or curl to perform these exercises and examples. Although we provide an integrated UI in these docs and links to our swagger hub, we are also developers and in our experience new learnings stick better whilst actually doing instead of clicking.
If you are running a local agent with ngrok, you cannot use the Swaggerhub examples directly. What you need to do is load the OpenAPI definition through the swagger editor like this: https://petstore.swagger.io/?url=https://api.swaggerhub.com/apis/SphereonInt/OID4VP/0.1.0 Now you should be able to execute against localhost.

Authorization request and session initiation

In the below exercises you will learn how to present credentials using an Authorization Request by value and by reference. You will actually present using a mobile wallet!

Exercise 1: Credential offer by value, using pre-authorized_code

Please make sure to have performed the OID4VCI exercises first, so your wallet has credentials it can present to the Relying Party

Please use the create-authorization-request API (docs) or API (swaggerhub) to create a so-called Authentication Request by providing a presentation definition.

You will need to provide a definitionId for instance with value PensionSdJwt as path parameter, given every OID4VP Authorization Request will internally use a Presentation Definition (or DCQL query in the future)

The expected outcome should be an HTTP response with something like:

{
  "correlationId": "2cc29d1c-7d00-46f8-b0ae-b4779d2ff143",
  "definitionId": "PensionSdJwt",
  "authRequestURI": "openid-vc://?request_uri=https://agent.findynet.demo.sphereon.com/siop/definitions/PensionSdJwt/auth-requests/b5cab09e-7c08-42c9-870b-c2b83a2c8acd",
  "authStatusURI": "https://agent.findynet.demo.sphereon.com/webapp/auth-status"
}

The above authRequestURI is the actual URI value that can be used in a link or QR code (more on that later). A wallet is using that value to start the presentation process with the Relying Party. This value is always by reference.

Contrary to the OID4VCI API where you can define whether the request should be by value or by reference, for OID4VP we only allow to create by reference. The reason is that too much data needs to go into the QR code, making it unusable by value. Our libraries and wallets however do support by reference

The authStatusURI is the URI you need to use to track the status progress over time (next exercise). You will use the correlationId in that process to get the correct session.

Copy the response payload, as we will use it later

Exercise 2: Create a QR code from the URI and scan it with a mobile wallet

Unfortunately the OID4VP REST API currently does not have built-in QR code generation support like the OID4VCI API. We will add that soon though.

In a lot of situations your wallet is on a mobile device, while you might be visiting the relying party website using another device like a tablet or desktop computer. This is the so called cross-device flow. AAlthough new mechanisms are being developer whereby your mobile wallet can register with your browser, right now an Authorization Request is being conveyed using a QR code. This is a short exercise to show that all you really need to do is display the request value in a QR code to really start the interaction with the relying party from a mobile device.

Take the outcome of Exercise 1 and copy the value of the authRequestURI field from the JSON response. So do not copy the whole JSON response. You should only copy the value that starts with openid-vc://?request_uri=.... also minus the quotes in front and at the end.

Go to an online QR code generator like: https://www.qr-code-generator.com/ and past the request value. You should get a QR code back. Below is an example QR code. Obviously yours will differ.

The above example QR will not work as the relying party does not know about that session anymore!

Now take you mobile wallet application and make sure you are logged in. Typically the wallet app will have a QR code scanning functionality. You can use that to initiate the presentation process. Most wallets will also register with the Operating System, meaning you can also scan the QR code using your regular photo/camera app instead. Then depending on your brand the wallet will open, or you will get a choice which wallet app to open. Finish the presentation process on your mobile wallet, to see that all it took was creating a URI and displaying that as a QR code.

Exercise 3: Protect the QR code with a transaction code?

In the Issuer exercises we protected the QR code with a transaction/pin code. This is not possible in the OID4VP flow. Could you write down why that is? What are the big differences between the 2 flows and why was the pincode needed?

Session status

Up until now we only have been using one endpoint to start the presentation process from the API and then we received the request to present credentials in our wallet. Typically you will have a portal or web-app that is guiding the user. This is the case both in the same-device as cross-device flow. When you create a web frontend for the presentation process, you will want your frontend to react to states changes in the presentation process, to inform the user about errors, success etc.

Not only that, the goal is to eventually receive one or more presentations containing the credentials from the wallet holder.

The create-authrozation-request endpoint we used above internally creates a session. The session contains all kind of information needed by the relying party; one of the important properties it has in the session is the state. The state can have the following values:

stateDescription
createdA Authorization Request is created. This is the initial state
sentWhen the wallet has retrieved the Authorization Request by reference using the URI provided in the link
recievedWhen the Relying Party received the presentation(s) from the wallet
verifiedWhen the Relying Party has verified that the presentation(s) were cryptographicaly correct and were satisfying the credential queries (Presentation Exchange, DCQL)
errorAn error occured
The above states are more or less in order. The received/verified and error are mutually exclusive.

Exercise 4: Get the initial status

In this exercise we will retrieve the initial status of the credential offer. Without a wallet involved. The state will be created

Please use the get-authorization-status API (docs) or API (swaggerhub) to check the progress of the session using the correlationId value you supplied during session creation above and definitionId with the value you previously used in the path. You could also create a new session as explained in the above exercises.

You should get a response like this:

{
  "status": "created",
  "correlationId": "2cc29d1c-7d00-46f8-b0ae-b4779d2ff143",
  "definitionId": "PensionSdJwt",
  "lastUpdated": 1706515200000
}

The status is from the table above.

Exercise 5: Now scan the QR code

In this exercise we will continue where the previous exercise ended. We will use the wallet to scan the QR code. Make sure that you create the QR code from the authRequestUri Scan the QR code with the mobile wallet. Once the wallet is in the present screen, send in the credential

Please use the get-authorization-status API (docs) or API (swaggerhub) to check the progress of the session using the correlationId value you supplied during session creation above and definitionId with the value you previously used in the path. You could also create a new session as explained in the above exercises.

You should get a response like this:

{
  "status": "verified",
  "correlationId": "3505ec66-7886-455b-8bae-63ec359a6a44",
  "definitionId": "PensionSdJwt",
  "lastUpdated": 1739323518318,
  "payload": {
    "state": "3505ec66-7886-455b-8bae-63ec359a6a44",
    "presentation_submission": {
      "id": "6zoPcSLOVZAApZLajaKUc",
      "definition_id": "PensionSdJwt",
      "descriptor_map": [
        {
          "id": "e5d97b9a-d9cd-416e-9d08-1da46145a4a6",
          "format": "vc+sd-jwt",
          "path": "$"
        }
      ]
    },
    "vp_token": "eyJ0eXAiOiJ2YytzZC1qd3QiLCJraWQiOiJkaWQ6d2ViOmFnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tIzAzYzk4NTNmMTAzNjhiMDU3ZjAzM2FmMTU2OGI1MmIyNGVlMzg5MDU1YjczMTYyZjhmNGFlZjdkMDBhZmQ5ZTRkNyIsImFsZyI6IkVTMjU2In0.eyJ2Y3QiOiJQZW5zaW9uU2RKd3QiLCJQZW5zaW9uIjp7Il9zZCI6WyI0Vk9xMjhVaFREenMzeXk0M082UnFHbU9SRWpFVEJxdVZzNTBiaVZVVnZzIiwiTWh2YlVjNEtyQ3RKZU93a2U5Q05IYkVhbUhPYVZSRkxGempGVUhMc0doZyIsImVxOVNkcGY5MUdzZDgtcmhmcmZwTmJwZjl3VUF5bm1rNHl0WFZqaW1pNnciLCJuR2xpejc5cWo3dGgwajFubjJHa0pETUFkZTBMRThuVWtFcy1qbHk4aXdrIiwibzZXc3A3UEtGbEJLSVlxakxZU3Znc1pXUWxwNUNTQ1c0TGxlQzd0eGhQQSJdfSwiUGVyc29uIjp7Il9zZCI6WyI0TDBzTmVSa1FLeVdiWTB3amlETU9QUWFiUXJfNWhMVEtCdGFmTkFxQWZjIiwiRjZsM0Q1UWIzQkd6eEpHdC1Qc285dDBxbFdoRWU0ZDlzNW9RLTNGMXlCWSIsIkZiR0xNMnFpeHNoMGdLZ21qbFZRQUctRFRqdV9fblZoQmgzZUJoX2x1TXciLCJJNGxMOExaLVZsdTB1MVVlLU9oTjZoTzFNYW1GMVZvUXRkNk1YWTR5YzdRIl19LCJleHBpcmF0aW9uRGF0ZSI6IjIwMjYtMDItMTJUMDA6MDM6NDMuNzc4WiIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJ1cmkiOiJodHRwczovL2FnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tL3ZjL3N0YXR1cy1saXN0cy9wZW5zaW9uY3JlZGVudGlhbC1vYXV0aCIsImlkeCI6ODAwODV9fSwiY25mIjp7ImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5pSXNJblZ6WlNJNkluTnBaeUlzSW10MGVTSTZJa1ZESWl3aVkzSjJJam9pVUMweU5UWWlMQ0o0SWpvaVdHazBhMXBUWW1OaWRYVnZUMDh3ZVhoNWNESm9TUzA0TVZCemQzRjFUSEJ0U2xveVIybHhWM0pLZHlJc0lua2lPaUpVTTFSMVlXSjBhMFYwWDNkcGJGaHBVRTFEZWxCWk4zSnVNVWxIVUZjMlRpMXNORUZhUTE5VE9GaDNJbjAjMCJ9LCJpc3MiOiJkaWQ6d2ViOmFnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tIiwiaWF0IjoxNzM5MzE1MDIzLCJfc2RfYWxnIjoic2hhLTI1NiJ9.7Sdid1hKMaYAWVJE3GldqT0fHK-koGSghSE0Lws0DOI6Dez-P5Vxfj2nFYnsDSjubNelP4DgzGLIlBjNFloLtA~WyJjZmY0NjYxNy1iYzhiLTQ1MDgtODc1Ny0zY2ZkYmJjYmE4ZjYiLCJlbmREYXRlIiwiMjA3MC0wNS0xMCJd~WyI0MzEzMWRhNy1mOTZlLTQ0MGItODRlMC1jNzEwYTJmNjc4ZWUiLCJwcm92aXNpb25hbCIsIiJd~WyIyZWUyZWMwNS1mN2IwLTQ2NTgtODBkNS05ZjUzM2RjODhhZTYiLCJzdGFydERhdGUiLCIyMDQwLTAxLTAxIl0~WyI0MzA2OTg5Mi0zMjRkLTQzZjAtOWQ1Ny0zNWJmMGY2YjYxZTAiLCJ0eXBlQ29kZSIsIlBSRSJd~WyI4ZGE3ZGNlMS1kMDUyLTRlYzgtYmM4MC03NDY2ZDAxZGRkOTAiLCJmYW1pbHlfbmFtZSIsIkRvZSJd~WyI4OWI5OTY5Yi04Y2I1LTQ5NGMtODc2Ny1hZGY1MDFlMTZjNjciLCJwZXJzb25hbF9hZG1pbmlzdHJhdGl2ZV9udW1iZXIiLCIxMjM0NTY3OCJd~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MzkzMjM1MTcsIm5vbmNlIjoiYWJlYmM0MWUtZDU4Ni00N2E0LTg2NzYtNTdjOGQ0OGZiODhiIiwic2RfaGFzaCI6IjhTTnhvY3Y4bmZCbURZdHR1b2IyWXlNTldrT0pTQ2hMNnJBbTFXSDNpRzQiLCJhdWQiOiJkaWQ6d2ViOmFnZW50LmZpbmR5bmV0LmRlbW8uc3BoZXJlb24uY29tIn0.Hv_klG1XNl6ws0zyK951GQBar5dQiyW1QPEtP6bThAhJ5VSoGcYd7lFJT5ss35lqIcr6G-DfN743zi8QcM-9lA",
    "verifiedData": {
      "vct": "PensionSdJwt",
      "Pension": {
        "endDate": "2070-05-10",
        "provisional": "",
        "startDate": "2040-01-01",
        "typeCode": "PRE"
      },
      "Person": {
        "personal_administrative_number": "12345678",
        "family_name": "Doe"
      },
      "expirationDate": "2026-02-12T00:03:43.778Z",
      "status": {
        "status_list": {
          "uri": "https://agent.findynet.demo.sphereon.com/vc/status-lists/pensioncredential-oauth",
          "idx": 80085
        }
      },
      "cnf": {
        "kid": "did:jwk:eyJhbGciOiJFUzI1NiIsInVzZSI6InNpZyIsImt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiWGk0a1pTYmNidXVvT08weXh5cDJoSS04MVBzd3F1THBtSloyR2lxV3JKdyIsInkiOiJUM1R1YWJ0a0V0X3dpbFhpUE1DelBZN3JuMUlHUFc2Ti1sNEFaQ19TOFh3In0#0"
      },
      "iss": "did:web:agent.findynet.demo.sphereon.com",
      "iat": 1739315023
    },
    "nonce": "abebc41e-d586-47a4-8676-57c8d48fb88b"
  },
  "verifiedData": {}
}

The status is from the table above.

Note that the response all of a sudden contains the information that was submitted by the wallet