Skip to content
Last updated

Changelog

All notable changes to the DeepCredentials B2B API.

v2.4.0 — unreleased

Everything below is new since v2.3.0.

Breaking

  • Images at most 800 KB per credential — the image/jpeg claims of one credential may together be at most 800 KB decoded (was 4 MB per image). A credential is now delivered as a batch of up to ten copies, and a wallet takes at most 20 MB per batch. Offers, invitations and self-service requests over the limit are 400 invalid_argument.
  • POST /b2b/v1/verification-sessions verifies one credential definition, nothing else — credential_definition_id is required; case_id and allowed_issuer_dids are no longer taken, so a request that named only a case_id is now 400 invalid_argument (credential_definition_id is required). A verification case is launched at its own endpoint, POST /b2b/v1/verification-cases/{caseId}/sessions (see Added). A session trusts exactly the issuer DIDs pinned on its credential definition in X-Environment, reported in accepted_issuer_dids; there is no per-session allow-list. claims is optional: omitted or empty asks for every claim of the definition, where v2.3.0 refused an empty list.
  • The session resource has one shape (GET /b2b/v1/verification-sessions/{sessionId}, the list, and the verification.completed webhook's snapshot), whichever way the session was started. Removed: requested_fields, credential_subject_data, credential_subject_types, matched_vct, and credential_definition.vct. In their place:
    • case (id, version, title) on a session launched from a verification case, or credential_definition (id, name, has_logo) on a session created for a credential definition — one of the two, never both.
    • requested_attributes — what was asked for, each with its id, label, value_type, sensitive, constraints and bindings (keyed by credential definition id, never by vct).
    • attributes — on success, the disclosed values, keyed by the case's attribute ids or, on a credential-definition session, by claim keys. Value types are on requested_attributes[].value_type.
    • presented_credential_definition_id — on success, the credential definition of the credential the holder actually presented: one of the case's credential_definition_ids, or credential_definition.id on a credential-definition session.
  • A credential definition is used by its id, whoever owns it — POST /b2b/v1/verification-sessions and a verification case accept any organization's definition by id, not only your own and the official ones, and the new GET /b2b/v1/credential-definitions/{credentialDefinitionId} answers for any id. A definition id is not a secret (it is the vct of every credential of its type) and its schema is readable by protocol, so the id is what it takes. The list (GET /b2b/v1/credential-definitions) still shows your own and the official ones; its entries add own and is_listed, and is_listed means only that the owner lists the definition in the catalogue (other organizations' pickers in the portal) — it gates nothing.
  • is_public is renamed is_listed on credential definitions (GET /b2b/v1/credential-definitions): it means listed in the catalogue and nothing more — read below. A "public" flag implied a "private" that does not exist: a definition's id is in every credential and every QR code, and its schema is readable by protocol, listed or not.

Added

Verification cases

A new Verification Cases API and guide. A verification case is a rule: which credential definitions are accepted, which attributes are asked for, and the constraints their values must meet. It is authored once and launched by id.

  • Read — GET /b2b/v1/verification-cases lists your organization's cases (by default those that run in X-Environment and are not archived; filters availability=all, archived=exclude|only|include, reference_id). GET /b2b/v1/verification-cases/{caseId} reads one of yours, archived too, or any other organization's case by id (not archived; without its owner's reference_id). Requires deepcredentials.verify.
  • Author — POST /b2b/v1/verification-cases creates a case, PUT /b2b/v1/verification-cases/{caseId} replaces its contract and texts, PATCH /b2b/v1/verification-cases/{caseId}/archived archives or restores it. Requires deepcredentials.verification-cases. A case names its credentials as credential_definition_ids (your own, official ones, or any other organization's, by id; with several, the holder presents any one of them) and binds each attribute to a claim per definition id (bindings); an attribute's value_type may be omitted and is taken from the bound claims, and its label is one string for your staff. What a value must satisfy is grouped under the attribute's constraints (values, or min/max; a date bound may be relative, such as today-18y). What the holder reads is grouped under presentation: title (the verification screen's headline) and purpose (why the verification asks), each per language; a case has no separate internal description. In the swiyu Sandbox both are published with the case's request in the swiyu trust registry at its first launch. A case reads back in the shape it is written, and a session's requested_attributes carry the constraints the same way. POST documents 404 not_found for an unknown entry of credential_definition_ids.
  • Launch — POST /b2b/v1/verification-cases/{caseId}/sessions starts a session from a case, with optional case_version, reference_id and purpose. The case is the contract: the session asks for every attribute with its constraints, as authored; there is no per-session narrowing. The session is polled and reported like any other.
  • Your key — reference_id is your own identifier for what a case guards (a door, a zone), unique within your organization, archived cases included. A key in use is 409 already_exists with a message starting reference_id_conflict: and the holder's id in details.case_id.
  • Versions — version moves whenever what a case asks for changes, never on a rename. case_version on a launch runs the current version, or an older one the owner keeps open (launchable_versions, a migration window; keep_previous_launchable on an update opens it for the version it replaces); anything else is 409 aborted (case_version_mismatch:), as is a stale expected_version on an update or an update another save overtook.
  • Environments — a case runs wherever every credential it accepts is enabled, listed in environments. A launch in another environment is 400 failed_precondition with a message starting case_not_available: and details naming the environment and the definitions not enabled there, by id and name. A case whose date bounds have crossed since it was saved is refused at launch with 400 failed_precondition naming the attribute.
  • Any organization's case, by id — a case is run by whoever holds its id, as their own verification; the case stays its owner's to change, archive and grant. A case id is not a secret (it is in every QR code the case produces), and everything the case asks for is in the OpenID4VP request object any wallet fetches. Another organization's case reads in the same shape as your own — contract, bindings, presentation, launchable_versions, accepted_issuer_dids on its sessions — without the owner's reference_id; archived, it answers 404 to other organizations, on read and on launch. See Whose case.
  • What the verifier learns — a case session's attributes come back only when every constraint is met. A value that fails one closes the session as failed with attribute_value_rejected and one fixed error_description that names neither the attribute nor the value; nothing disclosed is stored.

Verification sessions

  • Same-device return — both session endpoints take an optional redirect_uri (absolute https, no fragment, no response_code). After the holder answers, the wallet offers a button back to it, with a fresh response_code. Such a session returns its attributes only to GET /b2b/v1/verification-sessions/{sessionId}?response_code=… with that code (valid 30 minutes; a wrong or expired code is 403 permission_denied). Without the code, and always in the list, attributes is left out and attributes_withheld is true. Sessions without redirect_uri are unchanged. See Same-Device Return.
  • case_id filter — GET /b2b/v1/verification-sessions?case_id=… lists the sessions your organization launched from one case, your own or another organization's.
  • POST /b2b/v1/verification-sessions documents 404 not_found for an unknown credential_definition_id, and 400 failed_precondition when no issuer of the definition is enabled in X-Environment.
  • Wallet errors speak the session's vocabulary — an error the holder's wallet reports is mapped before the session is stored: access_denied (the holder declined, or had nothing matching to present) reads as error_code: presentation_declined, a refusal of the request itself as request_rejected_by_wallet, anything else as wallet_error, each with an error_description written by the platform. The raw OAuth / OpenID4VP codes are no longer exposed. Every error_code a session can carry, with whether a new session can succeed, is listed under Session error_code values.

Credential definitions and issuance

  • GET /b2b/v1/credential-definitions/{credentialDefinitionId} reads one definition by id: your own, an official one, or any other organization's, listed in the catalogue or not. A definition id is not a secret (it is the vct of every credential of its type) and its schema is readable by protocol, so the id is what it takes. Definitions carry own and is_listed; is_listed means the definition is listed in the catalogue (other organizations' pickers in the portal), nothing more.
  • Name and description per language — definitions carry display (the name per language, base language first) and description (the description per language), in the {locale, name} shape a claim's display has. Your organization writes each language; name stays the name in the base language. The wallet shows the credential under the holder's language.
  • Images issued as data URLs — an image/jpeg claim is carried in the credential as a data URL (data:image/jpeg;base64,…), as the credential's rendering metadata declares and the swiyu wallets require to show it as a picture. Offers and invitations still take plain base64. A verification reports an image attribute as the wallet presented it, so for credentials issued from now on it is a data URL.
  • Batch delivery and background refill — a credential is delivered to the wallet as up to ten copies of the same data, and the wallet refills them for as long as the credential is active and not expired, with the same data and expiry; verifiers cannot link two presentations of it. Nothing changes in the API: one credential, one id, one status, which applies to every copy. See Copies in the wallet.
  • Richer claims — claims may carry description[], rules, options[] and sensitive; two new value types, date and enum. value_type is always one of string, integer, number, boolean, date, enum, image/jpeg; the legacy spellings are still accepted in requests.
  • vct is a URN minted by the platform — urn:vct:deepcredentials:<definition id> for definitions created from now on; treat it as opaque. Official types keep their identifiers.
  • Issuers per environment — GET /b2b/v1/credential-definitions lists the issuer DIDs pinned per definition (issuers). Offers and invitations refuse an environment the definition is not enabled in with 400 failed_precondition.
  • Lifetime as a day — valid_until (YYYY-MM-DD, end of day Europe/Zurich) on offers and invitations, mutually exclusive with validity_seconds; offers report credential_valid_until.
  • Validity claim — a definition may name one date claim as its validity_claim, filled from the credential lifetime; a request that sends it is refused.
  • Claim rules are enforced at issuance — offers, invitations and self-service reject a value that violates a claim's rules or options with 400 invalid_argument, details naming the claim and rule.
  • POST /b2b/v1/credential-offers/{offerId}/revoke withdraws a pending offer; its status becomes revoked.
  • issuing.status_changed reports revoked — an offer an operator withdrew (in the portal or through the revoke endpoint) is delivered with status: revoked, next to issued, failed and expired.
  • Swiss Beta-ID claims carry their real types (date, boolean, integer); self-service extra_fields use the claim shape (the old shape is still accepted on input).

Self-service

  • Requests end in revoked when their invitation or offer is withdrawn — an operator revoking it, or disabling the environment on the credential definition, used to leave the request at invitation_sent or issued for good. It now moves to the new terminal status revoked, and a new opt-in webhook, self_service.request.revoked ({ request_id }), is sent. Existing webhook configurations must enable the event in the portal.

Documentation

  • Every guide was checked against the API. Optional response fields are no longer marked as always present, offer statuses list revoked, the four swiyu.* system events that were delivered but undocumented are described, X-Webhook-Timestamp is documented as the unsigned delivery time, and valid_until is documented on invitations.

v2.3.0 — 2026-09-17

Breaking

  • is_system removed — GET /b2b/v1/credential-definitions no longer returns is_system, and it is no longer a required field of the credential definition schema. There are no system credential definitions any more: the platform's starter shapes (e.g. City Card) are templates in the portal's create wizard, not credential definitions, and have no id this API accepts.
  • You issue only from credential definitions your organization owns — POST /b2b/v1/credential-offers returns 404 not_found, and POST /b2b/v1/credential-invitations returns 400 invalid_argument, when credential_definition_id names an official credential definition or one that belongs to another organization — the same answers an unknown id gets. Previously any official, system or public credential definition was accepted. If you issued City Card as-is, create your own credential definition from the City Card template in the portal and use its id.

Changed

  • What the list holds — GET /b2b/v1/credential-definitions returns your organization's credential definitions plus the official ones (is_official: true), which are listed so you can verify against them. Other organizations' public credential definitions are no longer listed. Every entry with is_official: false is your own and issuable.
  • Self-service — a self-service configuration must be built on a credential definition your organization owns. The quickstart and the City Card walkthrough now start from the template.

v2.2.0 — 2026-08-18

Added

  • Credential Invitations API — Service users can create, list, get, rotate, and revoke invitations under /b2b/v1/credential-invitations. Create and rotate return the one-time plaintext code; list, get, and revoke expose code-free metadata.
  • credential_invitation.status_changed webhook — An opt-in terminal event for invitation redeemed, expired, and revoked transitions. The slim payload contains invitation_id, status, and offer_id only after redemption. Existing webhook configurations must enable the event in the portal.

Changed

  • Partner-owned invitation delivery — DeepCredentials does not email codes created or rotated through the B2B invitation API. The partner owns how it delivers the code and presents the redemption experience; its backend can exchange the recipient's email and code through the public redemption endpoint.
  • Progress callbacks clarified — Verification, invitation, and issuance webhooks are slim hints followed by authenticated B2B GET reconciliation. Webhook delivery currently makes one synchronous attempt and can be missed.
  • reference_id clarified — The field is optional correlation metadata only; it does not provide idempotency, uniqueness, or response caching.

v2.1.0 — 2026-06-09

Breaking

  • template_id → credential_definition_id — B2B request bodies and query parameters that previously accepted template_id now require credential_definition_id. Applies to POST /b2b/v1/verification-sessions, POST /b2b/v1/credential-offers, and the list query filters on those resources.
  • template → credential_definition in responses — The top-level response object previously named template is now named credential_definition. Inner fields (id, vct, name, has_logo) are unchanged. Terminology throughout the API is now "Credential Definition" rather than "Template".

Added

  • GET /b2b/v1/credential-definitions — Returns the credential definitions available to your organization. Any authenticated service user can call this endpoint (no additional scope required beyond a valid token with X-Org-Id and X-Environment).

Docs

  • "Open in API reference" deep-links — Every code sample in the guides now has an immediate deep-link to the matching operation in the rendered API reference.
  • Broadened intro — The home page and Quickstart now cover all three API pillars: verification, issuance, and self-service.

v2.0.0 — 2026-05-22

Breaking

  • Portal-issued credentials retired. API keys and OAuth clients minted in the DeepCredentials portal no longer work. Authentication now uses DeepAdmin service users authenticated against the DeepCloud SSO realm with the OAuth 2.0 Resource Owner Password Credentials grant. See the new Service Users guide.
  • POST /b2b/v1/oauth/token removed. Exchange credentials directly with POST <keycloak-realm>/protocol/openid-connect/token against deepcloud.swiss (prod) or int.deepcloud.swiss (test).
  • X-Org-Id and X-Environment headers are now required on every /b2b/v1/* request. X-Org-Id is the UUID of the organization the service user is a member of; X-Environment is beta or production.
  • X-Api-Key header is no longer accepted. Authenticate exclusively with Authorization: Bearer <access_token> from the DeepCloud SSO token endpoint.

Removed

  • API Keys and OAuth Clients management surfaces in the DeepCredentials portal.
  • Authentication API OpenAPI spec (the /b2b/v1/oauth/token operation is gone).

v1.4.0 — 2026-05-22

Changed

  • Scope strings renamed — B2B scopes are now namespaced under deepcredentials.*:

    • issuance → deepcredentials.issue
    • verification → deepcredentials.verify
    • self_service → deepcredentials.self-service

    Provision new credentials with the namespaced names. Enforcement will roll out together with the matching DeepAdmin update; until then, in-flight tokens carrying the legacy names continue to work.

v1.3.0 — 2026-05-20

Added

  • identification_filled claim source — Self-service configs can now mark a template claim as automatically pre-filled from the identification provider's verified attributes. Tenants configure the per-claim mapping in the portal (no B2B API surface). The webhook decision flow benefits passively: by the time self_service.request.submitted fires, the snapshot's claims field already reflects the merged result (user → identification → admin), so an empty admin_claims approve issues a credential carrying the identification-derived values.
  • editable_field_sources enum extended — GET /b2b/v1/self-service/requests/{id} may now return "identification_filled" alongside "user_filled" and "admin_filled". See the Self-Service Flow guide.

v1.2.0 — 2026-05-19

Added

  • Credential validity — Credentials are now issued with a tenant-controlled exp claim. Set a default on the credential template (portal → Templates) and override per-issuance via the new optional validity_seconds field on POST /b2b/v1/credential-offers. Send no value to inherit the template default; the credential is issued without an exp claim (never expires) only when both the template default and request override are absent. Self-service configs always carry their own explicit validity (or explicit "never expires") chosen at config creation. Allowed range: 60 seconds to 10 years. Issued-credential responses (GET /b2b/v1/issued-credentials/{id}, list, and webhook payload-fetch) now include valid_from and valid_until so you can show expiry in your UI and react before a credential lapses.
  • Self-service request snapshot — full claim schema — GET /b2b/v1/self-service/requests/{requestId} now returns editable_fields / editable_field_types / editable_field_sources describing every claim on the template (not just admin-filled ones). Enables rendering a "review & override" form without a second template fetch. See the Self-Service Flow guide.

Changed

  • Issued credential status semantics — The status field (active / suspended / revoked) is the revocation lifecycle (driven by the status list). Expiration is now expressed separately via valid_until. A credential can be active and past its valid_until simultaneously — wallets and verifiers reject it on the exp claim; we don't automatically flip its status.
  • admin_claims may override user-supplied claims — POST /b2b/v1/self-service/requests/{requestId}/decision already accepted any template claim key in the admin_claims payload; the contract is now explicit: when a key appears in both the user submission and your admin_claims, your value wins and is the one stamped on the credential. admin_filled_* continues to describe the required contract only.

v1.1.0 — 2026-05-16

Added

  • Self-Service API — GET /b2b/v1/self-service/requests/{requestId} returns the full request snapshot (status, captured claims, identification result, offer id, timestamps, plus the admin-claims schema — admin_filled_fields / admin_filled_types / admin_filled_required, mirroring how verification ships requested_fields + credential_subject_types so the tenant can dynamically render or validate admin_claims without hard-coded template knowledge). POST /b2b/v1/self-service/requests/{requestId}/decision approves or rejects an end-user-initiated request — required for tenants whose configs run in webhook verification mode.
  • Self-service webhook events — Six events covering the full lifecycle:
    • self_service.request.submitted — fires for webhook-mode configs when a request is ready for a decision.
    • self_service.request.approved / self_service.request.rejected — fire for all verification modes so you have an audit trail of every decision.
    • self_service.request.credential_issued — terminal success; the wallet accepted the credential.
    • self_service.request.offer_expired / self_service.request.failed — terminal post-approval failure paths (offer TTL exceeded, wallet rejected).
  • self_service scope — Required for the Self-Service API and selectable on API keys / OAuth clients in the portal.
  • Self-Service Flow guide — End-to-end walkthrough of the three verification modes (manual, webhook, automatic), lifecycle diagram, full user ↔ Portal ↔ tenant-backend sequence, and webhook payload contract.
  • Webhook subscription management — The portal webhook page now has a per-event checkbox grid grouped by family (Verification / Credential Issuance / Self-Service). Unchecked events are silently dropped at our edge — no delivery, no retry. New webhook configs default to all events on; existing configs keep their current set and have to explicitly opt in to events added in future releases.

Fixed

  • Scopes documentation — The scopes table previously listed credentials.read / credentials.write, which never existed. Replaced with the actual scopes: verification, issuance, self_service.

v1.0.0 — 2025-03-29

Added

  • Verification API — Create and poll OID4VP verification sessions (POST and GET /b2b/v1/verification-sessions).
  • Authentication API — OAuth 2.0 Client Credentials token exchange (POST /b2b/v1/oauth/token).
  • Health API — Credential validation and connectivity check (GET /b2b/v1/health).
  • Webhook Events API — OpenAPI 3.1 webhook spec for verification.completed with full event schema and signature headers.
  • Webhooks guide — Setup, HMAC-SHA256 signature verification, and secret rotation.
  • Authentication methods — API keys and OAuth 2.0.