How to troubleshoot web app verification
Work from a symptom to the analytic event that explains it, using the identifiers you already have.
This guide covers symptoms specific to the remote web app verification workflow, in both delivery modes. It assumes you know which events the flow emits, as described in Monitoring web app verification.
For the general method, which covers locating events by request ID, reading data.error, and what to
send our support team, refer to
How to monitor a flow.
A verification spans several separate exchanges, so the last event that fired is usually enough to tell you which one broke. Some requests are also rejected before MATTR VII records them, so the absence of an event does not mean the attempt never happened. Refer to when there is no event.
Start every investigation from the sessionId, or from the state value you supplied when you
started the session. Both are inside the event data, so query by event types over the window and
match in your own code.
Establish the mode before anything else. Several of the symptoms below only exist in one of them, and the same evidence can mean different things depending on which one you are looking at. Refer to telling same-device from cross-device apart. Each section below is labeled with the modes it applies to.
The session could not be started
Applies to: both modes.
What you will find: CREDENTIAL_PRESENTATION_SESSION_CREATE_FAIL, and nothing else for the
attempt.
The error.type names the cause, and several of them are about the mode rather than the request
itself:
- The verifier application does not allow the mode that was asked for. A verifier application can
be configured to support same-device only, cross-device only, or both. Supplying a
redirectUriagainst a cross-device-only application, or omitting one against a same-device-only application, is rejected here rather than later. - The redirect URI is not on the allow list. Same-device sessions are rejected unless the
redirectUrimatches one configured on the verifier application. - No valid origin. Cross-device sessions need the origin of the page that will frame the request web view, and it must be a domain the verifier application allows.
- The wallet provider could not be resolved, or the verifier application does not have an OpenID4VP configuration at all.
The fix: compare the options your application passes to requestCredentials() against the
verifier application configuration. The presence or absence of redirectUri is what selects the mode,
so an application that supplies one by default cannot start a cross-device session.
The QR code was displayed and nothing happened
Applies to: cross-device.
What you will find: CREDENTIAL_PRESENTATION_SESSION_CREATE_SUCCESS,
CREDENTIAL_PRESENTATION_SESSION_LOAD_SUCCESS, and one or more
CREDENTIAL_PRESENTATION_SESSION_REQUEST_CREATE_SUCCESS, and no
CREDENTIAL_PRESENTATION_SESSION_RETRIEVE_START for that session.
No wallet ever asked for the request object, which means no wallet ever read the QR code. The holder did not scan it, scanned it with something that is not a credential wallet, such as a camera app that could not resolve the URI scheme, or scanned it with a wallet that could not reach your tenant.
Look for CREDENTIAL_PRESENTATION_SESSION_ABORT_SUCCESS on the same session. If it is there, the
holder or your application closed the session deliberately. If it is not, the session was simply left
open.
The fix: confirm the wallet you expect holders to use is registered against a wallet provider with an authorization endpoint their operating system will route to it. Repeat cases across many holders point at the URI scheme rather than at individual holders.
The holder was never taken to their wallet
Applies to: same-device.
What you will find: CREDENTIAL_PRESENTATION_SESSION_LOAD_SUCCESS, and no
CREDENTIAL_PRESENTATION_SESSION_RETRIEVE_START for that session.
MATTR VII redirected the browser to the authorization request URI and no wallet ever collected the request. The redirect itself is not observable, so the failure is between the browser and the operating system.
The usual causes are the holder not having a wallet installed that is registered to handle the authorization endpoint, an operating system that offered a chooser the holder dismissed, or a custom URI scheme that the browser refused to hand off. Custom schemes fail silently more often than App Links and Universal Links do, because there is no domain association for the operating system to verify against.
The fix: check the authorization endpoint configured on the wallet provider. Where you use an HTTP link, host a fallback page at that URI so a holder without the app installed lands somewhere useful rather than on an error. Refer to Handling URI schemes.
The wallet collected the request and then went quiet
Applies to: both modes.
What you will find: CREDENTIAL_PRESENTATION_SESSION_RETRIEVE_SUCCESS, and no
CREDENTIAL_PRESENTATION_SESSION_RESPONSE_PROCESS_START.
The wallet fetched and validated the signed request object and then stopped. Everything that decides what happens next takes place in the wallet, so the events cannot distinguish between the possible causes. All of these produce the same signature:
- The holder read the request and declined to share.
- The holder abandoned the interaction, or the wallet timed out.
- The wallet could not establish trust in your verifier, for example because it could not match your
request signing certificate against its trusted verifier list, or could not resolve your
/.well-known/oauth-clientmetadata when using the domain trust model. - The holder held no credential matching the request, and the wallet stopped rather than responding.
A wallet that holds no matching credential often does respond rather than going quiet. In that case
you get a completed session whose result lists the credential under credentialErrors with an error
code of notReturned, as described in
Credential errors.
The fix: if this is happening to one holder, ask them what their wallet showed. If it is happening to every holder, treat it as a trust problem first. Check that your verifier root CA certificate has been shared with the wallet provider, and that the domain serving your request objects also serves the client metadata. Refer to the request object step.
The wallet says the request expired or was already used
Applies to: both modes, for different reasons.
What you will find: CREDENTIAL_PRESENTATION_SESSION_RETRIEVE_FAIL.
The request object is single use in both modes, so a wallet that retries a fetch it already completed is rejected.
In cross-device there is a second cause, and it is the more common one. The authorization request
behind the QR code is refreshed periodically, by default every 60 seconds, to reduce the risk of
session fixation attacks. Each refresh emits a new
CREDENTIAL_PRESENTATION_SESSION_REQUEST_CREATE_SUCCESS with a new sessionRequestId, and the
previous request stops working. A holder who scanned a code from a screenshot, or who scanned slowly,
is presenting a request that no longer exists.
Compare the sessionRequestId on the failure against the sessionRequestId on the last
CREDENTIAL_PRESENTATION_SESSION_REQUEST_CREATE_SUCCESS for the session. If they differ, the holder
scanned a stale code. If there is no refresh event at all, which is the normal case in same-device,
the request was simply collected twice.
The fix: in cross-device, the holder needs to scan the code currently on screen. If this is common in your deployment, review how long the code is on display before holders reach it and whether anything in your interface encourages screenshots. In same-device, a repeated collection usually means the holder used the back button or reopened the link, and they need to start the flow again.
MATTR VII rejected the wallet's response
Applies to: both modes.
What you will find: CREDENTIAL_PRESENTATION_SESSION_RESPONSE_PROCESS_START, then
CREDENTIAL_PRESENTATION_SESSION_RESPONSE_PROCESS_FAIL.
The wallet posted a response and MATTR VII would not accept it. This is different from a response
that was accepted and contained a credential that did not verify, which completes successfully and
reports the outcome in the result rather than in a FAIL event.
data.error distinguishes the cases. The common ones are a response that could not be decrypted with
the session's ephemeral key, a response whose state does not match the session, and a response for
a session that no longer exists.
The fix: a single occurrence usually means a stale or duplicated wallet submission and needs no action. A consistent pattern against one wallet implementation is worth raising with us, so capture the request ID.
The presentation completed but the holder never got back to your site
Applies to: same-device.
What you will find: CREDENTIAL_PRESENTATION_SESSION_RESPONSE_PROCESS_SUCCESS, and no
CREDENTIAL_PRESENTATION_SESSION_RESULT_EXCHANGE_START.
The holder shared their credential and MATTR VII stored the result, and then your application never
collected it. In same-device the response code travels back on the redirect to your redirectUri
rather than on the status endpoint, so if that redirect does not land in the browser session that
started the flow, the result is stranded.
The documented causes are all about the browser rather than the credential:
- The flow did not start in the default browser. The wallet's redirect is handled by the device default browser, so a flow started in an in-app browser or a second browser comes back to a different session and is not recognized.
- The flow started in private or incognito mode, where the storage the SDK relies on across the redirect does not survive.
- The redirect URI is on a different domain from the page the holder started on.
The redirect also opens a new tab, leaving the original one behind, which holders sometimes report as the flow having failed when it has actually completed in the other tab.
The fix: confirm the redirectUri shares a domain with the page that starts the interaction, and
advise holders to start in their default browser and not in private mode. Refer to
the verification results step.
Where this needs to be robust regardless of browser behavior, move result delivery to the back
channel, where your backend collects the result and the browser round trip stops mattering.
The session completed but the credential did not verify
Applies to: both modes.
What you will find: CREDENTIAL_PRESENTATION_SESSION_RESPONSE_PROCESS_SUCCESS and
MOBILE_CREDENTIAL_PRESENTATION_VERIFY_SUCCESS, followed by a normal result collection.
This is a successful exchange with an unsuccessful outcome, and the events cannot tell you which,
because the per-credential results are removed before the events are stored. The reason lives in the
session result, on credentials[].verificationResult.reason.type.
The reasons fall into two groups. The credential itself was not acceptable, for example because it was
expired, not yet valid, revoked, suspended, or signed by an issuer that is not on your trusted issuer
list. Or the credential verified and specific claims were missing, which appear under claimErrors
rather than affecting the verified flag at all.
The fix: read the result rather than the events. Refer to Understanding the verified flag. If the reason points at the issuer, check your trusted issuers list.
Your backend cannot retrieve the result
Applies to: both modes, back channel only.
What you will find: either CREDENTIAL_PRESENTATION_SESSION_RESULT_RETRIEVE_FAIL, or no result
event at all alongside a 401 returned to your backend.
The back-channel result endpoint is the one part of this flow that your backend calls with a MATTR VII
access token, while the exchanges around it are not authenticated that way. A 401 there is therefore
an authorization problem rather than a session problem. The token must be issued for the tenant you
are querying and must carry the presentations:sessions:result:read permission.
A RESULT_RETRIEVE_FAIL with the session found means the call was authorized and something else was
wrong, most often that the result had already been collected or that the session had ended.
The fix: check the client credentials your backend uses and the permissions on its role, then confirm your backend is retrieving each result once. Refer to Back channel delivery.
The front-channel result exchange was rejected
Applies to: both modes, front channel only.
What you will find: CREDENTIAL_PRESENTATION_SESSION_RESULT_EXCHANGE_FAIL.
The response code presented in exchange for the result was not accepted. The response code is single use, and it is presented together with the challenge that started the session, so the usual causes are a response code that has already been exchanged and a challenge that does not match the one the session was created with.
The fix: make sure only one part of your application exchanges the result. If you are validating
the challenge yourself, compare the value you generated against the one you passed to
requestCredentials().
You cannot tell which session belongs to which user
Applies to: both modes.
What you will find: sessions you can trace end to end in the events, with no way to connect them to the record in your system that the verification was for.
Nothing about the holder is recorded in this flow. There is no user record, and the credentials and claims are removed from the events before storage, so the events describe a session and not a person.
The fix: supply a state value when you start the session. It appears on the session creation
events and on the front-channel result event, and it is the only value in the events that means
anything in your system. Refer to
Correlating verification sessions.
Nothing appears for the session
Work through these in order:
- Check the step. The QR scan, the wallet being invoked on the same device, the holder's decision in their wallet, the redirect back to your site, and the SDK polling the session status all produce no events. Refer to what you can observe.
- Check the mode. Several events exist in only one mode, so a missing
CREDENTIAL_PRESENTATION_SESSION_REQUEST_CREATE_SUCCESSis expected in same-device rather than a sign that anything went wrong. - Check what you are searching on. The
sessionIdandstateboth sit insidedataand cannot be filtered on directly. Query by eventtypesover the window first, then match in your own code. - Check the time window and the tenant. Confirm you are querying the tenant the verifier application belongs to, and that your window is in UTC.
- Check whether the request could have been rejected before it was recorded. Refer to when there is no event.
Next steps
How would you rate this page?
Last updated on