light-mode-image
Learn
WebGuides

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:

  1. The value is persisted with the session when it is created.
  2. 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.
  3. It is used as the OpenID4VP state claim inside the signed request object. When state is not supplied, MATTR VII falls back to its internal sessionId for this claim.
  4. 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 no state is supplied and a wallet echoes an unexpected value.
  5. 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:

Verifier Web SDK example
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;
};
Reading state from the cross-device result
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;
};
Reading state from the same-device redirect callback
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:

Example back-channel result with state
{
  "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:

Example back-channel failure result with state
{
  "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.

Web application
// 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);
Same-device callback handler
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 }),
  });
}
Backend result handler
// 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 state claim in the signed request object falls back to the MATTR VII sessionId.
  • The same-device redirect URI is unchanged.
  • No state field 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

How would you rate this page?

Last updated on

On this page