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

The Semantic Catalog (L1)

Before any credential is designed, the catalog lets an organization describe its world: the kinds of party it deals with, the attributes those parties carry, and the relationships between them. Everything downstream, profiles, sets, VC channels, issuer designs, and verifier DCQL queries, derives from this one governed layer, so it is worth modeling deliberately rather than per credential.

The catalog is owned by a Party. Every catalog is bound to its owner through ownerPartyRef. In most organizations that owner is the tenant or the organization itself, though it can just as well be a local subsidiary that keeps its own catalog. Whoever owns it, a catalog is the organization's single source of truth for its world: one governed place where the meaning, classification, and structure of every attribute is defined once and reused everywhere.

The catalog is created as a shell (name, ownerPartyRef, and overlays), then entities and relationships are appended via sub-collection calls, and finally the catalog is published. This staged construction lets each entity and relationship be reviewed before the catalog is made available to the layers above it.

VDX guided authoring

The REST calls on this page are the same operations VDX performs when you author a catalog through its guided UI. The concepts and the resulting model are identical whether you use the UI or the API directly.

Four-layer semantic model: L1 catalog declares entities (PREDEFINED and SPECIALIZATION) and first-class relationships; L2 profile binds catalog entities to use-case roles and selects relationships; L3 set is a role-scoped traversal subselection of one profile; L4 channels are the set-bound artifacts that issuer designs and verifier DCQL both derive from.

Concepts

Entities: PREDEFINED and SPECIALIZATION

Each entity in the catalog is a kind of party. The platform ships a rich set of built-in party types, including natural_person, organization, address (a physical address), and group, alongside platform roles such as identity, credential_template, resource, and user. The partyType token is extensible too, so a catalog can declare any further type an organization needs. Each PREDEFINED entity names the party type it represents in its partyType.

There are two entity kinds:

  • kind: "PREDEFINED", a built-in party type modelled up front, identified by a partyType token. The walkthrough uses three of them: natural_person (Person), address (Address), and organization (Organization). Because address is a party type in its own right, any party links to an address through a named relationship rather than carrying address fields inline.
  • kind: "SPECIALIZATION", a use-case subtype that extends an existing entity with specializes: { entity: "<EntityName>" } and adds attributes on top of what it specializes. The walkthrough defines Employee (specializes Person) and Employer (specializes Organization). Specializations are the pattern for roles like Employee, Student, or Customer that a use case introduces without making them platform-level party types.

Every entity has a name, an attributes array, and an entity-level overlays.i18n block ({ "<lang>": { "label", "information" } }).

Attribute anatomy

Each attribute in an entity's attributes array has this shape:

FieldMeaning
pathThe attribute's location in the entity, an array of segments (e.g. ["given_name"]). Each segment names one attribute, so a segment never contains a .. This is the semantic path; where a value lands in an issued credential is a separate wire claimPath, decided later at L4.
valueTypeOne of the 7 OCA capture-base types: Text, Numeric, Boolean, Binary, DateTime, Reference, Array. Drives form widget, validation, and wire encoding.
overlaysThe overlay bag. Everything beyond the capture base, including all governance, lives here.

Every piece of meaning beyond the capture base is an overlay in the overlays bag:

Overlay keyMeaning
conformanceWhether a value is required. MANDATORY means the attribute must be present when the entity is captured or issued; OPTIONAL means it may be omitted.
sdPolicyThe default selective-disclosure stance once the attribute becomes a credential claim: ALWAYS (independently disclosable), NEVER (only ever disclosed bundled with the credential), or OPTIONAL (the holder chooses at presentation).
formatA value-format constraint such as a regular expression or an ISO 8601 date pattern, used for validation and to convey date/time precision.
widgetHintThe preferred capture widget for a form rendering, such as TEXT_INPUT or PICKLIST.
standardThe reference standard the values follow, such as "ISO 3166-1" for country codes. Informational provenance for the value domain.
entryCodesThe allowed value domain for a coded attribute, { "codes": ["NL", "DE", ...] }. Declaring it makes the attribute a closed code list; a profile or set may later narrow it to a subset, and the human-readable label for each code lives under i18n.<lang>.entries. A coded attribute must declare its entryCodes here; nothing downstream invents codes.
unitThe unit of measure for a numeric value, such as a currency or SI unit.
sensitiveMarks the attribute as sensitive so renderers mask it in UIs and logs.
i18nPer-language label, information text, and code display strings: { "<lang>": { "label", "information", "entries": { "<code>": "<display>" } } }. The entries map labels the codes declared in entryCodes, so its keys must be a subset of those codes.
classificationGovernance: { label?, impact?: { confidentiality, integrity, availability }, pii?, specialCategory?: { type } }. ISO 27001 label, NIST FIPS 199 impact triad, and the personal-data / GDPR Article 9 special-category markers.
processingGovernance: { purposes: [ { purpose, legalBasis?, retention? } ], operations: [] }. The DPV processing purposes, each with its GDPR Article 6 legal basis and its own retention rule.
residencyGovernance: { allowed, denied, localizationRequired?, transfer?: { mechanism } }. Where the value may reside and the Chapter V transfer mechanism for permitted cross-border movement.
assuranceGovernance: { loa?, attestationType?, authenticSourceRef? }. The eIDAS level of assurance and electronic-attestation type.

The four governance overlays (classification, processing, residency, assurance) are grounded in W3C DPV, GDPR, NIST FIPS 199, ISO 27001, and eIDAS2. They are authored once on the catalog attribute and flow through every layer into the wire form, never retyped per credential. Personal-data classification is overlays.classification.pii (with specialCategory for GDPR Article 9 data).

Relationships are first-class

Relationships between entities are first-class catalog objects. A relationship has:

FieldMeaning
nameThe relationship name, used as a path segment in traversal (e.g. residentialAddress).
relationTypeA semantic type token (e.g. has-address, employed-by).
sourceThe source end: { entity: { entity: "<Name>" }, role?, cardinality: { min, max? } }.
targetThe target end: { entity: { entity: "<Name>" }, role?, cardinality: { min, max? } }.
overlays.i18nLocalized labels.

Cardinality is per-end (UML look-across style): the cardinality on each end describes how many of that end exist from the perspective of the opposite end. For example, residentialAddress has source Person with { min: 0 } (a home address is optional for a person) and target Address with { min: 1, max: 1 } (when the relationship exists, it points to exactly one address).

Schema bindings

A catalog may also record how its entities and attributes relate to external schemas via two sub-collections:

  • Schema bindings (/catalogs/{id}/schemabindings): named pointers to external or internal schemas (JSON_SCHEMA, SD_JWT_VCT, etc.) with a purpose and an origin.
  • Schema relationships (/catalogs/{id}/schemarelationships): per-attribute mappings between a canonical attribute semanticPath and an external schema attribute, with a relation type (DERIVED_FROM_EXTERNAL, EQUIVALENT_TO_EXTERNAL, etc.) and a direction. These anchor provenance in the model, for example, this canonical birth_date is DERIVED_FROM_EXTERNAL the PID birthdate claim, rather than leaving it in a spreadsheet.

Lifecycle

A catalog's status moves through DRAFT | PUBLISHED. A DRAFT is editable; a PUBLISHED snapshot is frozen and can be pinned by version from a profile. The catalog also carries a monotonic version counter starting at 1.


REST: Build the Walkthrough Catalog

All calls use the operator's OIDC bearer token. The tenant is resolved from the token's tenant_id claim.

Authorization: Bearer <operator access token>
Content-Type: application/json

Every write in the enterprise tier is gated behind the semantic-attribute-binding license feature. With the unbounded default license the gate passes. See Modeling Your World.

The walkthrough catalog has five entities and two relationships:

EntityKindpartyTypeAttributes
PersonPREDEFINEDnatural_persongiven_name, family_name, email
AddressPREDEFINEDaddressstreet, postal_code, city, country
OrganizationPREDEFINEDorganizationlegal_name, registration_number
EmployeeSPECIALIZATION of Person,employee_id, job_title, employment_status
EmployerSPECIALIZATION of Organization,department
RelationshipTypeSourceTarget
residentialAddresshas-addressPerson (cardinality min: 0)Address, role home (cardinality min: 1, max: 1)
employmentemployed-byEmployee (cardinality min: 0)Employer, role employer (cardinality min: 1, max: 1)

Step 1: Create the catalog shell

POST /api/attributes/v1/catalogs
{
"name": "Acme Employee Catalog (layered)",
"description": "Canonical Acme employee semantic catalog authored via the layered /api/attributes/v1/catalogs surface.",
"ownerPartyRef": "acme",
"overlays": {
"meta": {
"en": {
"name": "Acme Employee Catalog",
"description": "Canonical employee + address + organization attributes governed by Acme HR"
},
"nl": {
"name": "Acme Medewerkerscatalogus",
"description": "Canonieke medewerker-, adres- en organisatieattributen, beheerd door Acme HR"
},
"es": {
"name": "Catálogo de empleados de Acme",
"description": "Atributos canónicos de empleado, dirección y organización gobernados por RR. HH. de Acme"
},
"fr": {
"name": "Catalogue des employés d'Acme",
"description": "Attributs canoniques d'employé, d'adresse et d'organisation régis par les RH d'Acme"
}
},
"governance": {
"jurisdictions": [
{
"region": "EU",
"laws": ["eu-gdpr"],
"frameworks": ["GDPR", "EIDAS2", "NIS2"],
"authority": "EDPB",
"residency": { "allowed": ["EU", "EEA"], "localizationRequired": false },
"defaultTransfer": { "mechanism": "ADEQUACY_DECISION" }
}
],
"primaryJurisdiction": "EU",
"controller": { "partyId": "acme", "role": "CONTROLLER", "establishedIn": "NL" },
"classificationScheme": ["PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED"]
}
}
}

The overlays.meta block carries localized catalog-level name and description (separate from the per-attribute overlays.i18n labels added below). The overlays.governance block grounds the catalog in DPV/GDPR/eIDAS2/NIS2: jurisdictions, residency rules, data controller identity, and a classification scheme.

Step 2: Add entities

All five entities are appended to the catalog shell via POST /api/attributes/v1/catalogs/{catalogId}/entities. Each call returns the updated catalog; check that the entities count increased.

Person (PREDEFINED, natural_person)

POST /api/attributes/v1/catalogs/{catalogId}/entities
{
"name": "Person",
"kind": "PREDEFINED",
"partyType": "natural_person",
"attributes": [
{
"path": ["given_name"],
"valueType": "Text",
"overlays": {
"conformance": "MANDATORY",
"sdPolicy": "ALWAYS",
"classification": { "pii": true },
"i18n": {
"en": { "label": "Given name" },
"nl": { "label": "Voornaam" },
"es": { "label": "Nombre" },
"fr": { "label": "Prénom" }
}
}
},
{
"path": ["family_name"],
"valueType": "Text",
"overlays": {
"conformance": "MANDATORY",
"sdPolicy": "ALWAYS",
"classification": { "pii": true },
"i18n": {
"en": { "label": "Family name" },
"nl": { "label": "Achternaam" },
"es": { "label": "Apellido" },
"fr": { "label": "Nom de famille" }
}
}
},
{
"path": ["email"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"sdPolicy": "ALWAYS",
"format": "^[^@]+@[^@]+$",
"classification": { "pii": true },
"i18n": {
"en": { "label": "Email" },
"nl": { "label": "E-mail" },
"es": { "label": "Correo electrónico" },
"fr": { "label": "E-mail" }
}
}
}
],
"overlays": {
"i18n": {
"en": { "label": "Person", "information": "A natural person" },
"nl": { "label": "Persoon" },
"es": { "label": "Persona", "information": "Una persona física" },
"fr": { "label": "Personne", "information": "Une personne physique" }
}
}
}

Each attribute path is an array of string segments. The overlays.i18n block is a language-keyed map; each entry carries label, information, and (for coded attributes) entries mapping code values to display strings. Personal-data classification lives in overlays.classification.pii, not as a flat field on the attribute.

Address (PREDEFINED, address)

Address is a predefined party in its own right (partyType: "address"). Keeping address as its own entity means any party type can link to an address through a named relationship, and the address attributes are governed and classified independently.

POST /api/attributes/v1/catalogs/{catalogId}/entities
{
"name": "Address",
"kind": "PREDEFINED",
"partyType": "address",
"attributes": [
{
"path": ["street"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"classification": { "pii": true },
"i18n": {
"en": { "label": "Street" },
"nl": { "label": "Straat" },
"es": { "label": "Calle" },
"fr": { "label": "Rue" }
}
}
},
{
"path": ["postal_code"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"classification": { "pii": true },
"i18n": {
"en": { "label": "Postal code" },
"nl": { "label": "Postcode" },
"es": { "label": "Código postal" },
"fr": { "label": "Code postal" }
}
}
},
{
"path": ["city"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"classification": { "pii": true },
"i18n": {
"en": { "label": "City" },
"nl": { "label": "Plaats" },
"es": { "label": "Ciudad" },
"fr": { "label": "Ville" }
}
}
},
{
"path": ["country"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"widgetHint": "PICKLIST",
"standard": "ISO 3166-1",
"entryCodes": { "codes": ["NL", "DE", "FR", "BE"] },
"classification": { "pii": true },
"i18n": {
"en": {
"label": "Country",
"entries": { "NL": "Netherlands", "DE": "Germany", "FR": "France", "BE": "Belgium" }
},
"nl": {
"label": "Land",
"entries": { "NL": "Nederland", "DE": "Duitsland", "FR": "Frankrijk", "BE": "België" }
},
"es": {
"label": "País",
"entries": { "NL": "Países Bajos", "DE": "Alemania", "FR": "Francia", "BE": "Bélgica" }
},
"fr": {
"label": "Pays",
"entries": { "NL": "Pays-Bas", "DE": "Allemagne", "FR": "France", "BE": "Belgique" }
}
}
}
}
],
"overlays": {
"i18n": {
"en": { "label": "Address", "information": "A physical postal address" },
"nl": { "label": "Adres" },
"es": { "label": "Dirección", "information": "Una dirección postal física" },
"fr": { "label": "Adresse", "information": "Une adresse postale physique" }
}
}
}

Organization (PREDEFINED, organization)

Organization is a predefined party (partyType: "organization") carrying legal_name and registration_number. Both attributes are non-personal (classification.pii: false), which flows through to DCQL and credential rendering.

POST /api/attributes/v1/catalogs/{catalogId}/entities
{
"name": "Organization",
"kind": "PREDEFINED",
"partyType": "organization",
"attributes": [
{
"path": ["legal_name"],
"valueType": "Text",
"overlays": {
"conformance": "MANDATORY",
"sdPolicy": "ALWAYS",
"classification": { "pii": false },
"i18n": {
"en": { "label": "Legal name" },
"nl": { "label": "Statutaire naam" },
"es": { "label": "Razón social" },
"fr": { "label": "Raison sociale" }
}
}
},
{
"path": ["registration_number"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"sdPolicy": "ALWAYS",
"classification": { "pii": false },
"i18n": {
"en": { "label": "Registration number" },
"nl": { "label": "KvK-nummer" },
"es": { "label": "Número de registro" },
"fr": { "label": "Numéro d'enregistrement" }
}
}
}
],
"overlays": {
"i18n": {
"en": { "label": "Organization", "information": "A legal organization" },
"nl": { "label": "Organisatie" },
"es": { "label": "Organización", "information": "Una organización jurídica" },
"fr": { "label": "Organisation", "information": "Une organisation juridique" }
}
}
}

Employee (SPECIALIZATION of Person)

Employee specializes Person and adds three employment-specific attributes. The specializes field names the predefined entity being extended. Employee inherits given_name, family_name, and email from Person and contributes employee_id, job_title, and employment_status of its own.

POST /api/attributes/v1/catalogs/{catalogId}/entities
{
"name": "Employee",
"kind": "SPECIALIZATION",
"specializes": { "entity": "Person" },
"attributes": [
{
"path": ["employee_id"],
"valueType": "Text",
"overlays": {
"conformance": "MANDATORY",
"sdPolicy": "ALWAYS",
"classification": { "pii": true },
"i18n": {
"en": { "label": "Employee identifier" },
"nl": { "label": "Medewerkernummer" },
"es": { "label": "Identificador de empleado" },
"fr": { "label": "Identifiant de l'employé" }
}
}
},
{
"path": ["job_title"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"sdPolicy": "ALWAYS",
"classification": { "pii": true },
"i18n": {
"en": { "label": "Job title" },
"nl": { "label": "Functietitel" },
"es": { "label": "Puesto" },
"fr": { "label": "Intitulé du poste" }
}
}
},
{
"path": ["employment_status"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"sdPolicy": "ALWAYS",
"widgetHint": "PICKLIST",
"entryCodes": { "codes": ["active", "suspended", "terminated"] },
"classification": { "pii": true },
"i18n": {
"en": {
"label": "Employment status",
"entries": { "active": "Active", "suspended": "Suspended", "terminated": "Terminated" }
},
"nl": {
"label": "Dienstverbandstatus",
"entries": { "active": "Actief", "suspended": "Geschorst", "terminated": "Beëindigd" }
},
"es": {
"label": "Situación laboral",
"entries": { "active": "Activo", "suspended": "Suspendido", "terminated": "Terminado" }
},
"fr": {
"label": "Statut d'emploi",
"entries": { "active": "Actif", "suspended": "Suspendu", "terminated": "Terminé" }
}
}
}
}
],
"overlays": {
"i18n": {
"en": { "label": "Employee", "information": "A person employed by the organization" },
"nl": { "label": "Medewerker" },
"es": { "label": "Empleado", "information": "Una persona empleada por la organización" },
"fr": { "label": "Employé", "information": "Une personne employée par l'organisation" }
}
}
}

Employer (SPECIALIZATION of Organization)

Employer specializes Organization and adds department. The pattern is symmetric: just as Employee narrows a Person, Employer narrows an Organization for the employment use case.

POST /api/attributes/v1/catalogs/{catalogId}/entities
{
"name": "Employer",
"kind": "SPECIALIZATION",
"specializes": { "entity": "Organization" },
"attributes": [
{
"path": ["department"],
"valueType": "Text",
"overlays": {
"conformance": "OPTIONAL",
"sdPolicy": "ALWAYS",
"classification": { "pii": false },
"i18n": {
"en": { "label": "Department" },
"nl": { "label": "Afdeling" },
"es": { "label": "Departamento" },
"fr": { "label": "Service" }
}
}
}
],
"overlays": {
"i18n": {
"en": { "label": "Employer", "information": "An organization in its role as employer" },
"nl": { "label": "Werkgever" },
"es": { "label": "Empleador", "information": "Una organización en su rol de empleador" },
"fr": { "label": "Employeur", "information": "Une organisation dans son rôle d'employeur" }
}
}
}

Step 3: Add relationships

Relationships are appended via POST /api/attributes/v1/catalogs/{catalogId}/relationships. Each call returns the updated catalog; check that the relationships count increased.

Cardinality follows the UML look-across convention: the cardinality on each end describes how many of that end exist from the perspective of the opposite end.

residentialAddress (Person to Address)

POST /api/attributes/v1/catalogs/{catalogId}/relationships
{
"name": "residentialAddress",
"relationType": "has-address",
"source": {
"entity": { "entity": "Person" },
"cardinality": { "min": 0 }
},
"target": {
"entity": { "entity": "Address" },
"role": "home",
"cardinality": { "min": 1, "max": 1 }
},
"overlays": {
"i18n": {
"en": { "label": "Residential address" },
"nl": { "label": "Woonadres" },
"es": { "label": "Dirección residencial" },
"fr": { "label": "Adresse de résidence" }
}
}
}

The source end cardinality: { min: 0 } means a person may have zero residential addresses (it is optional). The target end cardinality: { min: 1, max: 1 } means when the relationship exists, it points to exactly one address. The target role: "home" names what this Address represents in this relationship, it becomes part of the traversal semantics used by the set and VC channels.

employment (Employee to Employer)

POST /api/attributes/v1/catalogs/{catalogId}/relationships
{
"name": "employment",
"relationType": "employed-by",
"source": {
"entity": { "entity": "Employee" },
"cardinality": { "min": 0 }
},
"target": {
"entity": { "entity": "Employer" },
"role": "employer",
"cardinality": { "min": 1, "max": 1 }
},
"overlays": {
"i18n": {
"en": { "label": "Employment" },
"nl": { "label": "Dienstverband" },
"es": { "label": "Empleo" },
"fr": { "label": "Emploi" }
}
}
}

Step 4: Publish and resolve

Once all five entities and both relationships are appended, publish the catalog with a status transition. Publishing locks the version: downstream layers pin a specific catalogVersion and their view of the catalog does not change if you later create a new draft.

PUT /api/attributes/v1/catalogs/{catalogId}/status
{ "status": "PUBLISHED" }

200 OK returns the updated catalog with status: "PUBLISHED". Capture the published version, this is the catalogVersion the profile will pin.

Listing catalogs (the paged envelope)

Every list endpoint in the /api/attributes/v1 family is paginated and filterable uniformly.

GET /api/attributes/v1/catalogs?limit=10&offset=0

Filter and sort:

GET /api/attributes/v1/catalogs?nameContains=Acme&sort=name&sortDirection=ASC&limit=50

Accepted query parameters: limit, offset, page, size, sort, sortDirection, nameContains, status. See Modeling Your World for the full pagination reference.


What you have now

One published catalog that is the organization's single source of truth for its world: five governed party entities (three predefined, two specializations) and two first-class relationships, each attribute carrying OCA-grounded governance overlays that flow through every layer above without being retyped.

The catalog id and published version are the values the attribute profile will pin in its catalogRefs array.

Next steps

  • Attribute Profile (L2): bind catalog entities to use-case roles, select relationships, and apply narrow-only overrides
  • Modeling Your World: the concepts behind entity kinds, governance overlays, override precedence, and the license gate