Channels (L4)
L4 is the channel-rendering layer. A product-backed channel binds exactly one Data Product (L3) to a concrete output:
- a form with fields and widgets;
- a PDF document with placements and repeated sections;
- an API payload with an external naming convention and path mappings; or
- a verifiable credential with credential-format metadata and claim mappings.
An OID4VCI issuance channel is different. It composes one or more verifiable-credential channels through independent credential configurations and does not bind a Data Product directly.
Every product-backed channel carries the same setRef wire field. Its value is a DataProductRef with dataProductId and dataProductVersion. The field name is shared by the channel contract; the referenced resource is the L3 Data Product.
These layers are authored over REST. VDX presents a guided UI for the same channel model.
Channel Routes and Lifecycle
The API is rooted at /api/model/v1:
| Channel | Create | Replace |
|---|---|---|
| Forms | POST /channels/forms | PUT /channels/forms/{channelId} |
| PDF documents | POST /channels/pdf | PUT /channels/pdf/{channelId} |
| Verifiable credentials | POST /channels/credentials | PUT /channels/credentials/{channelId} |
| OID4VCI issuance | POST /channels/oid4vci | PUT /channels/oid4vci/{channelId} |
| API payloads | POST /channels/api | PUT /channels/api/{channelId} |
Channel-neutral reads use GET /api/model/v1/channels/{channelId} and listing uses GET /api/model/v1/channels. The channel list returns a paged response with data and pagination.
The model lifecycle values are DRAFT and PUBLISHED. Channel responses include status in their concrete payload. Form channels also expose PUT /channels/forms/{channelId}/publication with { "published": true | false }. Branding has its own status endpoint, described below.
The Channel Envelope
Channel create, update, get, and list operations return a ChannelEnvelope. Its wire shape is a type discriminator plus the concrete payload:
{
"channel": "VERIFIABLE_CREDENTIAL",
"payload": {
"id": "9f10...sdJwtChannelId",
"tenantId": "acme",
"name": "Acme Employee - SD-JWT",
"version": 1,
"channel": "VERIFIABLE_CREDENTIAL",
"setRef": {
"dataProductId": "1b2c...dataProductId",
"dataProductVersion": 1
},
"fieldMappings": [],
"credentialFormat": "dc+sd-jwt",
"vct": "https://issuer.acme.example/employee",
"vctHostingMode": "HOSTED",
"claimMappings": [
{
"set": { "roleRef": "employee", "path": ["given_name"] },
"disclosure": "ALWAYS"
},
{
"set": { "roleRef": "employee", "path": ["residentialAddress", "country"] },
"claimPath": ["address", "country"],
"disclosure": "SELECTIVE"
}
],
"brandingRef": {
"brandingId": "b1f2...brandingId",
"version": 1
},
"status": "DRAFT"
}
}
Read channel to determine the concrete channel type, then decode payload as the matching channel. The concrete payload repeats the channel discriminator inline.
The Product-Backed VC Channel
A verifiable-credential channel binds exactly one Data Product to one credential format. It carries the credential wire identity and maps credential claims to Data Product selections. The same channel can supply the shape used by issuer design generation and verifier DCQL generation.
The channel is role-neutral:
- an issuer can create a credential design from it;
- a verifier can derive a DCQL query from it.
The key fields are:
| Field | Meaning |
|---|---|
setRef | { dataProductId, dataProductVersion }, pinning the Data Product snapshot. |
credentialFormat | The wire format, such as dc+sd-jwt, vc+sd-jwt, mso_mdoc, or jwt_vc_json. |
vct | The SD-JWT VC type identifier when the format uses a VCT. |
vctHostingMode | Optional hosting intent: HOSTED, EXTERNAL, or NONE. |
doctype | The mdoc doctype when credentialFormat is mso_mdoc. |
namespace | Channel-level mdoc namespace. It is an identifier, not a claim-path segment. |
jsonLdContexts | JSON-LD @context URIs for a JSON-LD VC. |
claimMappings | Claim-to-Data-Product mappings with an optional wire claimPath, optional mdoc namespace, and disclosure. |
brandingRef | { brandingId, version }, pinning a credential-branding snapshot. |
Each claimMappings entry uses set: { roleRef, path } to identify a selected Data Product path. disclosure is ALWAYS or SELECTIVE. When claimPath is omitted, it defaults to the Data Product path. When it is present, it is a flat array of clean segments. An mdoc namespace is supplied by namespace, never by adding the namespace to claimPath.
Credential Branding
Credential branding is a reusable L4 presentation overlay. It carries the localized credential display name, description, logo, colors, optional background image, and optional layout hint. It is authored once per credential type and referenced by each format-specific VC channel through brandingRef.
Create branding
- Request
- Response
POST /api/model/v1/branding
{
"name": "Acme Employee Credential branding",
"description": "Presentation overlay for the Acme Employee credential.",
"layout": "card-default",
"locales": [
{
"language": "en",
"name": "Employee Credential",
"description": "Acme Corp employee identity credential",
"logoUri": "https://acme.example/logo.png",
"backgroundColor": "#1A237E",
"textColor": "#FFFFFF",
"backgroundImageUri": null
},
{
"language": "nl",
"name": "Medewerkerscredential",
"description": "Acme Corp medewerkersidentiteitscredential",
"logoUri": "https://acme.example/logo.png",
"backgroundColor": "#1A237E",
"textColor": "#FFFFFF",
"backgroundImageUri": null
}
]
}
201 Created returns the bare CredentialBranding. The record starts at version: 1 and status: "DRAFT".
{
"id": "b1f2...brandingId",
"tenantId": "acme",
"name": "Acme Employee Credential branding",
"version": 1,
"status": "DRAFT",
"locales": [
{
"language": "en",
"name": "Employee Credential",
"description": "Acme Corp employee identity credential",
"logoUri": "https://acme.example/logo.png",
"backgroundColor": "#1A237E",
"textColor": "#FFFFFF",
"backgroundImageUri": null
}
],
"layout": "card-default",
"description": "Presentation overlay for the Acme Employee credential.",
"createdAt": "2026-01-15T09:30:00Z",
"updatedAt": "2026-01-15T09:30:00Z"
}
Use PATCH /api/model/v1/branding/{brandingId} to update draft metadata, locales, or layout. Use PUT /api/model/v1/branding/{brandingId}/status to set DRAFT or PUBLISHED:
{ "status": "PUBLISHED" }
The version can be incremented with POST /api/model/v1/branding/{brandingId}/version, which has no request body. A VC channel references the exact published branding snapshot with brandingRef.
Create the Employee SD-JWT VC Channel
This channel binds the published Data Product to dc+sd-jwt. It maps eight selected paths to credential claims. The relationship traversal residentialAddress.country is placed under address.country on the wire.
- Request
- Response
POST /api/model/v1/channels/credentials
{
"name": "Acme Employee - SD-JWT",
"setRef": {
"dataProductId": "1b2c...dataProductId",
"dataProductVersion": 1
},
"credentialFormat": "dc+sd-jwt",
"vct": "https://issuer.acme.example/employee",
"vctHostingMode": "HOSTED",
"brandingRef": {
"brandingId": "b1f2...employeeBrandingId",
"version": 1
},
"claimMappings": [
{ "set": { "roleRef": "employee", "path": ["given_name"] }, "disclosure": "ALWAYS" },
{ "set": { "roleRef": "employee", "path": ["family_name"] }, "disclosure": "ALWAYS" },
{ "set": { "roleRef": "employee", "path": ["employee_id"] }, "disclosure": "SELECTIVE" },
{ "set": { "roleRef": "employee", "path": ["job_title"] }, "disclosure": "ALWAYS" },
{ "set": { "roleRef": "employee", "path": ["employment_status"] }, "disclosure": "SELECTIVE" },
{ "set": { "roleRef": "employee", "path": ["email"] }, "disclosure": "SELECTIVE" },
{
"set": { "roleRef": "employee", "path": ["residentialAddress", "country"] },
"claimPath": ["address", "country"],
"disclosure": "SELECTIVE"
},
{
"set": { "roleRef": "employer", "path": ["legal_name"] },
"claimPath": ["employer"],
"disclosure": "ALWAYS"
}
]
}
201 Created returns a ChannelEnvelope. The channel id and version are in payload.
{
"channel": "VERIFIABLE_CREDENTIAL",
"payload": {
"id": "9f10...sdJwtChannelId",
"tenantId": "acme",
"name": "Acme Employee - SD-JWT",
"version": 1,
"channel": "VERIFIABLE_CREDENTIAL",
"setRef": {
"dataProductId": "1b2c...dataProductId",
"dataProductVersion": 1
},
"fieldMappings": [],
"credentialFormat": "dc+sd-jwt",
"vct": "https://issuer.acme.example/employee",
"vctHostingMode": "HOSTED",
"claimMappings": [
{
"set": { "roleRef": "employee", "path": ["given_name"] },
"disclosure": "ALWAYS"
},
{
"set": { "roleRef": "employee", "path": ["residentialAddress", "country"] },
"claimPath": ["address", "country"],
"disclosure": "SELECTIVE"
}
],
"brandingRef": {
"brandingId": "b1f2...employeeBrandingId",
"version": 1
},
"status": "DRAFT"
}
}
PUT /api/model/v1/channels/credentials/{channelId} replaces the VC channel content and returns the same envelope shape.
Create the Business Card mdoc VC Channel
This channel binds the same Data Product to mso_mdoc. The channel-level namespace is combined with each claim element by the mdoc converter. doctype and namespace are separate fields.
- Request
- Response
POST /api/model/v1/channels/credentials
{
"name": "Acme Business Card - mdoc",
"setRef": {
"dataProductId": "1b2c...dataProductId",
"dataProductVersion": 1
},
"credentialFormat": "mso_mdoc",
"doctype": "org.acme.businesscard.1",
"namespace": "org.acme.businesscard.1",
"brandingRef": {
"brandingId": "b3c4...businessCardBrandingId",
"version": 1
},
"claimMappings": [
{ "set": { "roleRef": "employee", "path": ["given_name"] }, "disclosure": "ALWAYS" },
{ "set": { "roleRef": "employee", "path": ["family_name"] }, "disclosure": "ALWAYS" },
{ "set": { "roleRef": "employee", "path": ["job_title"] }, "disclosure": "ALWAYS" },
{ "set": { "roleRef": "employee", "path": ["email"] }, "disclosure": "SELECTIVE" },
{
"set": { "roleRef": "employee", "path": ["residentialAddress", "postal_code"] },
"claimPath": ["postal_code"],
"namespace": "org.acme.businesscard.1",
"disclosure": "SELECTIVE"
}
]
}
The response is a ChannelEnvelope whose outer channel is VERIFIABLE_CREDENTIAL. The concrete mdoc channel is in payload, with credentialFormat: "mso_mdoc", doctype, and namespace.
Create the OID4VCI Issuance Channel
An OID4VCI issuance channel composes the VC channels through credentialConfigurations. It has no setRef. Each configuration pins one VC channel and declares its own credential-configuration id, signing setup, and offer defaults.
- Request
- Response
POST /api/model/v1/channels/oid4vci
{
"name": "Acme Employee and Business Card issuance",
"credentialConfigurations": [
{
"credentialConfigurationId": "EmployeeCredential",
"channelRef": {
"channelId": "9f10...sdJwtChannelId",
"channelVersion": 1
},
"signing": {
"algorithm": "ES256",
"keyBinding": "did:jwk"
},
"offer": {
"grants": ["AUTHORIZATION_CODE"],
"txCodeRequired": false,
"validitySeconds": 600
}
},
{
"credentialConfigurationId": "BusinessCardMdoc",
"channelRef": {
"channelId": "a3b4...mdocChannelId",
"channelVersion": 1
},
"signing": {
"algorithm": "EdDSA",
"keyBinding": "x5c"
},
"offer": {
"grants": ["PRE_AUTHORIZED_CODE"],
"txCodeRequired": true,
"validitySeconds": 300
}
}
]
}
201 Created returns a ChannelEnvelope with outer channel: "OID4VCI". The concrete payload contains credentialConfigurations and status: "DRAFT".
{
"channel": "OID4VCI",
"payload": {
"id": "c5d6...oid4vciChannelId",
"tenantId": "acme",
"name": "Acme Employee and Business Card issuance",
"version": 1,
"channel": "OID4VCI",
"credentialConfigurations": [
{
"credentialConfigurationId": "EmployeeCredential",
"channelRef": {
"channelId": "9f10...sdJwtChannelId",
"channelVersion": 1
},
"signing": {
"algorithm": "ES256",
"keyBinding": "did:jwk"
},
"offer": {
"grants": ["AUTHORIZATION_CODE"],
"txCodeRequired": false,
"validitySeconds": 600
}
}
],
"status": "DRAFT"
}
}
PUT /api/model/v1/channels/oid4vci/{channelId} replaces the issuance channel content and returns the same envelope shape. The credentialConfigurationId values are the identifiers advertised by the issuer and referenced by its offers.
List Channels and Branding
Use the channel list filters nameContains, channel, and dataProductId together with the shared pagination parameters:
GET /api/model/v1/channels?channel=VERIFIABLE_CREDENTIAL&dataProductId=1b2c...dataProductId&limit=10&offset=0
The response is a { data, pagination } envelope. Each item in data is a ChannelEnvelope; read the concrete channel at .data[].payload.
{
"data": [
{
"channel": "VERIFIABLE_CREDENTIAL",
"payload": {
"id": "9f10...sdJwtChannelId",
"name": "Acme Employee - SD-JWT",
"version": 1,
"channel": "VERIFIABLE_CREDENTIAL",
"credentialFormat": "dc+sd-jwt",
"vct": "https://issuer.acme.example/employee",
"status": "DRAFT"
}
},
{
"channel": "OID4VCI",
"payload": {
"id": "c5d6...oid4vciChannelId",
"name": "Acme Employee and Business Card issuance",
"version": 1,
"channel": "OID4VCI",
"credentialConfigurations": [],
"status": "DRAFT"
}
}
],
"pagination": {
"limit": 10,
"offset": 0,
"page": 0,
"size": 2,
"total": 2,
"totalPages": 1,
"hasMore": false
}
}
Branding uses the corresponding /api/model/v1/branding resource. Its list response also uses { data, pagination }, and status accepts DRAFT or PUBLISHED.
License
The semantic-model authoring API is gated by the semantic-modeling.v1 enterprise feature. Every request in this walkthrough uses an operator bearer token, and the tenant context is resolved from that token.
What You Have Now
The example now has one Data Product, two VC channels in different credential formats, shared credential branding, and one OID4VCI issuance channel that composes both VC channels. Issuer design and verifier DCQL generation can consume the same VC channel, keeping the credential shape and requested shape aligned.
Next Steps
- Issuing: create issuer designs and offers from the VC channels.
- Verifying: derive a DCQL query from the same VC channels.
- Data Product (L3): review the role-rooted paths consumed by these channels.
- Modeling Your World: review the layer rules and governance model.