Receive Results
After you create a presentation request, the result arrives in the session whose ID the request returned. Your backend can be called by a webhook, follow a stream of status events, or poll the session; same-device flows also return the user's browser to you. States, failure codes and the outcome structure are listed in the session outcome reference.
| Method | Use when |
|---|---|
| Webhook | Your backend should be told about every completed or failed presentation. |
| Server-Sent Events | A page or service waits for one session, for example next to a QR code. |
| Polling | You cannot receive webhooks or keep a stream open. |
| Same-device redirect | The wallet runs on the same device and should return the user to your page. |
Webhookโ
EUDIPLO calls a webhook when a presentation reaches completed or failed. It uses the inline webhook of the request if present, otherwise the webhook endpoint referenced by the configuration's webhookEndpointId (Configure Verification). Expiry does not trigger a webhook.
The body always contains status, outcome and session, plus transaction_data if the request used it. Only a completed presentation also carries credentials, the disclosed claims per DCQL credential query ID:
{
"status": "completed",
"outcome": { "result": "success", "credentials": [{ "id": "membership", "verified": true }] },
"credentials": [{ "id": "membership", "values": [{ "name": "Max", "member_id": "M-001" }] }],
"session": "3f0c1d9e-4c1b-4f63-9a59-2f4f0b6a2c11"
}
A failed presentation, for example because the user declined, sends "status": "failed" with the reason in outcome and no credentials. The complete payload is described in Webhooks.
- Raw tokens: list credential query IDs in
includeRawTokensForof an inline requestwebhookto also receive the presented token (for example the SD-JWT) asrawToken. Webhook endpoints have no such option. - Redirect override: answer with
{ "redirectUri": "https://shop.example.com/done" }to send the user there instead of the configuredredirectUri. This works for completed and, for OpenID4VP, failed presentations. - Delivery: EUDIPLO sends one request and does not retry. A failed delivery is logged and does not change the session, so reconcile missed results by polling. Webhook URLs must pass the outbound URL policy.
Server-Sent Eventsโ
Subscribe from your backend to GET /api/session/{id}/events with the access token in the Authorization header. The stream is authorized like GET /api/session/{id}, so presentation sessions need presentation:request. Tokens in the URL are not accepted, which keeps management tokens out of access logs. The browser's EventSource cannot send headers: read the stream in your backend and pass the status on to your page, and never hand the management token to the browser.
curl -N "$EUDIPLO/api/session/$SESSION/events" -H "Authorization: Bearer $TOKEN"
With @eudiplo/sdk-core, subscribeToSession() reads the stream with fetch and the client's token:
const subscription = await client.subscribeToSession(sessionId, {
onStatusChange: ({ status }) => {
if (["completed", "failed", "expired"].includes(status)) {
// Read the result with GET /api/session/{id} and tell your page.
}
},
});
- The first event carries the current status, so a late subscriber does not miss a result.
- Each event has the form
{ "id": "<session id>", "status": "fetched", "updatedAt": "<ISO timestamp>" }; every status is sent once. - The stream ends after
completed,failedorexpired. Changes processed by another replica arrive within a few seconds. - A missing or invalid token returns
401, a token without a session role403, an unknown session or one of the other type than the client's roles cover404.
Pollingโ
Read the session with GET /api/session/{id}. The caller needs the presentation:request role; a client with only issuance roles gets 404 for presentation sessions. Poll every one or two seconds until status is completed, failed or expired:
{
"id": "3f0c1d9e-4c1b-4f63-9a59-2f4f0b6a2c11",
"status": "completed",
"requestId": "membership-check",
"expiresAt": "2026-10-03T10:05:00.000Z",
"consumedAt": "2026-10-03T10:01:12.000Z",
"responseCode": "6b1f0d0e-1c4e-4a54-8f7e-0b8f1c2d3e4f",
"credentials": [{ "id": "membership", "values": [{ "name": "Max", "member_id": "M-001" }] }],
"outcome": { "result": "success", "credentials": [{ "id": "membership", "verified": true }] }
}
The response contains further session fields; the result fields are explained in the session outcome reference. A request that runs out its lifetime is rejected immediately, but its status changes to expired only when the session maintenance job runs (SESSION_TIDY_UP_INTERVAL, default one hour). Stop waiting once expiresAt has passed.
Same-device redirectโ
When the request has a redirectUri, the wallet sends the user's browser back to it after the presentation. EUDIPLO replaces {sessionId} and appends a one-time response_code:
https://shop.example.com/verified?session=3f0c1d9e-4c1b-4f63-9a59-2f4f0b6a2c11&response_code=6b1f0d0e-1c4e-4a54-8f7e-0b8f1c2d3e4f
Before you accept the result for this browser:
- Read the session with
GET /api/session/{id}from your backend. - Check that
statusiscompletedand thatresponseCodeequals theresponse_codefrom the URL. - Only then attach the verified claims to the browser's session.
The check proves that this browser received the redirect, so an attacker cannot make a victim complete a session the attacker started (OID4VP ยง13.3). EUDIPLO has no lookup by response code; always start from the session ID you stored when you created the request.
If the presentation fails, the redirect carries error and error_description instead of a response_code. A declined request uses the wallet's error code, such as access_denied; a failed verification uses invalid_request. ISO 18013-7 requests redirect only after success.
Requests are single-use, and sessions are deleted or anonymized after the tenant's retention time; see Sessions.