Issuing
With the two VC channels in place (Channels), the issuer now renders a credential design from each one and offers them to a wallet. The central idea on this page: in the enterprise model, the design is derived from the VC channel, not authored beside it. You supply only what the channel does not carry: an optional alias, the hosting mode, and a reference back to the channel. There is no claims[] array, no bindings[] array, and no displays in the request; the converter reads the channel's claimMappings, traverses the set selection and the profile roles, and takes the localized display (name, description, logo, colors) from the channel's branding.
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/designs/credentials/fromchannel | Render a credential design from a set-bound VC channel |
| POST | /oid4vci/backend/credential/offers | Create a credential offer for one or more configurations |
All calls use the operator's OIDC bearer token. The design-from-channel render is a license-gated enterprise write (it derives, persists, and records lineage); offer creation is a standard issuer backend call.
Rendering a Design From a VC Channel
The converter derives the claim list, selective-disclosure policy, ordering, credential-type identity, and the localized display directly from the VC channel you created in Channels (the display comes from the channel's brandingRef). The request body carries only what the channel does not already encode: an optional alias, the hostingMode, and a channelRef pointing at the VC channel by id and version.
The converter reads the channel's claimMappings (each entry linking a set traversal path to a wire claim path with a disclosure policy), resolves those paths through the set, the profile roles, and the catalog entities, and emits:
- the design claim list, with selective-disclosure policy and ordering flowing from the
claimMappingsand catalog governance metadata, and - the credential-type identity (
format,vctfor SD-JWT, ordoctypefor mdoc) taken directly from the VC channel.
It also writes one ISSUE-role usage-lineage row for each consumed traversal path. See Provenance and Operations.
Render the Employee SD-JWT design
- Request
- Response
POST /api/v1/designs/credentials/fromchannel
{
"channelRef": {
"channelId": "11aa...employeeSdjwtChannelId",
"channelVersion": 1
},
"alias": "Employee Credential",
"hostingMode": "LOCAL"
}
201 Created. Returns the derived design and its provenance. Capture the design id.
{
"designId": "d100...employeeDesignId",
"credentialConfigurationId": "EmployeeCredential",
"format": "dc+sd-jwt",
"vct": "https://issuer.acme.example/employee",
"claims": [
// DERIVED from the VC channel's claimMappings -- not supplied in the request.
// Each entry's path, disclosure policy, and ordering come from the claimMappings
// and the catalog governance metadata traversed through the set + profile.
{ "path": ["given_name"], "disclosure": "ALWAYS" },
{ "path": ["family_name"], "disclosure": "ALWAYS" },
{ "path": ["employee_id"], "disclosure": "SELECTIVE" },
{ "path": ["job_title"], "disclosure": "ALWAYS" },
{ "path": ["employment_status"], "disclosure": "SELECTIVE" },
{ "path": ["email"], "disclosure": "SELECTIVE" },
{ "path": ["address", "country"], "disclosure": "SELECTIVE" },
{ "path": ["employer"], "disclosure": "ALWAYS" }
],
"provenance": {
"role": "ISSUE",
"channelId": "11aa...employeeSdjwtChannelId",
"channelVersion": 1
}
}
Request fields
| Field | Required | Notes |
|---|---|---|
channelRef | yes | Object with channelId and channelVersion pointing at the set-bound VC channel. |
alias | no | A short internal label for the design. |
hostingMode | yes | LOCAL for a self-hosted design in this walkthrough. |
The absence of a claims[] array and a displays array is intentional. The claim list, selective-disclosure policy, ordering, and credential-type identity are derived from the VC channel and the catalog; the localized wallet display (name, description, logo, colors) comes from the channel's brandingRef. This is what keeps the issuer and verifier in agreement, and the display consistent across formats: all of it is authored on the model, not on the design.
The eight derived claims reflect the Employee SD-JWT VC channel's claimMappings. Note that address.country is a two-segment path derived from the set traversal ["residentialAddress","country"] (crossing the catalog's residentialAddress relationship from Employee to Address), and employer is derived from ["legal_name"] on the employer role.
Render the Business Card mdoc design
- Request
- Response
POST /api/v1/designs/credentials/fromchannel
{
"channelRef": {
"channelId": "22bb...businessCardMdocChannelId",
"channelVersion": 1
},
"alias": "Business Card",
"hostingMode": "LOCAL"
}
201 Created. The mdoc design derives the mso_mdoc format, the doctype, and the namespace from the VC channel, and lists the bare elements that the namespace places at issuance. Capture the design id.
{
"designId": "d200...businessCardDesignId",
"credentialConfigurationId": "BusinessCardMdoc",
"format": "mso_mdoc",
"doctype": "org.acme.businesscard.1",
"namespace": "org.acme.businesscard.1",
"claims": [
// Each path is the bare element; the design's namespace places them at issuance.
{ "path": ["given_name"], "disclosure": "ALWAYS" },
{ "path": ["family_name"], "disclosure": "ALWAYS" },
{ "path": ["job_title"], "disclosure": "ALWAYS" },
{ "path": ["email"], "disclosure": "SELECTIVE" },
{ "path": ["postal_code"], "disclosure": "SELECTIVE" }
],
"provenance": {
"role": "ISSUE",
"channelId": "22bb...businessCardMdocChannelId",
"channelVersion": 1
}
}
The five mdoc claims come from the Business Card VC channel's claimMappings. postal_code is derived from the set traversal ["residentialAddress","postal_code"], crossing the same residentialAddress relationship used by the SD-JWT design. Selective disclosure in mdoc is handled by the namespace and element structure at issuance, so the converter emits the mdoc-appropriate policy rather than SD-JWT disclosures.
Both designs trace back to the same Acme Employee set and profile, and each binds a distinct credentialConfigurationId (EmployeeCredential, BusinessCardMdoc) that the issuer advertises in /.well-known/openid-credential-issuer via the OID4VCI issuance channel created in the previous step.
Creating the Offers
A credential offer invites a wallet to obtain a credential. Create one offer per credential configuration: a wallet obtains a single credential per offer, so the Employee credential and the Business Card are offered separately. Each offer uses the grant its credential configuration declared: the Employee credential via authorization code (the wallet authenticates as the employee first), the Business Card via a pre-authorized code (the offer carries the code, no interactive login).
The credential_configuration_ids used below (EmployeeCredential, BusinessCardMdoc) are the ids the OID4VCI issuance channel assigned to each composed credential in Channels (L4); the issuer advertises them in /.well-known/openid-credential-issuer, and each design above carries its id as credentialConfigurationId.
Offer the Employee credential
- Request
- Response
POST /oid4vci/backend/credential/offers
{
"credential_configuration_ids": ["EmployeeCredential"],
"issuer_id": "http://acme.localhost:8088",
"grants": { "authorization_code": {} },
"ttl_seconds": 600
}
201 Created.
{
"correlation_id": "0d5cfd20-342a-4ff0-af78-17be9cacd775",
"offer_uri": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Facme.localhost%3A8088%2Fcredentials%2Foffers%2F..."
}
Offer the Business Card credential
The Business Card offer targets the other configuration. Its OID4VCI channel declared a pre-authorized grant, so this offer carries a pre-authorized code rather than requiring an interactive login:
{
"credential_configuration_ids": ["BusinessCardMdoc"],
"issuer_id": "http://acme.localhost:8088",
"grants": { "pre_authorized_code": { "tx_code_required": true } },
"ttl_seconds": 600
}
Each offer returns its own correlation_id and offer_uri.
Request fields
| Field | Required | Notes |
|---|---|---|
credential_configuration_ids[] | yes | The configuration this offer is for. Create one offer per credential; an offer carries a single credential. |
issuer_id | yes | The OID4VCI issuer identifier. |
grants | yes | The grant for this offer, matching the credential configuration: { "authorization_code": {} } (the wallet authenticates first) or { "pre_authorized_code": { "tx_code_required": true } } (the offer carries a pre-authorized code). |
ttl_seconds | no | Offer lifetime in seconds. |
The offer_uri is the OID4VCI offer deeplink. It can be rendered as a QR code, a link, an NFC tag, or a deeplink; the rendering is a presentation choice and does not change the offer.
For a "one URL, many holders" scenario, the offer create accepts a uriLifecycle of SINGLE_USE (default) or REUSABLE_FRESH_PER_FETCH. See the OpenID4VCI REST API for the compact offer endpoint and its options.
How Issuance Proceeds
The wallet drives the rest of the flow over the OpenID4VCI protocol. The protocol legs themselves are unchanged from the standard issuer and are documented in the OpenID4VCI guide. In summary, the wallet:
- Fetches
/.well-known/openid-credential-issuerand/.well-known/oauth-authorization-server. - Runs the authorization-code and PKCE login as the employee.
- Exchanges the code at
/tokenfor an access token. - Generates an ephemeral holder key and requests a
c_noncefrom/oid4vci/nonce. - Builds a JWT proof of possession over the issuer id and
c_noncewith the holder key. - Requests the credential at
/oid4vci/credential(carrying the proof) and receives the signed credential.
Those six legs are the authorization-code flow, which the Employee credential uses. The Business Card credential is configured pre-authorized: the wallet skips the login (steps 2 and 3), takes the pre-authorized code from the offer, exchanges it at /token (entering the tx_code if required), then continues from the nonce and proof steps.
The holder obtains each credential from its own offer. The issued SD-JWT carries the selected attributes as selective-disclosure entries, its iss and kid identify the issuer key, and its holder binding (cnf.jwk) is the wallet's ephemeral public key. The mdoc is signed with an X.509 x5chain (a CA-signed leaf), and the issuer binds the wallet's device public key into the mdoc MSO. Both credentials are then presented together in Verifying.
The claim shape each credential carries is exactly the shape the design derived from the VC channel, which is the same shape the verifier will request in Verifying because the verifier's DCQL query derives from the same VC channels.
The designs rendered here derive from the set-bound VC channels created in Channels. The OID4VCI issuance channel created in that same step composes both VC channels and is what the issuer uses to advertise the EmployeeCredential and BusinessCardMdoc configuration ids.
Next Steps
- Verifying: author a multi-credential DCQL from the same channels, request a presentation, and read the verified data
- Provenance and Operations: the ISSUE-role lineage rows this render wrote, and governance flow-through
- OpenID4VCI: the full issuer protocol and the attribute pipeline