Sessions
A session is EUDIPLO's record of one issuance or presentation flow. It ties the wallet's requests to the offer or request your backend created, stores the result, and enforces that every one-time value is used once. This page explains the rules that apply to both flows.
Statesβ
| Status | Issuance | Presentation |
|---|---|---|
active | Offer created | Request created |
fetched | First credential issued | Wallet fetched the request object |
completed | Wallet reported credential_accepted | Response verified |
failed | Wallet reported credential_failure or credential_deleted | Verification failed, or the wallet sent an error |
expired | Offer not redeemed before expiresAt | No response before expiresAt |
The flow diagrams are on Issuance under the hood and Presentation under the hood.
- Terminal states are final.
completed,failedandexpirednever change again. Redeeming the offer of a finished session, or answering its presentation request, is rejected and does not overwrite the result. - Expiry is checked when the wallet arrives. A presentation request expires after the configuration's
lifeTime(default 300 seconds). An offer expires afterofferLifetimeSecondsof the offer or the issuance configuration, and never if neither is set. The wallet-facing endpoints that redeem an offer (offer retrieval, PAR, authorization, token) or serve and answer a presentation request compareexpiresAtwith the current time, so an overdue session is rejected immediately (invalid_grantat the token endpoint, HTTP 400 or 404 elsewhere). The maintenance job only records theexpiredstatus afterwards. - Redeemed offers do not expire. Once the token exchange succeeded, the wallet can keep requesting credentials with its tokens;
expiresAtonly limits redemption. - Results are structured. A failed presentation stores a machine-readable failure code and an
outcomewith per-credential details (Session outcome). - Changes are observable. Each status change is published on the event stream
GET /api/session/{id}/events, which starts with the current status and ends after a terminal one, and in thesessionsmetric (Receive results, Monitoring).
Single useβ
Each one-time value is consumed with one conditional database update, so two concurrent requests cannot both succeed, even across backend instances.
| Value | Consumed when | Second use gets |
|---|---|---|
| Credential offer fetched by reference | First retrieval (unless ISSUER_MULTI_CONSUMPTION) | HTTP 404 |
Pushed request_uri | First authorization request | invalid_request_uri |
| Authorization code or pre-authorized code | First successful token request (the built-in server marks the session consumed) | invalid_grant |
| Credential proof nonce | First credential request that uses it | invalid_nonce |
DPoP proof (jti) | First request that uses it | invalid_dpop_proof or invalid_token |
| Ready deferred credential | First retrieval, with a token of the same session | invalid_transaction_id |
| OID4VP or ISO 18013-7 response | First response that completes or fails the session | HTTP 400 "The presentation offer has already been used" |
A pre-authorized code is also only valid until the session's creation time plus the tenant's session TTL, and a transaction code (tx_code) locks the code after too many wrong attempts (Credential offers).
Session binding (OID4VP Β§13.3)β
A presentation session has two identifiers, because the QR code is visible to anyone near the screen:
- The session ID is for your backend: the management API, webhooks and the event stream use it. Whoever has it can read the result.
- The
walletNonceis for the wallet: it appears inrequest_uri,response_uriand asstate. Seeing the QR code reveals only thewalletNonce, not the session ID.
Two more values bind the response to the request:
nonceis a random value in the request object. The wallet signs it into its key binding JWT or mdoc device authentication, so a presentation captured from another session cannot be replayed.response_codeprotects same-device flows. After a verified response, EUDIPLO creates a randomresponse_code, stores it asresponseCodeon the session and appends it to the redirect URI the wallet opens. Your page passes it to your backend, which compares it withresponseCodefromGET /api/session/{id}before it accepts the result for this browser.
Without the response_code, an attacker could start a request at your site, send its link to a victim, and pick up the victim's verified result in the attacker's own browser session. With it, only the browser that the victim's wallet redirected can claim the result, and a session completes once, so it has exactly one code.
ISO 18013-7 responses are posted by your own page with the session ID, so these values do not apply there. How to implement the check is described in Receive results; the specification text is in OID4VP Β§13.3.
Finding sessionsβ
GET /api/session lists the sessions of the caller's tenant, most recently updated first. All filters are optional, combined with AND, and always limited to the tenant and to the session types the client's roles allow (Tenants and access).
| Parameter | Matches |
|---|---|
createdFrom, createdTo | Creation time, bounds included. ISO 8601 with a time zone, for example 2026-10-02T08:00:00Z. |
updatedFrom, updatedTo | Time of the last change, for example sessions that completed or failed in the last hour. |
status | One or more states; repeat the parameter, for example status=active&status=fetched for pending sessions. |
type | issuance or presentation. |
credentialConfigurationId | Issuance sessions that offer this credential configuration. |
requestId | Presentation sessions of this presentation configuration. |
failureCode | Failed sessions with this failure code, for example trust_chain_not_trusted. |
id | Sessions whose ID starts with the value. |
q | Search, see below. |
sortBy, sortOrder | id, status, createdAt, updatedAt (default) or requestId; asc or desc (default). |
A range that starts after it ends, an unknown status or an ID longer than 255 characters answers 400.
GET /api/session?type=presentation&status=failed&createdFrom=2026-10-02T08:00:00Z&requestId=age-check&sortBy=updatedAt&sortOrder=desc
credentialConfigurationId only finds issuance sessions created with 9.x or later releases that store the offered configuration IDs next to the encrypted offer; older sessions are not backfilled.
Searchβ
q takes whatever identifier you have at hand, for example from a log line, a support request or a screenshot of a QR code:
- a session ID or its beginning,
- the
walletNonceof a presentation, - a pre-authorized code,
- the
referenceset when the offer or request was created, - a pasted link: a credential offer link (by reference or by value) finds its session, an OID4VP request link (
openid4vp://?β¦request_uri=β¦) finds the session of itswalletNonce.
The search term can contain a pre-authorized code, so its value is replaced by [redacted] in all log lines and in the url.query attribute of traces. The web client offers the same filters in the session list and keeps them in the URL, so a filtered view can be bookmarked or shared; its update-time presets (for example "Last hour") stay relative in a bookmark.
Your own referenceβ
POST /api/issuer/offer and POST /api/verifier/offer accept an optional reference, an identifier of your system such as an order or case ID (up to 255 characters). It is returned in the session list and detail, sent in the session's webhooks and found by q. The reference is stored in plaintext and stays when sessions are anonymized: never put personal data into it.
From a log line to the sessionβ
Once a wallet request has resolved its session (by offer ID, walletNonce, issuer_state, code or access token), every following log line of that request carries sessionId and tenantId, with or without OpenTelemetry, and the request's trace gets the session.id attribute. See Logging.
Session cleanupβ
Sessions contain personal data: claims, offers, authorization requests and presented credentials. Sensitive fields are encrypted at rest (Security model), and sessions are kept only for a retention period:
- A maintenance job runs every
SESSION_TIDY_UP_INTERVALseconds (default one hour). It marks overdue sessions asexpired, then processes every session older than the tenant's TTL, counted from creation (defaultSESSION_TTL, 24 hours). - In
fullmode (default) the session is deleted. - In
anonymizemode the session keeps its status, timestamps and protocol metadata, butcredentials,credentialPayload,auth_queries,offer,requestObjectandresponseEncryptionPrivateJwkare cleared. The plaintextreferenceandcredentialConfigurationIdsare kept.
The TTL also limits how long a pre-authorized code is valid. The global defaults are SESSION_TTL and SESSION_CLEANUP_MODE (environment variables); a tenant can override both through /api/session-config. Session log entries are stored separately and controlled by LOG_SESSION_STORE (Logging).