How to troubleshoot the OID4VCI Pre-authorized Code flow
Work from a symptom to the analytic event that explains it, using the identifiers you already have.
This guide covers symptoms specific to the OID4VCI Pre-authorized Code flow. It assumes you know which events the flow emits, as described in Monitoring the OID4VCI Pre-authorized Code flow.
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.
An issuance spans three 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.
The holder says the offer no longer works
What you will find: OPENID_PRE_AUTHORIZED_OFFER_CREATE_SUCCESS for the offer, and no
OPENID_TOKEN_START carrying that offerId.
Three different causes produce this same signature:
- The offer expired. By default an offer expires 5 minutes after creation, and the maximum you
can configure is 10 minutes. Compare
data.expiresAtonOPENID_PRE_AUTHORIZED_OFFER_CREATE_SUCCESSagainst when the holder says they tried. - The offer was already claimed. Pre-authorized codes are single use. Once a credential has been
issued the code is consumed, and every later attempt fails, including by the same holder. Refer to
single-use offers. Look
for a
USER_CREDENTIAL_CREATE_SUCCESScarrying the sameofferId. If you find one, the offer was claimed successfully and the holder is retrying an old offer. - The offer was invalidated by too many wrong transaction codes. Covered below.
The fix: create a new offer. Offers cannot be extended or re-armed.
If holders hit this often, the usual cause is the gap between creating the offer and the holder opening it. Create the offer at the moment the holder is ready to claim it rather than in advance, and consider configuring a longer expiry.
The holder entered a transaction code and it was rejected
What you will find: OPENID_TOKEN_FAIL.
The behavior changes with the number of attempts:
| Attempt | error.type | Effect |
|---|---|---|
| 1 and 2 | invalid_tx_code | The offer remains claimable |
| 3 | invalid_grant | The offer is invalidated |
| 4 and later | No event | The offer no longer exists |
After three wrong attempts the offer is permanently invalidated, as described in accepting a credential offer. Every attempt after that is indistinguishable from an unknown code.
You will also see OPENID_TOKEN_FAIL with an error.type of invalid_request when a transaction
code was required but not supplied, or supplied when the offer did not require one.
The fix: create a new offer and deliver the transaction code again. If this happens frequently, check how you are communicating the code to holders.
The wallet obtained an access token but the credential request failed
What you will find: OPENID_TOKEN_SUCCESS, then nothing at all, not even
OPENID_CREDENTIAL_START.
The usual causes here are all rejected in front of the credential handler, so none of them reaches
OPENID_CREDENTIAL_FAIL. A missing OPENID_CREDENTIAL_START is the signal that the request was
turned away this early, and the HTTP response the wallet received is what tells the causes apart:
- The access token expired or was invalid. MATTR VII responds
400with{"error": "invalid_token"}. Comparedata.expires_inonOPENID_TOKEN_SUCCESS, and that event's timestamp, against when the holder says the wallet requested the credential. - The token scope does not cover the requested credential. MATTR VII responds
400with{"error": "insufficient_scope"}. - The wallet named a credential configuration your tenant does not have. MATTR VII responds
400with{"error": "unknown_credential_configuration"}.
The scope case is worth understanding because it fails late and quietly. If no credential
configuration matches the offer, MATTR VII still issues an access token, and it issues one with
no scope. The token exchange therefore looks entirely successful. The problem only surfaces at
the credential endpoint. If you see an OPENID_TOKEN_SUCCESS whose data.scope is absent or
empty, the offer referenced a credential configuration that MATTR VII could not resolve.
The fix: for expiry, have the wallet complete the exchange promptly. For scope, check that the
credentials array in your offer request references a
credential configuration that exists and is an
mDoc configuration. Because none of these reach the events, the response the wallet received is the
primary evidence, so ask the holder or the wallet vendor for it.
Your claims source did not respond usably
What you will find: OPENID_CLAIM_SOURCE_RETRIEVE_START, then
OPENID_CLAIM_SOURCE_RETRIEVE_FAIL, then OPENID_CREDENTIAL_FAIL on the same request ID.
Issuance depends on those claims, so a claims source that does not return usable data ends the whole credential request. The two sides of the exchange see different things, and you need both to work out what happened:
- What MATTR VII records.
OPENID_CLAIM_SOURCE_RETRIEVE_FAILputs the reason indata.error.type, which distinguishes MATTR VII being unable to reach your claims source at all, your claims source responding with a non-success status, and your claims source responding with something that is not a JSON object. The event also carriesclaimSourceId, andOPENID_CLAIM_SOURCE_RETRIEVE_SUCCESScarriesclaimSource.urlandclaimSource.requestMethod, so you can confirm which endpoint MATTR VII called. - What the wallet receives. MATTR VII responds to the credential request with
503and the body{"error": "service_unavailable"}, not an OAuth rejection. The holder sees a service error rather than being turned away. A claim that cannot be mapped returns400with{"error": "invalid_credential_request"}instead, so a holder reporting a service error points at the claims source rather than at your claim mappings.
The outbound call to your claims source carries the same x-request-id as the credential request
that triggered it. If you log that header on your claims source, you can match your own server logs
to the MATTR VII events directly.
The fix: check your claims source availability and its response format. If it uses OAuth, look
also for OPENID_CLAIM_SOURCE_AUTHORIZATION_FAIL.
Claims came back but the credential could not be built
What you will find: OPENID_CREDENTIAL_FAIL, with a message naming a claim that could not be
resolved.
This means the claims your source returned do not line up with what your credential configuration expects. The usual causes are a required claim that is absent from the source response, a claim that resolved to a different data type than the element expects, and a validity claim that is not an ISO 8601 string.
The fix: compare the claims your source returns against the claimMappings in your
credential configuration.
A certificate or validity period problem
What you will find: OPENID_CREDENTIAL_FAIL with a data.error.type of
InvalidCredentialRequestPayload and the message Failed to create mobile credential. The wallet
receives 400 with {"error": "invalid_credential_request"}.
That generic message is what separates this from a claim problem. A claim that could not be resolved
names the claim, so a bare Failed to create mobile credential points at the certificates MATTR VII
needed rather than at your data. The specific certificate reason is not carried on the event.
The causes fall into two groups:
- No usable certificate. No active IACA covered the requested credential validity, or there was no valid document signer under it.
- A validity period that cannot be satisfied. The credential would outlive the document signer, or falls outside the validity of any active IACA.
The fix: check certificate management. Confirm your IACA
is active and that its validity window covers the validUntil you are asking for. If the
certificates check out, send us the request ID, as described in
How to monitor a flow.
The credential was issued but the webhook never arrived
What you will find: OPENID_CREDENTIAL_SUCCESS, and then either WEBHOOK_EVENT_PROCESS_FAIL or
no webhook events at all.
WEBHOOK_EVENT_PROCESS_FAILmeans MATTR VII tried and your endpoint rejected the delivery or was unreachable. Retries appear as furtherWEBHOOK_EVENT_PROCESS_STARTandWEBHOOK_EVENT_PROCESS_FAILpairs rather than as a retry count on a single event.- No webhook events at all means the webhook was skipped. A disabled or deleted webhook produces no event whatsoever, so confirm the webhook still exists and is enabled.
Refer to Webhooks for delivery behavior and signature validation.
Nothing appears for the attempt
Work through these in order:
- Check the step. The
.well-knownmetadata fetch and the wallet reading the offer produce no events at all. Refer to what you can observe. - Check the credential format. Pre-authorized Code offers support mDocs only.
- Check the time window and the tenant. Confirm you are querying the tenant that issued the offer, 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