Skip to main content
Version: v0.25.0 (Latest)

Attribute Pipeline

To issue a credential the EDK needs the claim values that go into it. Where those values come from, and when in the flow you give them to the issuer, is up to you. The EDK supports three integration patterns covering the common cases, from "I have all the data up front" to "my back-office system answers a few seconds later through a callback". Picking the right pattern for your situation is the most important design decision you make as an integrator; the rest is configuration.

This page describes the patterns, when to use each, and what each implies for storage and timing. The mechanics of hooking up your system (REST calls, configuration, SPI implementations) are in Attribute Sources. The REST endpoints are in the REST API.

The Guiding Principle: Provide Attributes as Late as You Can

Every attribute that lives in an issuance session is data the EDK has to encrypt, store, and eventually delete. It is regulated PII for the time the session is alive. The shorter that time, the less surface there is for compliance, leaks, and retention disputes.

The EDK session is built around this. The status of an in-flight session is in the clear; the actual attribute values are encrypted under a tenant key (or a session-bound key, see Persistence). But encrypted data at rest is still data at rest. If you can avoid putting an attribute into a session at all until the wallet is actually calling /credential, that is always the right answer.

In practice this means:

  • Avoid pushing the whole claim payload at session creation when you do not need to. If the wallet flow may take an hour or a day from offer creation to the wallet calling /credential, putting given_name, family_name, date_of_birth, address, national_id into the session at creation means those values sit in your database for that hour or day. Push lookup keys (an employee_id, an identity_id) at creation and pull the actual attributes at /credential time instead.
  • Pull from authoritative systems on demand. When the wallet calls /credential, the EDK can call your HR system, your CRM, or your identity store, assemble the credential from the live answer, sign it, and return. The attribute values exist in the session for a few seconds at most.
  • For slow systems, use the async callback path. When the upstream takes longer than the wallet should wait, dispatch the outbound call when /credential is hit, let the wallet's request fall through to a deferred-credential response, and have the upstream call back when it has the answer. The session still does not store the values for hours: the EDK assembles and signs as soon as the callback arrives.

The three patterns below correspond to these three positions.

Pattern 1: Provide Attributes at Session Start

The simplest case. Your system already has every attribute the credential needs at the moment you trigger issuance. An HR portal that wants to issue an "employee credential" knows the employee's name, role, start date, and so on at the moment of the create-offer click.

You push the attributes when you create the session: by calling POST /oid4vci/sessions with the full set of initialAttributes (see the REST API). The session stores them (encrypted at rest), the wallet eventually calls /credential, the EDK assembles the credential from the bag and signs it.

This is the right pattern when:

  • The flow from offer creation to the wallet calling /credential is short and predictable (minutes, not hours).
  • You hold the attribute values at offer creation time and they will not change before the wallet picks them up.
  • The total attribute payload is small.

It is the wrong pattern when the flow may take hours, when the underlying data is mutable and you want the freshest values at issuance, or when the payload is large enough that you do not want it sitting in the issuer's database. In those cases, prefer Pattern 2.

Pattern 2: Pull Attributes When the Wallet Asks for the Credential

The preferred default for most production integrations. At session creation you push only lookup keys: short identifiers that point at the user (an employee id, an internal identity id, an email). The full attributes are not in the session. The EDK is configured to call your system when the wallet calls /credential, takes the lookup key and fetches the live attributes, and assembles the credential from the fresh response.

The session never carries the personal data outside of the few seconds it takes to assemble and sign the credential. After issuance, the bag and lookup keys are gone (or retained explicitly to a vault if you want re-issuance support; that is opt-in, not the default).

How the EDK calls your system is your choice and is independent of the pattern:

  • Over HTTP, by configuring the built-in HTTP source to GET your endpoint with the lookup key in the query string and map the JSON response into attributes. No code on the EDK side.
  • Through a small SPI implementation, when you want strict control over auth, query shape, or error handling. A few dozen lines of Kotlin.
  • Direct SQL, when your data lives in a database the EDK service can reach. Implement the DatabaseLookupExecutor SPI to express the query; the surrounding wiring is done.

All three are documented in Attribute Sources.

This is the right pattern when:

  • Your authoritative system can answer synchronously within a wallet-acceptable response time (a few seconds).
  • You want the attribute values fresh at issuance time, not at offer creation.
  • You want the issuer's database to carry as little personal data as possible.

The trade-off: your system has to be reachable and reasonably fast when the wallet calls /credential. If it might be slow, use Pattern 3.

Pattern 3: Async Callback from a Slow Upstream

For systems that cannot answer in a synchronous window. A typical example is a back-office process that needs human review, or a legacy lookup that takes 30 seconds, or a third-party verification that goes through an external queue.

The EDK dispatches the request to your system when the wallet calls /credential (or earlier), and gives your system an opaque callback URL with a one-shot token. The wallet's request is held open for a configured sync window (a few seconds, typically). If your system calls back within that window, the wallet's request completes synchronously with the credential. If not, the wallet gets a deferred-credential response and polls; the next poll after the callback arrives returns the credential.

Your system does not need to be reachable from the wallet, only from the EDK issuer. You give back attributes (and optionally further lookup keys) when you POST to the callback URL. The capability token in the URL authenticates the callback; it is bound to one specific session and one specific source, so leaked callback URLs do not let someone tamper with other sessions.

The integration shape from your perspective:

  1. You configure a source on the EDK side (HTTP, custom, anything) with callbackStyle = ASYNC_CALLBACK and a syncWaitWindow (typically a few seconds). On dispatch the EDK calls your endpoint with the lookup keys it has and a callback URL it minted for this dispatch.
  2. Your system queues the work, does whatever it needs to do (human review, slow lookup, queue processing), and at some point POSTs the answer back to the callback URL.
  3. The EDK validates the callback token, drops the attributes into the session, and either completes a still-waiting /credential request synchronously or marks the session ready for the next /credential_deferred poll.

This is the pattern that turns "we cannot integrate with that system because it is too slow" into a workable wallet UX: the wallet sees a brief "issuing..." spinner and either gets the credential or transparently flips to a deferred-credential flow.

The callback endpoint shape and the body format are in the REST API.

Mapping Patterns to Protocol Moments

The EDK organises everything around phases. A phase is a named moment in the OID4VCI flow when contributions can happen. The phases you will care about as an integrator:

PhaseProtocol momentUsed by
SESSION_INITAn offer is created, or POST /oid4vci/sessions is calledPattern 1 (full payload) and Pattern 2 (lookup keys only)
AUTHORIZATIONThe authorization-code flow runs at the AS, the wallet authenticatesThe EDK captures OIDC userinfo claims here automatically when configured; integrators rarely push anything themselves
TOKENThe token endpoint exchanges the auth code for an access tokenGood place to resolve an external identifier (email from OIDC userinfo) to an internal one before the credential request needs it
CREDENTIAL_REQUESTThe wallet calls /credentialPattern 2 (synchronous pulls) and Pattern 3 (async dispatches)
DEFERREDThe wallet polls /credential_deferred, or a callback arrivesPattern 3 (where async callbacks deliver their answers)
POST_ISSUANCEAfter the credential is signed and returnedOptional vault retention for re-issuance

A simple Pattern 1 integration uses just SESSION_INIT. A Pattern 2 integration uses SESSION_INIT for lookup keys plus CREDENTIAL_REQUEST for the lazy pull. A Pattern 3 integration uses CREDENTIAL_REQUEST to dispatch and DEFERRED to receive the callback. You do not have to wire every phase; bind sources only to the phases you actually need.

Lookup Keys vs Attributes

Two kinds of values flow through a session. They behave differently and they exist for different reasons.

Attributes are the values that may end up in the credential. They are the claims: given_name, family_name, date_of_birth, email. The session encrypts them at rest. They are what the EDK assembles into the signed credential.

Lookup keys are short identifiers used to find attributes (employee_id = E1042, identity_id = 9af3-...). They are not attributes. They are correlation tokens that point at a user in some upstream system. The session also encrypts them at rest, but they are not what goes into the credential by default; their purpose is to let the EDK say "fetch the attributes for this user" when it calls your system.

This distinction is what makes Pattern 2 useful. Instead of pushing { given_name: "Ada", family_name: "Lovelace", date_of_birth: "1815-12-10", ... } at session creation, you push a single lookup key { name: "employee_id", value: "E1042" }. The session carries one short string instead of a full identity record. The personal data is fetched only at the moment the credential is being assembled.

A lookup key can be promoted to an attribute if you want the value to appear in the credential too (for example, an email that is both a lookup key for your CRM and a claim in the credential). By default it stays operational and does not appear in the credential.

Grouping at the REST Surface Is a Wire-Format Convenience

The V1 contribution endpoints accept a list of groups rather than a flat list of attribute records (see REST API). A group factors out the shared sourceId, phase, timestamp, and retention once for every attribute and lookup key it contains, and lets a semantic-set bundleId resolve the per-attribute path, value kind, classification, and retention server-side. This is a wire-format convenience only: the HTTP adapter decodes groups into the existing List<AttributeRecord> and List<LookupKey> before the pipeline runs. The phase model, the per-attribute provenance the engine carries through, the source-binding semantics, the deferral and approval gates, and the engine itself are unchanged. The same is true for preSeededGroups on the V1 offer endpoint: the decoder flattens groups into the IDK Map<String, JsonElement> shape before handing them to the underlying offer command.

Session Lifecycle from an Integrator's Perspective

You will see these statuses through the REST API or an admin UI:

StatusWhat it means for your integration
CREATEDThe session exists. If your pattern pushed at start, your input is in the session. The protocol has not run yet.
PHASE_EXECUTINGA phase is currently running. Brief.
PHASE_COMPLETEDBetween phases. Most sessions you observe will be here.
AWAITING_DEFERREDA /credential call returned a deferred response because not all required attributes were present. The session is waiting for callback ingress (Pattern 3) or the wallet's next poll.
AWAITING_APPROVALAll attributes are present but the credential binding requires explicit operator approval before issuance. The session stays here until someone calls /approve.
READYAll attributes present, all approvals done. The next /credential or /credential_deferred call returns the credential.
COMPLETEDDone. The wallet has the credential. The bag is still in the session until the retention sweeper removes it.
FAILEDTerminal failure.
EXPIREDPast expiresAt without completing.

If a session gets stuck in AWAITING_DEFERRED, the response to GET /oid4vci/sessions/{correlationId}/attributes lists every deferral entry with the source id and the reason. That is usually enough to diagnose which integration is not answering.

AWAITING_APPROVAL is for credentials whose binding has approvalRequired = true. Use it when a human must sign off on issuance before the wallet gets the credential. The approval is POST /oid4vci/sessions/{correlationId}/approve with the reviewer's identity and decision.

When You Have Multiple Sources

A single credential may need attributes from more than one place: a name and address from your HR database, a clearance level from a separate compliance system, an organisational role from a directory. The EDK runs every source bound to the same phase in lookup-key dependency order automatically: a source that needs identity_id runs after the source that produces identity_id. Sources at the same dependency level run in parallel.

You do not express the ordering. You declare what each source consumes and produces; the engine figures out the rest. If you have a chain (the OIDC userinfo gives email, the identity resolver turns email into identity_id, the HR system uses identity_id to fetch the rest), you bind three sources and the EDK schedules them in the right order. If two sources both need identity_id and neither feeds the other, the EDK runs them concurrently.

Retention: What Happens to the Data After Issuance

By default the session keeps its bag until it expires (set by expiresAt, default an hour, configurable per session at creation) and is then deleted by the retention sweeper. After that there is no record of the attribute values inside the issuer. The standard EDK audit log retains the command-level history (who triggered what, when, against which session id) without the attribute values themselves.

If the holder may ask for a new copy of the same credential later, you can opt in to vault retention at POST_ISSUANCE: write the assembled attributes into the encrypted EDK vault keyed by an identifier you choose. A future re-issuance flow then reads the vault instead of going back to your authoritative system. The trade-off is explicit: vault retention is opt-in, the retention period is configurable, and the vault has its own access controls.

If you do not opt in, the issuer's database is clean of the issued credential's claim values within the session TTL. Pattern 2 plus no vault retention is the smallest possible attribute-residency footprint the EDK supports while still serving credentials.

Putting It Together

The decision tree for a new integration:

  1. Can you provide every attribute at the moment you trigger issuance, and will the wallet call /credential within minutes? → Pattern 1.
  2. Can your authoritative system answer synchronously when the wallet calls /credential? → Pattern 2.
  3. Can it not? → Pattern 3.

For most production integrations the answer is Pattern 2: push lookup keys at session creation, fetch attributes lazily at /credential. The session never holds the personal data outside the few seconds of assembly, the wallet gets a synchronous credential, and retention is minimal by construction.

The next page, Attribute Sources, shows how to hook your system up under each pattern: which REST calls to make for Pattern 1, which sources to configure or write for Pattern 2, and how to wire the callback endpoint for Pattern 3.