Skip to content
Last updated

Walkthrough — City Card

A concrete end-to-end example of the self-service flow. Built around the City Card system credential definition, this is what you'd show in an integration meeting: one form, one webhook, one approve call.

If you're a developer and want the smallest possible setup, see Quickstart first. If you need the full state machine, every event payload and edge case, see Detailed Reference.

Scenario

The City of Springfield wants to issue a digital City Card to verified residents. The card carries the resident's name and date of birth, signed by the city, and lives in their wallet — usable for library lookups, public transport, age checks, and so on.

The constraint: residents shouldn't have to type anything. They should be able to prove they're who they say they are through DeepID — picking whichever identification method suits them at the time — and the card should drop into their wallet seconds later.

The interaction

Only the cross-system parts. Everything inside DeepCredentials and DeepID (identification UI, method choice on the DeepID side, OID4VP exchange with the wallet, state machine) is intentionally hidden — that's our problem to solve.

Your BackendDeepCredentialsResidentYour BackendDeepCredentialsResidentClaims are pre-filledfrom the verified attributesOpen share-link, complete DeepID identification1POST /your-webhookself_service.request.submitted2GET /b2b/v1/self-service/requests/{id}3200 — snapshot with pre-filled claims4POST /decision { decision: "approve" }5City Card lands in wallet6
Your BackendDeepCredentialsResidentYour BackendDeepCredentialsResidentClaims are pre-filledfrom the verified attributesOpen share-link, complete DeepID identification1POST /your-webhookself_service.request.submitted2GET /b2b/v1/self-service/requests/{id}3200 — snapshot with pre-filled claims4POST /decision { decision: "approve" }5City Card lands in wallet6

Three roundtrips total. Anything more complex — identification failure, rejection, expiry, retries — is a variation of the same shape and is covered in the Detailed Reference.

What the resident sees

  1. Opens the share-link Springfield published on its website.
  2. Hands off to DeepID, picks an identification method available there (e-ID, QES, passport-based, …), completes it.
  3. Returns to the share-link page; a moment later a wallet deeplink appears (or arrives by email). Card is in the wallet.

No form to fill in. No data to type.

What Springfield does — once, in the portal

No code. Three UI tasks:

  1. Pick the credential definition. Credential Definitions → System Credential DefinitionsCity Card. (Already platform-provided. If you wanted a custom shape you'd clone an official credential definition instead.)
  2. Create the self-service config. Self-Service → New config:
    • Credential Definition — City Card.
    • Verification mode — webhook.
    • Identification — required, via DeepID. Pick the methods you'll accept (QES, Swiss e-ID passport, …); DeepID surfaces the choice to the resident at runtime and returns a canonical set of verified attributes regardless of which one they used.
    • Claim mapping — for each City Card claim, pick the identification attribute it draws from:
      • given_namegiven_name
      • family_namefamily_name
      • birth_datebirth_date
    • Credential validity2 years.
  3. Register a webhook in the DeepCredentials portal under Settings → Webhooks and subscribe to at least self_service.request.submitted and self_service.request.credential_issued. For the runtime B2B calls, use a Bearer token obtained for a DeepAdmin service user through DeepCloud with the deepcredentials.self-service scope; DeepCredentials does not mint an API key.

That's the entire setup. The share-link is now live at whatever slug you chose.

What Springfield's backend does — per request, in code

Three things, in this order: verify the webhook signature, fetch the snapshot, post the decision. With pre-filled identification claims and no admin-fill fields on this credential definition, the backend doesn't need to provide any data of its own — a passive approve is enough.

@app.post("/webhooks/deepcredentials")
def handle_webhook(req):
    # 1. Verify the HMAC signature (see Webhooks guide for the helper).
    verify_signature(req)

    if req.json["event"] != "self_service.request.submitted":
        return 200  # ignore other events for now

    request_id = req.json["data"]["request_id"]

    # 2. Fetch the snapshot — claims are already pre-filled from DeepID.
    snapshot = dc.get(f"/b2b/v1/self-service/requests/{request_id}").json()

    # (Optional) cross-check the resident against your roll.
    if not springfield_roll.is_resident(snapshot["claims"]):
        return dc.post(
            f"/b2b/v1/self-service/requests/{request_id}/decision",
            json={"decision": "reject", "reason": "Not on the resident roll."},
        )

    # 3. Approve. No admin_claims needed — every claim is identification-pre-filled.
    return dc.post(
        f"/b2b/v1/self-service/requests/{request_id}/decision",
        json={"decision": "approve"},
    )

That's the whole production-grade integration. ~20 lines.

If you want to override a claim

Maybe Springfield's CRM has the resident's preferred name on file and wants to use it instead of the legal given_name DeepID returned. Add the override to the approve payload:

dc.post(
    f"/b2b/v1/self-service/requests/{request_id}/decision",
    json={
        "decision": "approve",
        "admin_claims": {"given_name": "Lisa"},
    },
)

Override precedence is fixed: user → identification → admin. Admin wins on any collision, every other claim flows through as it was pre-filled. See Detailed Reference → Overriding user-supplied claims for the full rules.

Smaller alternative — manual mode

If Springfield's volume is low (a handful of cards a week), they can skip the backend integration entirely:

  1. In the config, set Verification mode = manual (instead of webhook).
  2. No webhook endpoint needed.
  3. Springfield's ops team gets a Requests table in the portal with Approve and Reject buttons. The same identification pre-fill works the same way.

When volume grows, flipping the config to webhook is the only change — the rest of the setup (credential definition, identification mapping, validity) stays as is.

Recap

From the resident's tap on the share-link to the card landing in their wallet is under 30 seconds with zero data entry. Springfield wrote about 20 lines of code, and the heavy lifting — identity verification, credential issuance, wallet handover — is on us.

See also

  • Quickstart — the same flow without the use-case framing.
  • Detailed Reference — every state, every event, error codes, idempotency, signature rotation.
  • Webhooks — signature verification, event subscriptions, one-attempt delivery, and reconciliation.
  • Credential Issuance Flow — machine-to-machine alternative when your backend already has the data.