A verification case is a rule: which credentials are accepted, which attributes are asked for, and the constraints their values must meet. Launching a case starts a verification session that asks for exactly that; the holder's data comes back only when every constraint is met. Cases are made in the portal by people, or by your integration over this API, typically one per object you guard: a door, a zone, a ticket category.
You need a case when a check accepts more than one credential type, or when a presented value must meet a constraint; a check with one credential type and no constraints is a plain credential definition session. Any organization's case is launched by its id: if the case is not yours, ask its owner for the id.
Reading and launching cases requires the deepcredentials.verify scope; creating, changing and archiving them requires deepcredentials.verification-cases. See Authentication.
A case accepts one or more credential definitions (credential_definition_ids): your organization's own, official ones such as the Beta-ID, or any other organization's, by id. With several, the holder presents any one of them.
Its requested attributes are what a session answers with. Each attribute has its own id and is bound to a claim in every accepted credential (bindings, keyed by credential definition id), so a session answers with the same keys whichever credential the holder presented. A session's own requested_attributes carry the same bindings, keyed by credential definition id. Where the credentials name the same fact differently, the bindings say so: {"<badge id>": "zone", "<pass id>": "area"}. What a presented value must satisfy sits under the attribute's constraints: values (allowed values, for text, choice and yes/no attributes) or min/max (bounds, for numbers and dates, where a date may be relative such as today-18y). An attribute without constraints is only asked for; one with them is checked by the verifier. values, min or max at the attribute's top level are refused with 400 invalid_argument, so a check is never silently dropped.
An attribute takes the type of the claims it is bound to, and the type decides which fields its constraints can carry:
value_type of the bound claims | Constraints |
|---|---|
string | values: the allowed strings |
enum | values: keys from the claim's options (a key that is not an option is refused) |
boolean | values: [true] or [false] |
integer, number | min, max: numbers, inclusive |
date | min, max: an ISO date (2000-01-01) or a date relative to the day of verification (today, today-18y, today+30d, today-6m), inclusive. "constraints": {"max": "today-18y"} on a birth date means at least 18 years old |
image/jpeg | none: it can be asked for, not constrained |
An attribute without constraints just asks for the value. A claim's own rules in the credential definition (pattern, min_length and the like) are checked when a credential is issued and are not constraints of a case.
- Read the definition with
GET /b2b/v1/credential-definitions/{credentialDefinitionId}(your own, an official one, or any other organization's: a definition id is not a secret, it is thevctof every credential of its type). Itsclaimsmap claim keys to theirvalue_type,displaynames and, for anenum, itsoptions. - For each fact the case asks for, add an attribute with your own
id, bind it to the claim key in every accepted definition, and add the constraints its type allows. - Check the definitions'
environments: the case runs only where all of them are enabled.
A case is run by its id. It is one of your organization's own, or any other organization's whose id you were given: there is nothing to switch on, and either way it stays its owner's to change; you read and launch it. A case id is not a secret: it is in every QR code the case produces, and everything the case asks for (its accepted credential definitions, the claim paths it requests, the issuers it trusts) is in the OpenID4VP request object every launch produces, which any wallet fetches unauthenticated. Holding the id and the contract is what a verifier has anyway, so the platform does not pretend otherwise.
Another organization's case reads in the same shape as your own — its contract (accepted credentials, requested attributes with their bindings and constraints, version, launchable_versions to pin against, environments) and what the holder reads (name, presentation) — without the owner's own bookkeeping: reference_id is left out, and it reads only while it is not archived. A session you launch from it reads exactly like one of your own case, bindings and accepted_issuer_dids included. GET /b2b/v1/verification-cases lists only your organization's own cases; there is no catalogue of other organizations' cases.
What stays the owner's: changing the case (its contract, texts and reference_id), archiving it, granting it to its members, and seeing which organizations ran it. What the owner cannot do is keep anyone who holds the id from running it, short of archiving it.
A case has no environment of its own: it runs wherever every credential it accepts is enabled, listed in its environments. GET /b2b/v1/verification-cases lists the cases that run in X-Environment; add availability=all to see every case with its environments.
A case's version moves whenever what it asks for changes (credentials, attributes, constraints), never on a rename or a new purpose. Every session records the case and the version it ran, under case (id, version).
Pin the version your integration was built against with case_version on a launch. A pin runs:
- the current version, always;
- an older version while it is listed in the case's
launchable_versions. The owner can keep the version it replaces open when saving a new one (keep_previous_launchable), and close it later: a migration window, so integrations move to the new rule at their own pace. The launch then runs that older version's contract (its credentials, attributes and constraints) with the case's current texts.
Any other pin, an older version the owner has not kept open or a version that does not exist, is refused with 409 aborted and case_version_mismatch: re-read the case and upgrade deliberately. Without case_version, a launch always runs the current version.
A case its owner archived answers 404 not_found on launch, the same as an id that never existed. On read, another organization's archived case is 404 too; your own archived case still reads, with archived_at, so you can restore it.
A session's attributes come back only when every constraint of the case is met. A disclosed value that fails one closes the session as failed with error_code: attribute_value_rejected and a fixed error_description that names neither the attribute nor the value, and nothing the holder disclosed is stored: which rule failed is what the case gates on.
POST /b2b/v1/verification-cases/{caseId}/sessions starts a session in X-Environment, optionally pinned to a case_version. A case that does not run in X-Environment is refused with 400 failed_precondition, a message starting case_not_available: and details naming the environment and the credentials not enabled there (see Error Handling), by id and name, whoever's case it is. A case whose date bounds have crossed since it was saved (a relative min such as today-10y that has overtaken an absolute max) cannot be passed by anyone: its launch is refused with 400 failed_precondition naming the attribute, until the case's author changes the bounds. The session is polled and reported like any other; see Verification Flow. A launch takes redirect_uri like any session: see Same-Device Return.
With the deepcredentials.verification-cases scope your integration can author its own cases instead of having them made in the portal: typically one per access object, a door or a zone, with the rule that opens it. Key each case with your own reference_id and find it again with GET /b2b/v1/verification-cases?reference_id=door-7B (the list's defaults still apply: add availability=all and archived=include to find the case wherever it runs and whatever its state).
POST /b2b/v1/verification-cases
{
"reference_id": "door-7B",
"name": "Door 7B",
"presentation": {
"title": [{ "locale": "de", "name": "Zutritt Serverraum" }, { "locale": "en", "name": "Server room access" }],
"purpose": [{ "locale": "de", "name": "Zutritt zum Serverraum" }, { "locale": "en", "name": "Access to the server room" }]
},
"credential_definition_ids": ["5b0c…"],
"requested_attributes": [
{ "id": "zone", "constraints": { "values": ["server-room"] },
"bindings": { "5b0c…": "zone" } }
]
}Every attribute needs a binding for every accepted credential, to a claim of the same type in each. value_type may be omitted: it is taken from the bound claims. label is an optional display name for your staff (the kiosk result and the session detail in the portal); the holder never sees it, their wallet shows its own claim labels. Without one, the bound claim's label is shown. What the holder reads is under presentation: title, the headline of the verification screen, and purpose, why the verification asks, each one entry per language (at most 1000 characters per language for purpose). A launch may give a different purpose for one run, as one string. Neither is part of the contract, so changing them never moves the version.
In the swiyu Sandbox, a case's request is published in the swiyu trust registry when it is first launched (a Verification Query Public Statement): its title as the name, its purpose as the description, the credentials and attributes it asks for, and the allowed values of their constraints. Anyone can read what is published; a launch's own one-run purpose is not published. A case reads back in the same shape, so you can change what you read and send it back.
- Create answers with the case at
version1 and theenvironmentsit runs in; credentials that share no environment are refused with400 invalid_argument. Areference_idalready held by another of your cases is409 already_exists(reference_id_conflict), with that case's id indetails.case_id. - Update (
PUT /b2b/v1/verification-cases/{caseId}) replaces the contract and texts. Sendexpected_versionwith the version you read: if someone changed the case since (a person in the portal, another instance of your integration), the update is refused with409 aborted(case_version_mismatch) instead of overwriting their change; a save that lands between your read and your write is refused the same way. Omittingreference_idkeeps the current key. The version moves when what the case asks for changes; launches that pin the old version (case_version) are then refused until they upgrade. - Archive (
PATCH …/archived {"archived": true}) when the object goes away: launches answer404, updates400until you restore it with{"archived": false}. Sessions already run stay as they were. - A case has no environment to set: it runs wherever its credentials are enabled. On an update that moves the version,
keep_previous_launchablekeeps the replaced version inlaunchable_versionsfor integrations that pin it. - Cases you create are ordinary cases: they appear in the portal, where a manager can read and edit them, and the audit log names your service user as the author.