Submitting a Presentation
Learn how to present credentials.
The presentation workflow has four steps:
Handle invitation
Get presentation definition
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.
Related guide: Configure OID4VP for wallets
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"
}
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-definitionandpresentation-submitendpoints
- Use V2 of the
- 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.
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 credentialrequired: Whether a claim is requireduserSelection: A flag that the wallet should put a selection toggle on this claim
noteIf there are no suitable credentials found, the response includes a
failureHintin place ofapplicableCredentials. -
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 IDsrequired: 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.
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
- For each of these claims shared, every child with
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)
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 invitationsubmission: Object mapping query IDs to credential selections- Keys must match query IDs from
credentialQueriesin the presentation definition - Each value can be either a single credential submission object or an array of credential submission objects
- Keys must match query IDs from
credentialId: The ID of the credential being submitteduserSelections: Array of claim paths for claims whereuserSelection: truethat 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 choiceEmail- 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.