Correlating verification sessions with your system
Learn how to use the optional state parameter to link a MATTR VII verification session to a record in your own application without implementing separate state management.
When a web application starts a remote verification session, MATTR VII generates its own
sessionId (a UUID) to track the session through its lifecycle. That identifier is fine for
referring to the session within MATTR VII, but it does not map to anything in your own system. If
you need to attach the verification to an existing record on your side, such as an onboarding
application, a checkout intent, or an account opening flow, you need a correlation reference of
your own.
The state parameter on requestCredentials() is intended for exactly this. You pass an opaque
string that means something in your system, and MATTR VII carries it through the verification
session and returns it to you with the result. No additional state management is required on
your application or backend.
When to use state
Use state when your application has an existing record that a verification session needs to be
attached to, and you want a single value that:
- You generate and control.
- Travels through the entire session, including the wallet interaction.
- Comes back to you in both same-device and cross-device flows.
- Is available on both successful and failed results.
Typical examples include an onboarding application reference, a checkout or transaction identifier, or any internal record ID that needs to be reconciled with the verification outcome.
If you only need to verify a credential and immediately act on the result in the same browser
session, state is not required. The MATTR VII sessionId is enough on its own. Use state
when correlation needs to survive the round-trip to the wallet and back.
The state value is transmitted in plain text as a query parameter on the wallet-facing
authorization request URI and is stored in session records. It should not contain personally
identifiable information (PII), credentials, or anything sensitive. Use an opaque reference,
such as an internal record ID or a randomly generated token your system can map back to the
underlying record.
How state flows through a session
When you supply state, MATTR VII threads the value through the OpenID4VP presentation flow:
- The value is persisted with the session when it is created.
- It is appended as a plain
&state={value}query parameter on the wallet-facing authorization request URI, making it readable to the wallet without parsing the signed request object. - It is used as the OpenID4VP
stateclaim inside the signed request object. Whenstateis not supplied, MATTR VII falls back to its internalsessionIdfor this claim. - The wallet's response is validated against the stored value. A mismatch results in a
failure result with a
VerificationError, the same behavior as when nostateis supplied and a wallet echoes an unexpected value. - The value is returned to your application in the session result, on both success and failure shapes.
This flow applies to OpenID4VP browser flows in both same-device and cross-device variants. It does not apply to the Digital Credentials API flow.
Setting state when starting a session
Pass state alongside the other options when calling requestCredentials() in the Verifier
Web SDK:
const options: MATTRVerifierSDK.RequestCredentialsOptions = {
credentialQuery: [credentialQuery],
challenge: MATTRVerifierSDK.utils.generateChallenge(),
state: "onboarding-app-7f3a4e8c",
openid4vpConfiguration: {
redirectUri: window.location.origin,
walletProviderId: process.env.NEXT_PUBLIC_WALLET_PROVIDER_ID,
},
};
const results = await MATTRVerifierSDK.requestCredentials(options);The value you choose is opaque to MATTR VII. Generate it in whatever way fits your system, for example by reusing an existing internal record ID, by minting a fresh random token at the moment you start the verification, or by deriving a one-way reference from a database row.
Constraints
state is optional. When provided it should be a non-empty string of at most 256 characters.
A request that supplies an empty string or a value longer than 256 characters is rejected with
an HTTP 400 validation error.
The 256-character limit is enforced at the API boundary and is sufficient for any UUID, opaque token, or short reference ID. If your internal identifier is longer, store it on your side and pass a shorter reference that maps back to it.
Reading state back
state is returned everywhere a session result is exposed: in the SDK return values for both
same-device and cross-device flows, and in the back-channel result endpoint used by your
backend. It appears on both PresentationSuccessResult and PresentationFailureResult, so you
can correlate failures as well as successes.
Cross-device flow (front channel)
In cross-device flows, the Verifier Web SDK resolves results in the same call that started the
session. The state you supplied is echoed in the returned RequestCredentialsResponse:
type RequestCredentialsResponse = {
sessionId: string;
state?: string;
result?: PresentationSessionResult;
sessionCompletedInRedirect?: boolean;
};const results = await MATTRVerifierSDK.requestCredentials(options);
if (results.isOk()) {
const { state, result } = results.value;
// state === "onboarding-app-7f3a4e8c"
// Use state to look up the matching record in your system.
}Same-device flow (front channel)
In same-device flows, the wallet redirects the user back to your redirectUri after the
presentation completes, and your application calls handleRedirectCallback() to retrieve the
result. The returned state is the same value you supplied at session creation:
type HandleRedirectCallbackResponse = {
sessionId: string;
state?: string;
result?: PresentationSessionResult;
};const results = await MATTRVerifierSDK.handleRedirectCallback();
if (results.isOk()) {
const { state, result } = results.value;
// state === "onboarding-app-7f3a4e8c"
}The SDK persists state to localStorage before redirecting the browser to the wallet and
cleans it up after handleRedirectCallback() completes. This local persistence is an
implementation detail of the same-device flow and is needed because some relying-party
frameworks strip query parameters from redirect URIs. You do not need to read or write this
storage entry yourself.
Back channel
When your verifier application is configured with resultAvailableInFrontChannel: false, your
backend retrieves results by calling the
retrieve presentation session result
endpoint. The response body includes the same state field, present only when a value was
supplied at session creation:
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"challenge": "c5a27e4c-85b6-4b3c-9f1a-2d8e5f3a4b7c",
"state": "onboarding-app-7f3a4e8c",
"credentialQuery": [
{
"profile": "mobile",
"docType": "org.iso.18013.5.1.mDL",
"nameSpaces": {
"org.iso.18013.5.1": {
"family_name": { "intentToRetain": false },
"given_name": { "intentToRetain": false }
}
}
}
],
"credentials": [
{
"docType": "org.iso.18013.5.1.mDL",
"claims": {
"org.iso.18013.5.1": {
"family_name": { "value": "Smith" },
"given_name": { "value": "Jane" }
}
},
"verificationResult": { "verified": true }
}
]
}The same field appears on failure results, allowing you to correlate failed sessions to your internal record:
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"challenge": "c5a27e4c-85b6-4b3c-9f1a-2d8e5f3a4b7c",
"state": "onboarding-app-7f3a4e8c",
"credentialQuery": [
{
"profile": "mobile",
"docType": "org.iso.18013.5.1.mDL",
"nameSpaces": {
"org.iso.18013.5.1": {
"family_name": { "intentToRetain": false }
}
}
}
],
"error": {
"type": "SessionAborted",
"message": "User aborted the session"
}
}state is a correlation reference, not a security mechanism. It does not replace the
challenge used to detect session replay. When using back-channel delivery, your backend
should still generate a unique challenge per session and validate the value returned in
the result against the one you stored. See the
handling verification results guide
for the full back-channel pattern.
A worked example
A common pattern is to mint an internal reference when a user starts a verification step,
record it against the user's session in your system, pass it as state, and use it on the
return trip to find the right record.
// Mint or retrieve an internal reference for this user's onboarding step.
const applicationRef = await fetch("/api/onboarding/start", { method: "POST" })
.then((r) => r.json())
.then((r) => r.applicationRef);
const options: MATTRVerifierSDK.RequestCredentialsOptions = {
credentialQuery: [credentialQuery],
challenge: await createChallenge(),
state: applicationRef,
openid4vpConfiguration: {
redirectUri: `${window.location.origin}/onboarding/complete`,
walletProviderId: process.env.NEXT_PUBLIC_WALLET_PROVIDER_ID,
},
};
await MATTRVerifierSDK.requestCredentials(options);const results = await MATTRVerifierSDK.handleRedirectCallback();
if (results.isOk()) {
const { sessionId, state, result } = results.value;
// Hand sessionId and state to your backend so it can fetch the result
// and attach it to the correct application record.
await fetch("/api/onboarding/complete", {
method: "POST",
body: JSON.stringify({ sessionId, applicationRef: state }),
});
}// In your backend handler for /api/onboarding/complete:
const result = await fetchPresentationResult(sessionId);
if (result.challenge !== storedChallengeFor(sessionId)) {
throw new Error("Challenge mismatch");
}
// result.state is the same applicationRef the web application supplied.
await applications.attachVerificationResult(result.state, result);This pattern lets your backend reconcile the verification outcome with the originating
application record without maintaining a separate sessionId-to-application mapping.
When state is not supplied
state is fully optional and additive. If you do not pass it:
- No
&state=query parameter is added to the authorization request URI. - The OpenID4VP
stateclaim in the signed request object falls back to the MATTR VIIsessionId. - The same-device redirect URI is unchanged.
- No
statefield appears in any result, on either success or failure shapes.
Existing integrations that do not pass state will see no change in behavior.
Next steps
- Review the Verifier Web SDK API reference for complete type definitions.
- See the handling verification results guide for the broader result structure and the back-channel challenge validation pattern.
- See the workflow guide for where the
statevalue sits within the end-to-end remote verification flow.
How would you rate this page?
Last updated on