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.
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.
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 apartyTypetoken. The walkthrough uses three of them:natural_person(Person),address(Address), andorganization(Organization). Becauseaddressis 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 withspecializes: { 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:
| Field | Meaning |
|---|---|
path | The 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. |
valueType | One of the 7 OCA capture-base types: Text, Numeric, Boolean, Binary, DateTime, Reference, Array. Drives form widget, validation, and wire encoding. |
overlays | The 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 key | Meaning |
|---|---|
conformance | Whether a value is required. MANDATORY means the attribute must be present when the entity is captured or issued; OPTIONAL means it may be omitted. |
sdPolicy | The 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). |
format | A value-format constraint such as a regular expression or an ISO 8601 date pattern, used for validation and to convey date/time precision. |
widgetHint | The preferred capture widget for a form rendering, such as TEXT_INPUT or PICKLIST. |
standard | The reference standard the values follow, such as "ISO 3166-1" for country codes. Informational provenance for the value domain. |
entryCodes | The 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. |
unit | The unit of measure for a numeric value, such as a currency or SI unit. |
sensitive | Marks the attribute as sensitive so renderers mask it in UIs and logs. |
i18n | Per-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. |
classification | Governance: { 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. |
processing | Governance: { purposes: [ { purpose, legalBasis?, retention? } ], operations: [] }. The DPV processing purposes, each with its GDPR Article 6 legal basis and its own retention rule. |
residency | Governance: { allowed, denied, localizationRequired?, transfer?: { mechanism } }. Where the value may reside and the Chapter V transfer mechanism for permitted cross-border movement. |
assurance | Governance: { 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:
| Field | Meaning |
|---|---|
name | The relationship name, used as a path segment in traversal (e.g. residentialAddress). |
relationType | A semantic type token (e.g. has-address, employed-by). |
source | The source end: { entity: { entity: "<Name>" }, role?, cardinality: { min, max? } }. |
target | The target end: { entity: { entity: "<Name>" }, role?, cardinality: { min, max? } }. |
overlays.i18n | Localized 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 apurposeand anorigin. - Schema relationships (
/catalogs/{id}/schemarelationships): per-attribute mappings between a canonical attributesemanticPathand an external schema attribute, with arelationtype (DERIVED_FROM_EXTERNAL,EQUIVALENT_TO_EXTERNAL, etc.) and adirection. These anchor provenance in the model, for example, this canonicalbirth_dateisDERIVED_FROM_EXTERNALthe PIDbirthdateclaim, 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:
| Entity | Kind | partyType | Attributes |
|---|---|---|---|
| Person | PREDEFINED | natural_person | given_name, family_name, email |
| Address | PREDEFINED | address | street, postal_code, city, country |
| Organization | PREDEFINED | organization | legal_name, registration_number |
| Employee | SPECIALIZATION of Person | , | employee_id, job_title, employment_status |
| Employer | SPECIALIZATION of Organization | , | department |
| Relationship | Type | Source | Target |
|---|---|---|---|
residentialAddress | has-address | Person (cardinality min: 0) | Address, role home (cardinality min: 1, max: 1) |
employment | employed-by | Employee (cardinality min: 0) | Employer, role employer (cardinality min: 1, max: 1) |
Step 1: Create the catalog shell
- Request
- Response
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"]
}
}
}
201 Created returns the bare SemanticCatalog with status: "DRAFT" and version: 1. Capture .id and .version.
{
"id": "5b8c...catalogId",
"name": "Acme Employee Catalog (layered)",
"version": 1,
"status": "DRAFT",
"ownerPartyRef": "acme"
}
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)
- Request
- Response
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" }
}
}
}
200 OK returns the updated SemanticCatalog with the entity appended to entities (count now 1). The same shape is returned by every entity and relationship add (attribute overlays elided):
{
"id": "5b8c...catalogId",
"name": "Acme Employee Catalog (layered)",
"version": 1,
"status": "DRAFT",
"ownerPartyRef": "acme",
"entities": [
{
"name": "Person",
"kind": "PREDEFINED",
"partyType": "natural_person",
"attributes": [
{ "path": ["given_name"], "valueType": "Text", "overlays": { /* conformance, sdPolicy, i18n, classification */ } },
{ "path": ["family_name"], "valueType": "Text", "overlays": { /* ... */ } },
{ "path": ["email"], "valueType": "Text", "overlays": { /* ... */ } }
],
"overlays": { "i18n": { "en": { "label": "Person", "information": "A natural person" } } }
}
],
"relationships": []
}
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.
- Request
- Response
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" }
}
}
}
The updated catalog is returned. The country attribute carries entryCodes (the code list itself) in overlays.entryCodes and the human-readable display strings under overlays.i18n.{lang}.entries.
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.
- Request
- Response
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" }
}
}
}
The updated catalog is returned with Organization appended to entities.
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.
- Request
- Response
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" }
}
}
}
The updated catalog is returned. Employee contributes employee_id, job_title, and employment_status; it inherits given_name, family_name, and email from Person through the specialization.
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.
- Request
- Response
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" }
}
}
}
The updated catalog is returned with all five entities in entities.
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)
- Request
- Response
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" }
}
}
}
200 OK returns the updated catalog with the relationship appended to relationships (entities elided):
{
"id": "5b8c...catalogId",
"name": "Acme Employee Catalog (layered)",
"version": 1,
"status": "DRAFT",
"ownerPartyRef": "acme",
"entities": [ /* Person, Address, Organization, Employee, Employer */ ],
"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)
- Request
- Response
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" }
}
}
}
200 OK returns the updated catalog with both relationships in relationships (entities elided):
{
"id": "5b8c...catalogId",
"version": 1,
"status": "DRAFT",
"entities": [ /* the five entities */ ],
"relationships": [
{
"name": "residentialAddress",
"relationType": "has-address",
"source": { "entity": { "entity": "Person" }, "cardinality": { "min": 0 } },
"target": { "entity": { "entity": "Address" }, "role": "home", "cardinality": { "min": 1, "max": 1 } }
},
{
"name": "employment",
"relationType": "employed-by",
"source": { "entity": { "entity": "Employee" }, "cardinality": { "min": 0 } },
"target": { "entity": { "entity": "Employer" }, "role": "employer", "cardinality": { "min": 1, "max": 1 } }
}
]
}
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.
- Publish
- Resolve
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.
GET /api/attributes/v1/catalogs/{catalogId}/resolved
200 OK returns the fully resolved catalog with all entities and their attributes, plus all relationships (here, 5 entities and 2 relationships). Use this to verify the catalog is complete before building the profile (attributes elided):
{
"id": "5b8c...catalogId",
"version": 1,
"status": "PUBLISHED",
"entities": [
{ "name": "Person", "kind": "PREDEFINED", "partyType": "natural_person", "attributes": [ /* given_name, family_name, email */ ] },
{ "name": "Address", "kind": "PREDEFINED", "partyType": "address", "attributes": [ /* street, postal_code, city, country */ ] },
{ "name": "Organization", "kind": "PREDEFINED", "partyType": "organization", "attributes": [ /* legal_name, registration_number */ ] },
{ "name": "Employee", "kind": "SPECIALIZATION", "specializes": { "entity": "Person" }, "attributes": [ /* employee_id, job_title, employment_status */ ] },
{ "name": "Employer", "kind": "SPECIALIZATION", "specializes": { "entity": "Organization" }, "attributes": [ /* department */ ] }
],
"relationships": [
{ "name": "residentialAddress", "relationType": "has-address", "source": { "entity": { "entity": "Person" } }, "target": { "entity": { "entity": "Address" }, "role": "home" } },
{ "name": "employment", "relationType": "employed-by", "source": { "entity": { "entity": "Employee" } }, "target": { "entity": { "entity": "Employer" }, "role": "employer" } }
]
}
Listing catalogs (the paged envelope)
Every list endpoint in the /api/attributes/v1 family is paginated and filterable uniformly.
- Request
- Response
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
200 OK. The body is always the { data, pagination } envelope, read the page from .data (never a bare top-level array) and the counts from .pagination.
{
"data": [
{
"id": "5b8c...catalogId",
"name": "Acme Employee Catalog (layered)",
"version": 1,
"status": "PUBLISHED"
}
],
"pagination": {
"limit": 10,
"offset": 0,
"page": 0,
"size": 1,
"total": 1,
"totalPages": 1,
"hasMore": false
}
}
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