Credential Design Integration
The EDK glues OCA into the credential design system through three components. The OcaDesignMapper converts a parsed OCA bundle into the canonical credential design model used by the IDK (claim presentations, displays, derived render hints). The OcaBundleDesignProvider plugs OCA into the IDK's design resolution pipeline as a layer provider, so calls to resolveCredentialDesign can fold OCA-derived metadata into the result alongside OID4VCI metadata, SD-JWT VCT, and local overrides. The OcaNativePersistenceService stores the original OCA records alongside the canonical design so the bundle remains round-trippable even after the design has been mapped, edited, and republished.
This page explains how the three components fit together and what each one is responsible for. The underlying credential design model (claim paths, value kinds, widget hints, render variants) is documented in the IDK credential design guide and is not reshaped by the OCA integration; OCA is one of several sources that feed it.
The Mapper
OcaDesignMapper.toCanonical(bundle, bindings) is the conversion entry point. It accepts a ParsedOcaBundle and a list of DesignBindings and returns an OcaCanonicalResult containing:
- A
CredentialDesignRecordwith the bindings, locale-aware displays, and aClaimPresentationper attribute, ordered as they appear in the capture base - A
DerivedRenderHintsRecordwith per-fieldFieldRenderHintentries carrying the OCA-derived value kind, widget hint, character encoding, format pattern, standard reference, parsed cardinality, and sensitivity flag - The same displays and claims as flat lists, in case the caller wants them directly
The mapper delegates the overlay walk to OcaOverlayProcessor (described in Bundle Service), then maps the processed result onto the design model types. The translation is mechanical:
| OCA Concept | Canonical Mapping |
|---|---|
| Attribute and its type | ClaimPresentation with valueKind from OcaTypeMapping.valueKind |
| Attribute with no entry codes | widgetHint from OcaTypeMapping.defaultWidget |
| Attribute with entry codes | widgetHint = ClaimWidgetHint.PICKLIST (overrides the default) |
label overlay per locale | ClaimLabel(locale, label) added to the claim's labels |
information overlay per locale | ClaimLabel.description for the matching locale |
entry overlay per locale | ClaimLabel.entryValues for the matching locale |
conformance mandatory flag | ClaimPresentation.mandatory = true |
cardinality string | ClaimCardinality(min, max) via parseCardinality |
sensitive overlay | FieldRenderHint.sensitive = true |
unit overlay | ClaimPresentation.unit |
meta overlay per locale | LocalizedCredentialDisplay(locale, name, description) |
format overlay | FieldRenderHint.formatHint |
character_encoding overlay | FieldRenderHint.characterEncoding |
standard overlay | FieldRenderHint.standard |
The bindings parameter lets the caller decide how the resulting design should be matched to credentials at resolution time. When no bindings are provided the mapper falls back to a single OCA-SAID binding (DesignBinding(ocaSaid = bundleCaptureBaseSaid)), which makes the design match credentials referenced by their capture base SAID. In practice, callers usually pass an explicit binding such as a CREDENTIAL_CONFIGURATION_ID or VCT so the OCA-derived design slots into the same lookup keys as designs from other sources.
The mapper does not touch render variants; those are the responsibility of separate ingestion paths (SVG templates, W3C render method references, simple cards). An OCA bundle that wants to carry a render template should attach the template through a render variant in the credential design system, not through OCA itself.
The Layer Provider
OcaBundleDesignProvider implements the IDK's DesignLayerProvider interface and registers itself with the design resolution engine. At resolve time, the provider looks for an OCA bundle in ResolveCredentialDesignInput.externalMetadata.ocaBundle, parses it, runs the SAID structural check, and runs the mapper to produce a CredentialDesignLayerResult. The layer result carries the displays, claims, and derived render hints contributed by OCA, plus a set of providedFields markers the engine uses to track which fields the OCA layer touched.
The provider's sourceType is DesignSourceType.OCA_BUNDLE and its authoritative flag is false by default, which means lower-priority providers can still contribute fields the OCA bundle did not cover, and higher-priority providers (notably LOCAL_OVERRIDE) win on conflict. If your deployment treats OCA as the canonical source for a particular credential type, set authoritative = true on a custom subclass and bind that instead; the engine will then field-lock everything the OCA layer contributes.
val resolved = designService.resolveCredentialDesign(
tenantId = tenantId,
input = ResolveCredentialDesignInput(
bindingKey = DesignBindingKey.VCT,
bindingValue = "https://issuer.example.com/identity",
preferredLocales = listOf("en", "nl"),
externalMetadata = ExternalDesignMetadata(
ocaBundle = bundleJson,
),
),
)
When the provider returns a non-null layer, the resolution engine merges its contributions in priority order with everything else (schema inference, JSON-LD context, OID4VCI metadata, SD-JWT VCT, W3C render method, local override). The full priority list is in the IDK resolution and import guide.
Native Persistence
When an OCA bundle is imported through the design service, the canonical credential design produced by the mapper is what the IDK stores. The original OCA records are not kept by the design store itself. The EDK provides a parallel native persistence path so the original bundle and overlays can be retrieved unchanged later, for republication, for export, or just for audit.
OcaNativePersistenceService.persistBundle(tenantId, bundle, designId, sourceSnapshotId) writes an OcaBundleRecord for the bundle and an OcaOverlayRecord for each overlay, linked back to the corresponding CredentialDesignRecord via designId and to the original fetched payload via sourceSnapshotId. The records carry every overlay type and language present in the bundle, plus the original payload JSON, so the bundle can be reassembled byte-for-byte.
The two repository interfaces:
interface OcaBundleRepository {
suspend fun findById(tenantId: String, id: Uuid): OcaBundleRecord?
suspend fun findByBundleSaid(tenantId: String, bundleSaid: String): OcaBundleRecord?
suspend fun findByDesignId(tenantId: String, designId: Uuid): List<OcaBundleRecord>
suspend fun create(record: OcaBundleRecord): OcaBundleRecord
suspend fun delete(tenantId: String, id: Uuid): Boolean
}
interface OcaOverlayRepository {
suspend fun findById(tenantId: String, id: Uuid): OcaOverlayRecord?
suspend fun findByBundleId(tenantId: String, bundleId: Uuid): List<OcaOverlayRecord>
suspend fun create(record: OcaOverlayRecord): OcaOverlayRecord
suspend fun delete(tenantId: String, id: Uuid): Boolean
}
The dialect-specific implementations follow the same Postgres*Repository / Mysql*Repository pattern as the rest of the credential design persistence layer (see Persistence and Caching). The records persist as native JSON for the overlay payloads with a small number of indexed top-level columns (bundle_said, capture_base_said, design_id, tenant_id) for efficient lookups.
Two additional record types exist for richer OCA workflows:
OcaOverlayDefinitionRecord describes a custom overlay schema. OCA supports user-defined overlay types beyond the core spec; this record stores the definition (namespace, name, version, unique-key fields, definition JSON) so an ecosystem can publish and reference its own overlays.
OcaAuthoringArtifactRecord describes the source files used to author a bundle (raw bundle JSON, capture base JSON, individual overlay JSON, OCAfile or overlay-file declarations). The kind enum (BUNDLE_JSON, CAPTURE_BASE_JSON, OVERLAY_JSON, OCAFILE, OVERLAYFILE) drives interpretation. The actual content is stored in the blob store under contentBlobPath; the record carries the metadata and links back to the design and source snapshot.
Both additional record types have their own repositories with the same dialect-specific implementations.
End-to-End Import Flow
The typical OCA import looks like this:
- The client calls
importCredentialDesign(or the OCA-specific import variant) with the source URL or raw bundle. - The design service fetches the bundle through the
DesignExternalFetcher, which enforces SSRF protection and size limits. - The fetched content is stored as a
SourceSnapshotRecord, producingsourceSnapshotId. - The bundle is parsed and the SAID is structurally verified.
- The mapper produces a canonical
CredentialDesignRecordandDerivedRenderHintsRecord. - The design service persists those records and creates a
DesignVersion(see Versioning). - The
OcaNativePersistenceServicewrites the nativeOcaBundleRecordandOcaOverlayRecords, linked to the newdesignIdandsourceSnapshotId.
After import, two consumers can read the design:
- The IDK design service returns the canonical
CredentialDesignRecordfromgetCredentialDesign(or any resolution call). - The OCA repositories return the native
OcaBundleRecordand overlays fromfindByDesignId, which can be reassembled into the original bundle JSON for export or republication.
If the bundle is later refreshed because the external source changed, a new SourceSnapshotRecord is stored, the mapper runs again, a new DesignVersion is committed, and a new set of native OCA records is written. The historical OCA records remain queryable through the version-aware repository methods.
Driving Attribute Provisioning from a Bundle
The EDK OID4VCI attribute-contribution endpoint accepts an OCA bundle id directly in its compact request body. Inside each contribution group, a semanticAttributeSets entry carries a bundleId (and an optional version) plus a values map of term -> JSON value. The decoder resolves the bundle through the SemanticAttributeSetService, indexes the resulting SemanticAttributeDefinitions by attribute term, and for each term derives the wire-side fields the integrator would otherwise have to spell out:
path(the JSON Pointer the attribute lands at in the session bag)valueKind(how the JSON value is wrapped as anAttributeValue)dataClassification(e.g. PII, sensitive PII)legalBasis(GDPR Art. 6 lawful basis, jurisdiction-specific values supported)retentionPolicy(ephemeral, session, or retained withretentionDaysand friends)sensitive(drives masking and PII-handling pathways)sdPolicy(selective-disclosure policy at credential-assembly time)
The same semanticAttributeSets[i].bundleId field is accepted by POST /api/v1/oid4vci/offers under preSeededGroups[i].semanticAttributeSets[i].bundleId, so an integrator can seed attributes at offer creation time using exactly the same notation.
A typical contribution becomes a small JSON payload that names the bundle and lists term-value pairs:
{
"groups": [
{
"sourceId": "hr-backend",
"phase": "oid4vci_credential_request",
"semanticAttributeSets": [
{
"bundleId": "eu.europa.ec.eudi.pid.1",
"values": {
"given_name": "Alice",
"family_name": "Smith",
"birth_date": "1990-04-01"
}
}
]
}
]
}
The server-side resolver call goes through SemanticAttributeSetService.getAttributeSetByName(bundleId, version). When version is omitted, the service returns the tenant's pinned active bundle version (falling back to the highest available version), so tenants pinned to an earlier bundle get deterministic resolution rather than silent drift. Unknown terms short-circuit the entire request with a single 400 response listing every unresolved item across all sets; the session is never partially updated.
Per-attribute overrides (retention, priority, assurance, verified, sourceDetail, sdPolicy) remain available through the semanticAttributeSets[i].attributes[] form, with the precedence per-item > set-level > group-level > semantic-derived > default. The wire shape and the precedence rules are documented in the OID4VCI REST API.
When to Pass OCA Inline vs. Import It
Two distinct flows use OCA bundles, and they call into different parts of the integration:
Inline resolution. A wallet has a credential and an OCA bundle that came with it (for example, attached to an OID4VCI offer or fetched on the side from a published location). The wallet calls resolveCredentialDesign with the bundle in externalMetadata.ocaBundle. The layer provider parses, maps, and contributes claims and displays for this single call, but nothing is persisted. The OCA bundle is treated as transient data.
Registry import. An issuer admin uploads or references an OCA bundle that should become a long-lived credential design in the registry. The bundle goes through the full import flow above: it is fetched, snapshotted, parsed, verified, mapped, persisted as a design, persisted natively as OCA records, and versioned. Subsequent resolution calls find the canonical design through its bindings and do not need to pass the OCA bundle again.
The inline path is what mobile wallets and one-off rendering use. The registry path is what issuer onboarding and verifier-side credential type catalogs use.