Skip to content
Last updated

Verification Flow

This guide explains the full lifecycle of an identity verification session — from creation to result retrieval.

Prerequisites

  • A service-user access token requested with the deepcredentials.verify scope (and deepcredentials.verification-cases to author cases). See Authentication.
  • Something to verify against — one of two:
    • A credential definition: one of your organization's own, an official one such as the Beta-ID, or any other organization's whose id you were given (a definition id is not a secret: it is the vct of every credential of its type). GET /b2b/v1/credential-definitions lists your own and the official ones; a definition another organization lists in the catalogue (is_listed) shows in the portal's pickers, and any definition reads by id with GET /b2b/v1/credential-definitions/{credentialDefinitionId}.
    • A verification case: a rule authored in the portal or by your integration, possibly accepting several credential types and constraining values. GET /b2b/v1/verification-cases lists your own; any organization's case is read and launched by its id. See Verification Cases.

Session lifecycle

POST /verification-sessions or /verification-cases/{id}/sessions

User presents valid credentials

Presentation rejected or invalid

Session TTL exceeded

pending

success

failed

expired

POST /verification-sessions or /verification-cases/{id}/sessions

User presents valid credentials

Presentation rejected or invalid

Session TTL exceeded

pending

success

failed

expired

Statuses

Status    Description
pendingSession created, waiting for user to scan QR and present credentials.
successCredentials verified successfully. attributes contains the disclosed values. Check credential_status too — a suspended or revoked credential still reports success here.
failedVerification failed. Check error_code and error_description for details.
expiredSession exceeded its TTL without completion.

Which one: a credential definition or a case

The rule is short:

  • Verify a credential definition when one credential type is enough and any value the holder presents is fine. You pick the claims per session; the disclosed values come back under the claim keys.
  • Use a verification case when the check accepts more than one credential type, or when a value must meet a constraint (an allowed set, a bound, an age). A case is the only place either is expressed: a definition session takes no constraints and one definition.

A case also gives you attribute ids that stay the same whichever credential was presented, a holder-facing title and purpose, versions, and one rule that several integrations or organizations run alike — useful, but not what decides.

Whose it is does not decide either. Any organization's credential definition or verification case is used by its id: if it is not yours, ask its owner for the id (it is not a secret — a definition id is the vct of every credential of its type, a case id is in every QR code the case produces). What you cannot do with someone else's is issue from the definition or change the case.

Step 1: Create a session

There are two ways to start a session, each with its own endpoint: verify a credential definition (POST /b2b/v1/verification-sessions) or launch a verification case (POST /b2b/v1/verification-cases/{caseId}/sessions). Either way the session is the same resource: polled with GET /b2b/v1/verification-sessions/{sessionId} and reported by the same webhook. What it ran is named on the session, under credential_definition or case, never both. Neither path takes an issuer list: a session trusts the issuers pinned on its credential definitions in X-Environment, reported in accepted_issuer_dids. Both take an optional redirect_uri, which brings a holder on their phone back to you after they answer; see Same-Device Return.

Verify a credential definition

Ask the holder for one credential of a given definition. claims picks the claims to request; omit it (or send it empty) to ask for every claim of the definition.

Open in API reference →

curl -i -X POST \
  https://api.dev.deepcredentials.swiss/b2b/v1/verification-sessions \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>' \
  -H 'Content-Type: application/json' \
  -H 'X-Environment: beta' \
  -H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08' \
  -d '{
    "claims": [
      "string"
    ],
    "credential_definition_id": "631a1b36-dbe6-4899-b427-3167b0844238",
    "purpose": "string",
    "redirect_uri": "string",
    "reference_id": "string"
  }'

The session names the definition under credential_definition (id, name, has_logo). On success, attributes is keyed by the claim keys of the definition, and presented_credential_definition_id is the definition's id.

Launch a verification case

Launch a case by its id, your own or any other organization's. The case is the contract: the session asks for every attribute of the case, with its constraints, exactly as authored; there is no per-session narrowing. purpose is optional and replaces the case's purpose for this run; case_version pins the contract version your integration was built against: the current one runs, an older one runs while the case lists it in launchable_versions, anything else is 409 case_version_mismatch (see Versions). The case must run in X-Environment (400 case_not_available otherwise). The session names the case under case (id, the version it ran, the case's title). On success, attributes is keyed by the case's attribute ids, whichever accepted credential the holder presented, and presented_credential_definition_id says which of the case's credential_definition_ids it was; a value that fails a constraint closes the session as failed with attribute_value_rejected and nothing disclosed is stored. See Verification Cases for what a case is and how to author one.

Open in API reference →

curl -i -X POST \
  'https://api.dev.deepcredentials.swiss/b2b/v1/verification-cases/{caseId}/sessions' \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>' \
  -H 'Content-Type: application/json' \
  -H 'X-Environment: beta' \
  -H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08' \
  -d '{
    "case_version": 1,
    "reference_id": "door-7B-2026-09-29-0815",
    "purpose": "Access to the server room"
  }'

Response

Both endpoints answer with the launch: the session id, the QR code, the deeplink, the status and the expiry.

session_idstring, (uuid)required

UUID v4 identifier.

Example:"550e8400-e29b-41d4-a716-446655440000"
qr_content_base64stringrequired

Base64-encoded PNG; QR payload is the same URI as deeplink.

deeplinkstringrequired

Wallet app link, e.g. openid4vp://?client_id=...&request_uri=...

statusstring(verification.SessionStatus)required

Lifecycle state of a verification session.

Enum:"pending""success""failed""expired"
expires_atstring, (date-time)required

Session TTL deadline (RFC3339).

Step 2: Present the QR code

You have two options to present the verification request to the user:

QR code (desktop / in-person)

The qr_content_base64 field is a standard Base64-encoded PNG image. Display it directly:

<img src="data:image/png;base64,iVBORw0KGgo..." alt="Scan to verify your identity" />

Or decode it to a file:

import base64

png_bytes = base64.b64decode(qr_content_base64)
with open("qr.png", "wb") as f:
    f.write(png_bytes)

The deeplink field contains a URI that opens the wallet app directly (its scheme, openid4vp or swiyu-verify, depends on the deployment):

swiyu-verify://?client_id=...&request_uri=...

Use this when the user is on a mobile device — redirect them to the deeplink instead of showing a QR code. To have the wallet bring them back to you once they have answered, create the session with redirect_uri: see Same-Device Return.

Step 3: Retrieve the result

Option A: Polling

Poll GET /b2b/v1/verification-sessions/{sessionId} until the status is no longer pending:

Open in API reference →

curl -i -X GET \
  'https://api.dev.deepcredentials.swiss/b2b/v1/verification-sessions/{sessionId}?response_code=string' \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>' \
  -H 'X-Environment: beta' \
  -H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08'

Response

session_idstring, (uuid)required

Session id (matches the value returned at creation time). UUID v4 identifier.

Example:"550e8400-e29b-41d4-a716-446655440000"
statusstring(verification.SessionStatus)required

Current lifecycle state of the session.

Enum:"pending""success""failed""expired"
expires_atstring, (date-time)required

When a pending session expires if not completed (RFC3339).

environmentstringrequired

Swiyu environment: "beta" or "production" (matches the credential binding).

created_atstring, (date-time)required

When the session was created (RFC3339).

accepted_issuer_didsArray of strings

The issuer DIDs this session trusted: those pinned on its credential definitions in its environment, every accepted credential's together. A credential from any other issuer is refused.

attributesobject

On success: the disclosed values keyed by attribute id — the case's attribute ids, the same keys whichever accepted credential the holder presented, or the claim keys on a credential-definition session. Absent for sessions of cases launched before attributes existed.

attributes_withheldboolean

True when the session has disclosed values that this read leaves out: a session created with `redirect_uri` returns `attributes` only to GET /b2b/v1/verification-sessions/{sessionId} with the `response_code` the wallet's redirect carried, and never in the list.

caseobject(b2b.sessionCaseRef)

The verification case this session ran, only on a session launched from a case (POST /b2b/v1/verification-cases/{caseId}/sessions). Exactly one of `case` and `credential_definition` is present.

completed_atstring, (date-time)

Set when the session leaves pending (success, failed, or expired).

credential_definitionobject(b2b.sessionCredentialDefinitionRef)

The credential definition this session verifies, only on a session created from one (POST /b2b/v1/verification-sessions). Exactly one of `case` and `credential_definition` is present.

credential_statusstring

On success: the token-status-list state of the credential the holder presented — "valid", "suspended" (shown as "Locked" in the portal), or "revoked". Resolved when the holder presented and never recomputed.

A "suspended" or "revoked" value still accompanies a successful verification with its disclosed claims: the wallet can present such a credential and the holder can consent to sharing it, so the verifier is given the state and applies its own policy. Branch on this in addition to `status` if a locked credential should not be accepted.

deeplinkstring

Only when status is pending: wallet deep link (e.g. openid4vp://...).

error_codestring

Machine-readable failure reason when status is failed or expired: one of the session error codes listed in the Verification Flow guide ("Session error_code values"), each with whether launching a new session can succeed. A wallet's own error is mapped onto that vocabulary (presentation_declined, request_rejected_by_wallet, wallet_error); the raw OAuth code is never exposed.

error_descriptionstring

Human-readable error detail when status is failed or expired, written by the platform for the code. On attribute_value_rejected it is one fixed sentence that names neither the attribute nor the value: which rule failed is what the case gates on.

issuer_trustobject(b2b.b2bIssuerTrust)

On success: on what basis the presented credential's issuer was accepted. `method` is "trusted_authority" when the issuer is one of `accepted_issuer_dids`, or "trust_protocol_2" when the session pins none (Sandbox only) and the swiyu trust registry vouches for the issuer; the other fields are that registry's trust marks.

presented_credential_definition_idstring, (uuid)

On success: the credential definition of the credential the holder actually presented, one of the case's `credential_definition_ids`. On a credential-definition session it is `credential_definition.id`.

Example:"550e8400-e29b-41d4-a716-446655440000"
purposestring

Purpose string from the create request, if any.

qr_content_base64string

Only when status is pending: Base64 PNG QR code for the wallet.

redirect_uristring

The same-device redirect the session was created with, if any.

reference_idstring

Your reference_id from the create request, if any.

requested_attributesArray of objects(b2b.b2bSessionAttribute)

The attributes this session asked for, as launched: the case's, or the definition's claims on a direct session. Bindings are keyed by credential definition id, as on the case.

A session reads the same whichever way it was launched. What it ran is under case (id, version, title) on a session launched from a case, or credential_definition (id, name, has_logo) on a session created for a definition: exactly one of the two is present. GET /b2b/v1/verification-sessions?case_id=… lists the sessions your organization launched from one case, your own or another organization's.

While status is pending, the response carries qr_content_base64 and deeplink next to what the session asked for, and no disclosed data. On success, attributes holds the disclosed values keyed by attribute id — on a case session the case's attribute ids, the same keys whichever accepted credential the holder presented; on a credential-definition session the claim keys — and presented_credential_definition_id names the credential definition of the credential the holder actually presented (one of the case's credential_definition_ids; on a credential-definition session, credential_definition.id); qr_content_base64/deeplink are omitted. requested_attributes echoes what the session asked for, each with its value_type, sensitive and constraints, and its bindings keyed by credential definition id exactly as on the case (on a credential-definition session, the one definition's id). A session reads the same whichever organization's case it ran (see Whose case).

A value that fails one of an attribute's constraints (values, or min/max) fails the session with error_code: "attribute_value_rejected" and the fixed error_description described in Verification Cases. The constraint is enforced by the verifier after the wallet's response, so treat it as authoritative and attributes as already checked.

A session created with redirect_uri returns attributes only to a read that passes the response_code the wallet's redirect carried; without it, attributes is left out and attributes_withheld is true, and the list never carries them. See Same-Device Return.

On success, credential_status reports the token-status-list state of the credential the holder actually presented. Values: valid, suspended, or revoked.

A locked or revoked credential still verifies successfully

suspended and revoked arrive with status: "success" and the full attributes. This is not a bug: a wallet can present a locked or revoked credential and the holder can consent to sharing it, so the presentation genuinely verified — the issuer has simply since withdrawn the credential's validity.

Checking status == "success" alone will accept a withdrawn credential. If your integration must not accept one, branch on credential_status in addition to status.

Note

When status is no longer pending, qr_content_base64 and deeplink are omitted from the response — the wallet flow is complete.

Polling recommendations:

  • Poll every 2–3 seconds.
  • Stop when status is success, failed, or expired.
  • Respect the expires_at timestamp — stop polling after expiration.

Option B: Webhooks

For production integrations, opt in to the Verification Completed webhook event. Its data field is a slim terminal hint containing session_id, status, and, on successful status resolution, credential_status. Follow it with authenticated GET /b2b/v1/verification-sessions/{sessionId} to reconcile the authoritative session and read disclosed fields. The callback is not a copy of the GET response.

See the Webhooks guide for setup, signature verification, and secret rotation.

Error handling

On failure: the same response object is returned with status: failed and error_code/error_description populated. See the full schema above. error_code is always one of the values below, and error_description is written by the platform for that code: what the holder's wallet said is never passed through.

Session error_code values

The wallet reports the holder's side of the exchange with the OpenID4VP error codes, and the platform folds them into this vocabulary before the session is stored: access_denied becomes presentation_declined, a refusal of the request itself becomes request_rejected_by_wallet, and anything else wallet_error. Branch on error_code; the third column says whether launching a new session for the same holder can succeed, all else being equal.

error_codeMeaningCan a new session succeed?
presentation_declinedThe holder closed the request without presenting a credential: they declined, or had nothing matching to present. The swiyu wallet does not distinguish the two, by design.Yes — ask the holder again.
session_expiredThe session passed expires_at while still pending (status: expired).Yes.
credential_not_acceptedThe holder presented a credential type the session does not accept.Not with that credential; yes if the holder holds one the case accepts.
issuer_not_trustedThe credential was issued by a DID the session does not trust: not one of accepted_issuer_dids, or, on a Sandbox session that pins none, not an issuer the swiyu trust registry vouches for (see issuer_trust).No, until the issuers pinned on the credential definition change, or the holder obtains a credential from a trusted issuer.
issuer_trust_unavailableThe session pins no issuer, and the swiyu trust registry could not be asked about this one.Maybe later, once the trust registry answers.
attribute_value_rejectedA disclosed value did not satisfy one of the case's constraints. error_description is one fixed sentence that names neither the attribute nor the value.No, for this holder: the case's rule is what failed.
verification_failedThe credential's signature, holder binding or nonce did not verify.No, with this credential.
status_check_failedThe credential's token-status-list entry could not be fetched or its signature could not be verified. credential_status is absent.Maybe later — see the note below; the cause is on the issuer's side.
did_resolution_failedThe issuer's DID could not be resolved.Maybe later, once the DID registry answers.
key_extraction_failedThe issuer's DID document does not carry the key the credential is signed with.Maybe later, once the issuer's DID document lists the key again.
invalid_vp_tokenThe wallet's vp_token could not be parsed, or names a credential query the presented credential does not answer.Maybe — a wallet fault; report it if it recurs.
response_read_failedThe wallet's response could not be decrypted or parsed, or the wallet sent the presentation unencrypted.Maybe — a wallet fault; report it if it recurs.
request_rejected_by_walletThe wallet did not accept the verification request (error_description names its code, e.g. invalid_request, vp_formats_not_supported).No: nothing the holder or your integration can change; report it.
wallet_errorThe wallet reported an error the platform has no closer reading for (error_description names its code, e.g. server_error, temporarily_unavailable).Maybe later.
invalid_request_objectThe platform's own stored request object was unusable. Internal.Yes — a new session builds a new request; report it if it recurs.
claims_parse_failedThe verified claims could not be decoded. Internal.Maybe; report it if it recurs.

On a status-check failure: if the credential's token-status-list entry can't be fetched or its signature can't be verified, the session fails with error_code: status_check_failed rather than being reported with an unknown or stale state. credential_status is absent on such a session.

Status-check failures can originate from issuer infrastructure

A status_check_failed rejection may result from problems with the issuer's status-list infrastructure, not from a cryptographically invalid credential. For example, the issuer rotates its signing key and republishes the status list under the new key — credentials issued before the rotation reference the old key and can no longer have their list verified — or the issuer issues credentials with no status reference at all. In both cases, a valid credential is rejected and your session returns no claims. This is especially relevant when verifying credentials from issuers you do not operate.

On expiration:

Sessions that remain pending past their expires_at transition to expired automatically.

See Error Handling for a complete list of error codes.

Next steps

  • Same-Device Return — Bring a holder on their phone back to you, and bind the result to their browser.
  • Webhooks — Receive verification results in real time.
  • Error Handling — Handle all possible error scenarios.