How to Add a Custom SD-JWT Credential Type
This guide walks through the end-to-end process of defining a new verifiable credential type and integrating it with a SIROS ID issuer and verifier. It covers creating the credential type metadata, configuring issuance, and enabling verification.
The example throughout this guide uses a fictional Employee Badge credential issued by an organization to its employees.
Prerequisites
- A GitHub account (for publishing credential type metadata)
- Access to a SIROS ID issuer (hosted or self-hosted)
- Access to a SIROS ID verifier (hosted or self-hosted)
- A test wallet — open id.siros.org and create one if you haven't already
Overview
Adding a custom credential type involves three phases:
- Define the credential type by creating a VCTM (Verifiable Credential Type Metadata) file and publishing it
- Configure an issuer to construct and sign credentials of this type
- Configure a verifier to request and validate presentations of this type
Phase 1: Define the Credential Type (VCTM)
A VCTM file defines everything wallets and verifiers need to know about your credential type: the claims it contains, how it should be displayed, and which claims support selective disclosure.
Step 1: Author the Credential Definition
Create a markdown file describing your credential. For the Employee Badge example:
---
vct: https://example.com/credentials/employee-badge
background_color: "#1a365d"
text_color: "#ffffff"
---
# Employee Badge
An employee identification credential issued by an organization
to verify employment status and role.
## Claims
- `given_name` (string): Employee's given name [mandatory] [sd=always]
- `family_name` (string): Employee's family name [mandatory] [sd=always]
- `email` (string): Employee's work email address [mandatory] [sd=always]
- `employee_id` (string): Employee identifier [mandatory] [sd=always]
- `department` (string): Department name [sd=always]
- `role` (string): Job title or role [sd=always]
- `hire_date` (date): Date of hire [sd=always]
## Images

Key elements:
| Element | Purpose |
|---|---|
vct (front matter) | Unique identifier for this credential type. Used in OID4VCI and OID4VP protocols. |
background_color, text_color | Display hints for wallets rendering the credential card. |
[mandatory] | Marks a claim as required in every issued credential. |
[sd=always] | Enables selective disclosure — the holder can choose whether to share this claim. |
The vct value is a URI that uniquely identifies your credential type. Use a domain you control. For EU-regulated credentials, URN-based identifiers following the urn:eudi: scheme are conventional (e.g., urn:eudi:pid:arf-1.8:1).
You can also write a .vctm.json file directly instead of markdown (see Option B: Write VCTM JSON directly below).
Step 2: Publish to a Credential Type Registry
Once you have your credential definition, you need to publish it so issuers, verifiers, and wallets can discover it. There are two approaches:
Option A: Publish to registry.siros.org (Recommended for Public Credentials)
registry.siros.org is the public SIROS Credential Type Registry. It is built automatically by registry-cli and hosted on GitHub Pages. GitHub is the write API — there are no PUT/DELETE endpoints. Instead, you publish credentials by pushing to a GitHub repository, and the registry discovers them automatically.
How registry.siros.org works:
- A
sources.yamlfile declares which GitHub repositories to scan - Repositories tagged with the
vctmGitHub topic are autodiscovered — no manual registration needed - A GitHub Actions workflow runs
registry-cli buildevery 6 hours, cloning all source repositories and detecting credential definitions - The built site is deployed to GitHub Pages at
https://registry.siros.org
To get your credential listed on registry.siros.org:
- Use the vctm-template — click "Use this template" to create your own repository
- Place your credential markdown file(s) in the
credentials/directory - Push to the
mainbranch — the included GitHub Action (mtcvctm) converts your markdown to.vctm.jsonon thevctmbranch - Tag your repository with the
vctmGitHub topic so the registry autodiscovers it
After the next registry build cycle (up to 6 hours), your credential appears at:
https://registry.siros.org/<your-org>/<slug>.vctm.json
The registry also provides a TS11-compliant API with JWS-signed responses:
https://registry.siros.org/api/v1/schemas.json # All schemas
https://registry.siros.org/api/v1/schemas/<id>.json # Individual schema
Since GitHub is the write API, access control is handled through GitHub's standard mechanisms: repository permissions, branch protection rules, and pull request reviews. To add or update a credential, you push a commit. To remove one, you delete or untag the repository.
Option B: Run Your Own Registry (For Private or On-Premise Deployments)
If you need a private credential catalogue, want full control over the build pipeline, or operate in an air-gapped environment, you can run registry-cli yourself. A Docker image is published to ghcr.io/sirosfoundation/registry-cli for every release.
Key differences from registry.siros.org:
| registry.siros.org | Self-hosted registry | |
|---|---|---|
| Hosting | GitHub Pages (static) | You choose: any static file server, S3, Caddy, nginx, etc. |
| Source discovery | GitHub topic autodiscovery | Any mix of GitHub topics, explicit git URLs, or local directories |
| Build trigger | GitHub Actions (every 6 hours) | You decide: cron, CI/CD, manual |
| Access control | GitHub repository permissions | Your infrastructure's access controls |
| JWS signing | PKCS#11 with production HSM | Dev keys, SoftHSM, or your own HSM |
| Credential sources | Public GitHub repositories only | Git repos (public or private), local directories (file://) |
Quick start with Docker Compose:
- Create a
sources.yaml:
defaults:
branch: vctm
sources:
# Autodiscover from your GitHub org
- "github:topic/vctm?org=your-org"
# Explicit private repository
- "git:https://github.com/your-org/private-credentials.git"
# Local directory (useful for air-gapped environments)
- url: "file:///data/sources/local-creds"
organization: "MyOrg"
- Run with Docker Compose:
services:
registry:
image: ghcr.io/sirosfoundation/registry-cli:latest
ports:
- "8080:8080"
volumes:
- ./sources:/data/sources:ro
- ./output:/data/output
environment:
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
docker compose up
# Open http://localhost:8080
Set GITHUB_TOKEN if your sources include GitHub topic searches or private repositories.
- For production, build the static site and deploy it to your web server:
docker run --rm \
-v ./sources:/data/sources:ro \
-v ./output:/data/output \
-e GITHUB_TOKEN="${GITHUB_TOKEN}" \
ghcr.io/sirosfoundation/registry-cli:latest \
registry-cli build \
--sources /data/sources/sources.yaml \
--output /data/output \
--base-url https://registry.your-org.com
Then serve the ./output directory with any static file server.
See the registry-cli documentation for full configuration options including JWS signing, SoftHSM setup, and TS11 compliance testing.
Option B: Write the VCTM JSON Directly
If you prefer not to use the markdown authoring workflow, you can write a VCTM JSON file directly following the SD-JWT VC Type Metadata specification. A minimal example:
{
"vct": "https://example.com/credentials/employee-badge",
"name": "Employee Badge",
"description": "Employee identification credential",
"display": [
{
"lang": "en",
"name": "Employee Badge",
"description": "Verifies employment status and role",
"rendering": {
"simple": {
"logo": {
"uri": "https://example.com/logo.svg",
"alt_text": "Example Corp"
},
"background_color": "#1a365d",
"text_color": "#ffffff"
}
}
}
],
"claims": [
{
"path": ["given_name"],
"display": [{"lang": "en", "label": "Given Name"}],
"sd": "always"
},
{
"path": ["family_name"],
"display": [{"lang": "en", "label": "Family Name"}],
"sd": "always"
},
{
"path": ["email"],
"display": [{"lang": "en", "label": "Email"}],
"sd": "always"
},
{
"path": ["employee_id"],
"display": [{"lang": "en", "label": "Employee ID"}],
"sd": "always"
},
{
"path": ["department"],
"display": [{"lang": "en", "label": "Department"}],
"sd": "always"
},
{
"path": ["role"],
"display": [{"lang": "en", "label": "Role"}],
"sd": "always"
},
{
"path": ["hire_date"],
"display": [{"lang": "en", "label": "Hire Date"}],
"sd": "always"
}
]
}
Host this file at a stable URL accessible to your issuer, or place it in a repository that your registry discovers. Both registry.siros.org and self-hosted registries detect .vctm.json files automatically.
Phase 2: Configure the Issuer
With the credential type defined, configure the SIROS ID issuer to construct and sign credentials of this type. The issuer needs to know:
- Where to find the VCTM
- How to authenticate users
- How to map identity claims to credential claims
2.1 Place the VCTM File
Make the VCTM file available to the issuer. For Docker deployments, mount it into the container:
services:
issuer:
volumes:
- ./metadata/employee-badge.vctm.json:/metadata/vctm_employee_badge.json:ro
2.2 Add the Credential Constructor
Add an entry to the credential_constructor section of your issuer configuration. The auth_method determines how users authenticate before receiving the credential.
Using OIDC Authentication
If your organization has an OIDC identity provider (Keycloak, Azure AD, Okta, etc.):
credential_constructor:
employee_badge:
vctm_file_path: "/metadata/vctm_employee_badge.json"
auth_method: oidc
format: "dc+sd-jwt"
apigw:
oidcrp:
enabled: true
client_id: "issuer-client"
client_secret: "${OIDC_CLIENT_SECRET}"
provider_metadata_url: "https://keycloak.example.com/realms/corp/.well-known/openid-configuration"
scopes:
- openid
- profile
- email
credential_config_id: "employee_badge"
The issuer maps OIDC claims from the ID token to credential claims automatically when claim names match (e.g., given_name → given_name). For non-matching names, configure explicit mappings.
Using SAML Authentication
For organizations with SAML-based identity federations:
credential_constructor:
employee_badge:
vctm_file_path: "/metadata/vctm_employee_badge.json"
auth_method: saml
format: "dc+sd-jwt"
apigw:
saml:
enabled: true
entity_id: "https://issuer.example.com/sp"
acs_endpoint: "https://issuer.example.com/saml/acs"
certificate_path: "/pki/sp-cert.pem"
private_key_path: "/pki/sp-key.pem"
credential_mappings:
- credential_config_id: "employee_badge"
entity_ids:
- "https://idp.example.com/idp"
attributes:
"urn:oid:2.5.4.42":
claim: "given_name"
required: true
"urn:oid:2.5.4.4":
claim: "family_name"
required: true
"urn:oid:0.9.2342.19200300.100.1.3":
claim: "email"
required: true
Using Pre-Authorized Code (API Integration)
For server-to-server issuance where your backend pushes credential data directly:
credential_constructor:
employee_badge:
vctm_file_path: "/metadata/vctm_employee_badge.json"
auth_method: basic
format: "dc+sd-jwt"
issuer:
pre_authorized_code:
enabled: true
pin_required: false
code_ttl: 300
Then issue credentials using the REST API:
# 1. Create an identity mapping (if not already created)
curl -X POST https://issuer.example.com/api/v1/identity/mapping \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"authentic_source": "hr.example.org",
"authentic_source_person_id": "EMP-12345",
"attributes": {
"family_name": "Smith",
"given_name": "Alice",
"birth_date": "1990-05-15"
}
}'
# Response includes an "id" field — use it in the next step
# 2. Upload document data to the datastore
curl -X POST https://issuer.example.com/api/v1/datastore \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"meta": {
"authentic_source": "hr.example.org",
"scope": "employee-badge",
"document_id": "emp-badge-001"
},
"identity_mapping_ids": ["<mapping_id_from_step_1>"],
"document_data": {
"given_name": "Alice",
"family_name": "Smith",
"email": "alice.smith@example.com",
"employee_id": "EMP-12345",
"department": "Engineering",
"role": "Senior Developer",
"hire_date": "2023-03-15"
}
}'
Once uploaded, the credential offer is available at the issuer's /offers UI endpoint. Users can scan the QR code or follow the deep link to receive the credential in their wallet.
See API Integration for the full API reference.
2.3 Choosing an Auth Method
| Auth Method | When to Use |
|---|---|
oidc | Your organization uses an OIDC identity provider (Keycloak, Azure AD, Okta) |
saml | Your organization participates in a SAML federation (eduGAIN, national eID) |
basic | Your backend system has verified user data and pushes it via API |
openid4vp | Users must present an existing credential to prove eligibility |
For the openid4vp method, you also specify which credential types and claims the user must present:
credential_constructor:
employee_badge:
vctm_file_path: "/metadata/vctm_employee_badge.json"
auth_method: openid4vp
auth_scopes: ["pid_1_8"]
auth_claims: ["given_name", "family_name", "birthdate"]
format: "dc+sd-jwt"
Phase 3: Configure the Verifier
The verifier needs to know which credential types it accepts and how to map credential claims into the OIDC ID tokens it produces for your application.
3.1 Register Credential Scopes
Map an OIDC scope to your credential type so applications can request it:
verifier:
openid4vp:
supported_credentials:
- vct: "https://example.com/credentials/employee-badge"
scopes: ["employee"]
Applications then include employee in their OIDC scope parameter to trigger a presentation request for this credential.
3.2 Configure DCQL Queries (Optional)
For more granular control over which claims are requested, define a DCQL query:
credentials:
- id: employee_badge
format: vc+sd-jwt
meta:
vct_values:
- "https://example.com/credentials/employee-badge"
claims:
- path: ["given_name"]
- path: ["family_name"]
- path: ["employee_id"]
- path: ["department"]
3.3 Map Claims to OIDC ID Token
Configure how credential claims appear in the OIDC ID token returned to your application:
verifier:
claim_mapping:
given_name: "$.vc.credentialSubject.given_name"
family_name: "$.vc.credentialSubject.family_name"
email: "$.vc.credentialSubject.email"
employee_id: "$.vc.credentialSubject.employee_id"
department: "$.vc.credentialSubject.department"
Your application then receives these claims in a standard OIDC ID token:
{
"iss": "https://verifier.example.org",
"sub": "pairwise-user-id",
"aud": "your-client-id",
"given_name": "Alice",
"family_name": "Smith",
"email": "alice.smith@example.com",
"employee_id": "EMP-12345",
"department": "Engineering"
}
3.4 Register the Verifier Client
If your application doesn't already have a client registration with the verifier, register one:
curl -X POST https://verifier.example.org/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Application",
"redirect_uris": ["https://my-app.example.com/callback"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"scope": "openid profile employee"
}'
Phase 4: Establish Trust
For the verifier to accept credentials from your issuer, the issuer must be trusted. SIROS ID supports several trust frameworks — choose the one that fits your deployment:
| Trust Framework | Use Case | Documentation |
|---|---|---|
| URL Whitelist | Development and small deployments | Simple list of trusted issuer URLs |
| ETSI Trust Status Lists | EU-regulated environments | Trust Infrastructure |
| Lists of Trusted Entities (LoTE) | JSON-based trust lists | LoTE Publishing |
| OpenID Federation | Dynamic, federated trust | OpenID Federation |
For development, a URL whitelist is the simplest approach. For production, use the trust framework required by your regulatory environment.
See Trust Services for detailed setup instructions.
Testing the Full Flow
1. Issue a Test Credential
- Open the SIROS ID Credential Manager (or any OID4VCI-compatible wallet)
- Navigate to Add Credential and select your issuer
- Authenticate with your identity provider
- Accept the credential — it should appear in your wallet with the display properties you defined in the VCTM
2. Verify the Credential
- In your application, trigger a login that uses the verifier as identity provider
- The verifier displays a QR code (or uses the W3C Digital Credentials API if supported by the browser)
- Scan the QR code with your wallet
- Review the claims being requested and approve
- Your application receives the mapped claims in the OIDC ID token
3. Verify Selective Disclosure
Test that selective disclosure works correctly:
- Configure the verifier to request only a subset of claims (e.g.,
given_nameanddepartment) - Verify that the wallet only asks the user to share those specific claims
- Confirm the ID token contains only the requested claims
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| Credential not appearing in wallet | VCTM not found or invalid | Check that the VCTM file is mounted correctly and valid JSON |
| Authentication fails during issuance | IdP misconfiguration | Verify redirect URIs, client credentials, and scopes |
| Verifier rejects credential | Issuer not trusted | Add the issuer to the trust framework (see Phase 4) |
| Claims missing in ID token | Claim mapping mismatch | Check claim_mapping paths against actual credential structure |
| Wallet shows raw claim names | Missing VCTM display metadata | Add display entries for each claim in the VCTM |
Next Steps
- registry-cli — Self-hosted credential type registry
- registry.siros.org — Public SIROS Credential Type Registry
- Issuer Configuration — Full issuer configuration reference
- Verifier Configuration — Full verifier configuration reference
- API Integration — Server-to-server credential issuance
- Trust Services — Trust framework setup
- Credential Type Registry — Publishing credential metadata