Skip to main content

Submitting a Presentation

Learn how to present credentials.

The presentation workflow has four steps:

  1. Handle invitation

  2. Get presentation definition

  3. Respond to the request

This page explains each step and how to complete them, with API samples for illustration. The SDK mirrors these API patterns, so developers familiar with this flow can apply the same logic when using the SDK.

Presentation workflow

Handle invitation

The verifier will share an invitation URL, often encoded as a QR code. The URL is generated from the verification protocol; your system must support the verification protocol used by the verifier to parse the offer correctly.

info

Use the "Handle invitation" endpoint to parse the invitation URL:

curl -L -X POST '/api/interaction/v1/handle-invitation' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
-d '{
"url": "https://example.com/invitation"
}'

The system returns an interaction reference and a request reference:

{
"interactionId": "f30c8550-92a9-4b94-84b9-e335deeae112",
"proofId": "3df46447-8ee8-4697-8f88-340398bc7a5a"
}
note

For mobile devices with multiple transport protocols configured (for example, MQTT and BLE), use transport to override the default protocol.

Get presentation definition

This function takes a proof request, evaluates it against available credentials, and returns matching presentations. There are two versions of this endpoint/function.

Which version should you use?

OpenID4VP uses different query languages depending on the version:

  • OpenID4VP 1.0 (final version) uses DCQL as the query language
    • Use V2 of the presentation-definition and presentation-submit endpoints
  • Earlier drafts of OpenID4VP use a different query language
    • Use V1 of these endpoints

The version is determined by the verifier's request. The system can support multiple versions and must respond according to whichever version the verifier is using.

hint

You can check capabilities.supportedPresentationDefinition in your configuration to determine which version of the endpoints to use with each instance of the protocol.

Presentation Definition (V2)

Pass the proof ID:

curl -X 'GET' \
'/api/proof-request/v2/3df46447-8ee8-4697-8f88-340398bc7a5a/presentation-definition' \
-H 'accept: application/json' \

Response structure

The V2 presentation definition response contains two main structures:

credentialQueries: An object where each key represents a query ID. Each query contains:

  • applicableCredentials: Array of credentials that match this query. Key fields for each credential include:

    • claims: Array of claims requested from the credential
      • required: Whether a claim is required
      • userSelection: A flag that the wallet should put a selection toggle on this claim
    note

    If there are no suitable credentials found, the response includes a failureHint in place of applicableCredentials.

  • multiple: Boolean indicating if multiple credentials can be submitted for this query

credentialSets: An array defining valid combinations of credentials. Each set contains:

  • options: Array of arrays, where each inner array represents one valid combination of query IDs
  • required: Boolean indicating if this credential set must be satisfied

Example response

{
"credentialQueries": {
"53ff9ab8-6a77-42fa-9cc2-e8fe74b02075": {
"multiple": false,
"applicableCredentials": [
{
"id": "8eeaa380-9dd2-429c-8f10-d81df5c6f4d1",
"createdDate": "2025-10-16T07:41:23.474Z",
"issuanceDate": "2025-10-16T07:42:53.000Z",
"state": "ACCEPTED",
"lastModified": "2025-10-16T07:42:53.889Z",

// Schema defining the credential structure
"schema": {
"id": "8889f5cb-81f0-47d8-8dec-bf7e513b3ac5",
"createdDate": "2025-10-16T07:33:12.612Z",
"lastModified": "2025-10-16T07:42:53.869Z",
"name": "Drivers License",
"format": "SD_JWT_VC",
"revocationMethod": "TOKENSTATUSLIST",
"organisationId": "ac915dd6-6319-46a2-9777-c5753912a921",
"keyStorageSecurity": "BASIC",
"schemaId": "https://example.com/driverslicence",
"schemaType": "SdJwtVc",
"layoutType": "CARD",
"allowSuspension": false
},

// Information about who issued the credential
"issuer": {
"id": "d52b2cfb-c4f0-4409-a060-530321d708a7",
"name": "issuer b755d3cd-e600-42bb-90c5-57bd468c3faf",
"createdDate": "2025-10-16T07:33:12.514Z",
"lastModified": "2025-10-16T07:33:12.514Z",
"state": "ACTIVE",
"type": "DID",
"isRemote": true,
"organisationId": "ac915dd6-6319-46a2-9777-c5753912a921"
},

// Array of claims that satisfy the request criteria
"claims": [
{
"path": "Name",

// Claim metadata
"schema": {
"id": "159ab7e8-2f04-46a3-a5a0-b4c78016939b",
"createdDate": "2025-10-16T07:33:12.612Z",
"lastModified": "2025-10-16T07:33:12.612Z",
"key": "Name",
"datatype": "STRING",
"required": true,
"array": false,
"claims": []
},
// Claim value
"value": "Erika Mustermann",
"userSelection": false,
"required": true
},
{
"path": "Email",
"schema": {
"id": "8729a1b2-b665-4f43-85a9-a7d5abbffa2e",
"createdDate": "2025-10-16T07:33:12.612Z",
"lastModified": "2025-10-16T07:33:12.612Z",
"key": "Email",
"datatype": "STRING",
"required": true,
"array": false,
"claims": []
},
// Claim value
"value": "erika@mustermann.com",
"userSelection": true,
"required": false
}
],
"role": "HOLDER",

// Identifier data used by the wallet for this credential
"holder": {
"id": "13ce172e-ffed-44ef-b45c-0a6b7ef32b36",
"name": "DID",
"createdDate": "2025-10-16T07:31:07.106Z",
"lastModified": "2025-10-16T07:31:07.106Z",
"state": "ACTIVE",
"type": "DID",
"isRemote": false,
"organisationId": "ac915dd6-6319-46a2-9777-c5753912a921"
},
"protocol": "OPENID4VCI_FINAL1"
}
]
}
},

// Constraints on which of the requested credentials to return
"credentialSets": [
{
"required": true,
"options": [
[
"53ff9ab8-6a77-42fa-9cc2-e8fe74b02075"
]
]
}
]
}

Understanding claim flags

All claims that could be shared with the verifier are shown in the claims object. To help parse what will be shared and where the end user may have options, the presentation definition response includes two key flags:

Required flag

The "required" flag indicates whether a claim must be included in the submission. For a claim to be "required": false, two conditions must be met:

  • The verifier makes the claim optional for the request
  • The credential and credential format support selective disclosure for this claim

If a verifier makes a claim optional but the credential format does not support selective disclosure, the claim will be "required": true.

Hint

A "required": false flag means the wallet can hide the claim from the verifier during submission. Whether the claim is shared or not depends on the choices made by the end users.

User selection flag

The "userSelection" flag indicates a claim which can optionally be submitted. If true, the wallet should show a toggle for this claim and allow the user to choose whether to share the claim or not.

However, it also crucial that the consequences of sharing optional claims is clear to the user. Due to the transitive nature of nested claims, submitting an optional claim can result in submission of other claims.

"userSelection" Rules:

If a claim with "userSelection": true is shared, the following claims are also shared:

  • Every child claim
  • Every parent claim leading up to the root
    • For each of these claims shared, every child with required: true, transitively branching out

This means that sharing an optional claim can result in sharing many additional claims. For example:

In this example, the verifier requests:

  • Claim 1 (required)
  • Claim 2.1 (optional)

All four claims are returned from presentation definition because all claims are potentially shareable. If the user chooses to share Claim 2.1:

  • Claim 2 is automatically included (parent of 2.1)
  • Claim 2.2 is automatically included (required when Claim 2 is shared)
note

If Claim 2.2 had a child claim 2.2.1 that was "required": true, and that claim had a child claim 2.2.1.1 that was "required": true, and so on, all claims would be shared by sharing Claim 2.1.

Understanding the request

Looking at our example response, the verifier is requesting:

  • Name - required ("required": true)
  • Email - optional ("required": false)

The Email claim also has "userSelection": true, indicating the wallet should show a toggle to let the user choose whether to share it.

Let's say Erika Mustermann decides to share her name but withhold her email address. In the next section, we'll see how to construct the presentation submission that reflects her choices.

DCQL extension: claim optionality

The DCQL specification in OpenID4VP v1.0 does not provide claim-level optionality directly. For example, "Annex D: Examples for DCQL Queries" includes a verifier's request "where an ID and an address are requested; either can come from an mDL or a photoid Credential". Here is the request, edited for brevity:

{
"credentials": [
{
"id": "mdl-id",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"id": "given_name",
"path": ["org.iso.18013.5.1", "given_name"]
},
{
"id": "family_name",
"path": ["org.iso.18013.5.1", "family_name"]
},
{
"id": "portrait",
"path": ["org.iso.18013.5.1", "portrait"]
}
]
},
{
"id": "mdl-address",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"id": "resident_address",
"path": ["org.iso.18013.5.1", "resident_address"]
},
{
"id": "resident_country",
"path": ["org.iso.18013.5.1", "resident_country"]
}
]
},
],
"credential_sets": [
{
"options": [
[ "mdl-id" ]
]
},
{
"required": false,
"options": [
[ "mdl-address" ]
]
}
]
}

To make the address optional, the verifier includes a second query for the credential and then includes an additional credential set containing the second query and marking that credential set as "required": false.

This is potentially problematic: a user holding multiple mDLs could respond to the above example with the name from one mDL and the address from another. The verifier may explicitly want the fields from a single credential, but there is no way to express this with DCQL. The verifier can only discern that two credentials were used after the proof is validated.

DCQL extension:

Procivis One can handle the above query, but has also been extended to handle options at the claims-level. This allows for verifiers to keep optional claims together on the same credential query as mandatory claims.

How to use the extension

Other systems can make use of the extension by adding "required": false to any claims they wish to make optional:

{
"credentials": [
{
"id": "mdl-id",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"id": "given_name",
"path": ["org.iso.18013.5.1", "given_name"]
},
{
"id": "family_name",
"path": ["org.iso.18013.5.1", "family_name"]
},
{
"id": "portrait",
"path": ["org.iso.18013.5.1", "portrait"]
},
{
"id": "resident_address",
"path": ["org.iso.18013.5.1", "resident_address"],
"required": false, // Extension
},
{
"id": "resident_country",
"path": ["org.iso.18013.5.1", "resident_country"],
"required": false, // Extension
}
]
},
],
"credential_sets": [
{
"options": [
[ "mdl-id" ]
]
}
]
}

Wallets that support the extension can parse the above query as having two optional claims while also only allowing a response from a single mDL credential.

Maintaining compatibility with wallets that do not support the extension

Wallets that do not support this extension will ignore the "required": false flag, instead interpreting all claims as being required.

To assist these wallets in handling claim optionality, use claim sets, specifying first a claim set with all claims and second a claim set with only the required claims:

{
"credentials": [
{
"id": "mdl-id",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.18013.5.1.mDL"
},
"claims": [
{
"id": "given_name",
"path": ["org.iso.18013.5.1", "given_name"]
},
{
"id": "family_name",
"path": ["org.iso.18013.5.1", "family_name"]
},
{
"id": "portrait",
"path": ["org.iso.18013.5.1", "portrait"]
},
{
"id": "resident_address",
"path": ["org.iso.18013.5.1", "resident_address"],
"required": false, // Extension
},
{
"id": "resident_country",
"path": ["org.iso.18013.5.1", "resident_country"],
"required": false, // Extension
}
]
},
],
"credential_sets": [
["given_name", "family_name", "portrait", "resident_address", "resident_country"], // Full set
["given_name", "family_name", "portrait"], // Only required
]
}

Any wallets capable of responding to the required set can then respond to the request, even if they cannot respond to the full set.

Submit presentation

This function submits the holder's chosen presentation to the verifier. After the holder reviews their options and makes their selections, the wallet must map these choices to the proper request structure and submit them.

Request structure

The presentation submission requires:

{
"interactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"submission": {
"query-id-1": [
{
"credentialId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"userSelections": ["claimPath1", "claimPath2"] // Only if the user chooses to share
}
]
}
}

Where:

  • interactionId: The interaction reference from handle invitation
  • submission: Object mapping query IDs to credential selections
    • Keys must match query IDs from credentialQueries in the presentation definition
    • Each value can be either a single credential submission object or an array of credential submission objects
  • credentialId: The ID of the credential being submitted
  • userSelections: Array of claim paths for claims where userSelection: true that the holder chooses to share
    • Only include paths for optional claims the holder selects
    • Omit entirely or use an empty array if withholding all optional claims

Mapping holder choices to the submission

Looking back at our presentation definition example, Erika has decided to share her name but withhold her email. Here's how the wallet maps this choice:

From the presentation definition response:

  • Query ID: 53ff9ab8-6a77-42fa-9cc2-e8fe74b02075
  • Credential ID: 8eeaa380-9dd2-429c-8f10-d81df5c6f4d1
  • Available claims
    • Name - required, no choice
    • Email - optional, she chooses not to share

Resulting submission

Since Erika is not sharing any optional claims, userSelections can be left out:

curl -X 'POST' \
'/api/interaction/v2/presentation-submit' \
-H 'Content-Type: application/json' \
-d '{
"interactionId": "f647515d-0bc1-4c91-9419-5b4d45a5205c",
"submission": {
"53ff9ab8-6a77-42fa-9cc2-e8fe74b02075": [
{
"credentialId": "8eeaa380-9dd2-429c-8f10-d81df5c6f4d1"
}
]
}
}'

If Erika had chosen to share her email as well, the submission would include:

"userSelections": ["Email"]

Reject presentation

If the holder decides not to share any information, the wallet can reject the presentation request.

In our example, if Erika chooses not to proceed, the wallet submits:

curl -L -X POST '/api/interaction/v1/presentation-reject' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"interactionId":"f647515d-0bc1-4c91-9419-5b4d45a5205c"
}'

Only the interactionId is required to reject a presentation request.