Issuing Mobile Driver's Licenses (mDL) via OID4VCI
This guide shows you how to issue an ISO/IEC 18013-5 Mobile Driver's License (mDL) using the walt.id Issuer2 API. You'll issue a credential from a ready-made profile in seconds, then see how to define your own.
mDL (Mobile Driver's License): A digital representation of a driver's license based on the ISO/IEC 18013-5 standard, stored securely on a digital ID wallet.
OID4VCI: The protocol used to deliver the credential to a wallet (mDoc issuance follows OID4VCI per ISO 18013-7).
Issuer2 issues credentials from profiles — reusable configurations declared ahead of time in a config file, with sensible defaults for the issuer key and credential data. If you're used to the original Issuer (v1), where the key and credential data were posted directly to an issuance endpoint every time, see Getting Started for what changed — you can still override a profile's defaults per-offer via runtimeOverrides.
Prerequisites
Before you begin, ensure you have:
- Issuer2 API running — Follow the Setup guide
- A test wallet — Use the walt.id Wallet for testing, or any OID4VCI-compatible wallet with mDoc support
Local Development: When running locally via Docker, the API is available at http://localhost:7002 (standalone) or http://localhost:7005 (Docker Compose). Use this as your base URL in the examples below.
Quick Start: Issue Using the Default Profile
The waltid-identity repository ships with a ready-made profile called isoMdl, bound to the org.iso.18013.5.1.mDL credential type. You can issue from it immediately — no configuration file editing required.
Step 1: Confirm the Profile Is Loaded
Endpoint: GET /issuer2/profiles/{profileId} | API Reference
Example Request
curl -X 'GET' \
'http://localhost:7002/issuer2/profiles/isoMdl' \
-H 'accept: application/json'
Example Response
{
"profileId": "isoMdl",
"name": "ISO 18013-5 Mobile Driving License",
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"issuerKey": {
"type": "jwk",
"jwk": {
"d": "KJ4k3Vcl5Sj9Mfq4rrNXBm2MoPoY3_Ak_PIR_EgsFhQ",
"crv": "P-256",
"x": "G0RINBiF-oQUD3d5DGnegQuXenI29JDaMGoMvioKRBM",
"y": "ed3eFGs2pEtrp7vAZ7BLcbrUtpKkYWAT2JPUQK4lN4E",
"kty": "EC"
}
},
"credentialData": {
"org.iso.18013.5.1": {
"un_distinguishing_sign": "A",
"biometric_template_signature_sign": null,
"portrait_capture_date": "2024-12-20",
"age_over_16": true,
"age_over_18": true,
"given_name_national_character": "Anna Maria",
"given_name": "Anna Maria",
"birth_place": "Graz",
"family_name": "Musterfrau",
"age_birth_year": 1988,
"age_over_12": true,
"nationality": "AUT",
"age_over_13": true,
"age_over_14": true,
"weight": 63,
"issuing_jurisdiction": "AT-9",
"expiry_date": "2035-01-15",
"signature_usual_mark": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z0i8AAAAASUVORK5CYII=",
"biometric_template_finger": null,
"eye_colour": "brown",
"resident_postal_code": "1070",
"sex": 2,
"age_over_62": false,
"issuing_country": "AT",
"hair_colour": "brown",
"issue_date": "2025-01-15",
"age_over_60": false,
"age_in_years": 36,
"administrative_number": "AT-FS-99817234",
"age_over_65": false,
"resident_state": "Wien",
"resident_city": "Wien",
"document_number": "DL-AT-2025-00018427",
"portrait": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z0i8AAAAASUVORK5CYII=",
"resident_country": "AT",
"resident_address": "Mariahilfer Straße 120/8",
"age_over_68": false,
"driving_privileges": [
{
"issue_date": "2006-09-14",
"vehicle_category_code": "B",
"expiry_date": "2035-01-15"
},
{
"issue_date": "2004-08-25",
"vehicle_category_code": "AM",
"expiry_date": "2035-01-15"
}
],
"age_over_21": true,
"height": 170,
"biometric_template_iris": null,
"birth_date": "1988-08-25",
"age_over_25": true,
"biometric_template_face": null,
"issuing_authority": "Bundesministerium für Inneres",
"family_name_national_character": "Musterfrau"
}
},
"idTokenClaimsMapping": {
"$.family_name": "$.['org.iso.18013.5.1'].family_name",
"$.given_name": "$.['org.iso.18013.5.1'].given_name"
},
"mDocNameSpacesDataMappingConfig": {
"org.iso.18013.5.1": {
"entriesConfigMap": {
"portrait_capture_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"driving_privileges": {
"type": "array",
"arrayConfig": [
{
"type": "object",
"entriesConfigMap": {
"issue_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"expiry_date": {
"type": "string",
"conversionType": "stringToFullDate"
}
}
},
{
"type": "object",
"entriesConfigMap": {
"issue_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"expiry_date": {
"type": "string",
"conversionType": "stringToFullDate"
}
}
}
]
},
"issue_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"portrait": {
"type": "string",
"conversionType": "base64StringToByteString"
},
"birth_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"expiry_date": {
"type": "string",
"conversionType": "stringToFullDate"
},
"signature_usual_mark": {
"type": "string",
"conversionType": "base64StringToByteString"
}
}
}
},
"x5Chain": [
"-----BEGIN CERTIFICATE-----\nMIIBeTCCAR8CFHrWgrGl5KdefSvRQhR+aoqdf48+MAoGCCqGSM49BAMCMBcxFTATBgNVBAMMDE1ET0MgUk9PVCBDQTAgFw0yNTA1MTQxNDA4MDlaGA8yMDc1MDUwMjE0MDgwOVowZTELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmllbm5hMRAwDgYDVQQKDAd3YWx0LmlkMRAwDgYDVQQLDAd3YWx0LmlkMRAwDgYDVQQDDAd3YWx0LmlzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEG0RINBiF+oQUD3d5DGnegQuXenI29JDaMGoMvioKRBN53d4UazakS2unu8BnsEtxutS2kqRhYBPYk9RAriU3gTAKBggqhkjOPQQDAgNIADBFAiAOMwM7hH7q9Di+mT6qCi4LvB+kH8OxMheIrZ2eRPxtDQIhALHzTxwvN8Udt0Z2Cpo8JBihqacfeXkIxVAO8XkxmXhB\n-----END CERTIFICATE-----"
]
}
The profile already ships with x5Chain (for CBOR signing) and mDocNameSpacesDataMappingConfig (to encode dates and images to the correct CBOR types) pre-configured — you don't need to understand every field yet to issue your first credential. We'll come back to them in Defining Your Own Profile below.
Step 2: Create a Credential Offer
Endpoint: POST /issuer2/credential-offers | API Reference
Example Request
curl -X 'POST' \
'http://localhost:7002/issuer2/credential-offers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "isoMdl",
"authMethod": "PRE_AUTHORIZED"
}'
Body Parameters
- profileId: String (required) - The profile to issue from. Here, the shipped default
isoMdl. - authMethod: String (required) - The OID4VCI flow used to claim the offer.
PRE_AUTHORIZEDrequires no user login and is the simplest way to get started. See Using the Authorization Code Flow Instead for the alternative.
Example Response
{
"offerId": "99825a89-73d0-47f4-80cb-321fea52cd54",
"profileId": "isoMdl",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1782910641406,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fissuer-api2%3A7002%2Fopenid4vci%2Fcredential-offer%3Fid%3D99825a89-73d0-47f4-80cb-321fea52cd54"
}
Response Fields
- offerId: String - Unique identifier for this offer.
- expiresAt: Long - Unix timestamp (ms) when the offer expires (default: 300 seconds after creation).
- credentialOffer: String - The OID4VCI credential offer URL. Turn this into a QR code or deep link for the wallet.
Claiming the Offer
- Generate a QR code from the
credentialOfferURL, or use it directly as a deep link - Open the walt.id Wallet and log in
- Click "Request Credential" and paste the offer URL into the field below the camera
- The wallet resolves the offer, requests the credential, and stores it
🎉 You've issued a Mobile Driver's License using OID4VCI.
To learn how to accept this credential offer programmatically instead, see How to Accept mDL Credentials via OID4VCI.
Defining Your Own Profile
To issue your own mDoc credential type instead of the shipped default, two config files need an entry, and the service needs a restart to pick them up.
Step 1: Declare the Credential Type
Add an entry to credential-issuer-metadata.conf:
credentialConfigurations = {
"org.iso.18013.5.1.mDL.custom" = {
format = "mso_mdoc"
scope = "org.iso.18013.5.1.mDL.custom"
doctype = "org.iso.18013.5.1.mDL"
cryptographic_binding_methods_supported = ["cose_key"]
credential_signing_alg_values_supported = [-7, -9]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
}
}
See Credential Issuer Metadata for the full field reference.
Step 2: Define a Profile
Add a matching profile to issuer2-profiles.conf. Note that mDoc profiles sign with x5Chain instead of issuerDid, and need mDocNameSpacesDataMappingConfig for any date or binary field (see why this is needed):
profiles {
myMdl {
name = "My mDL"
credentialConfigurationId = "org.iso.18013.5.1.mDL.custom"
issuerKey = ${defaultIssuerKey}
x5Chain = ${defaultIssuerX5chain}
credentialData = {
"org.iso.18013.5.1" = {
family_name = "Doe"
given_name = "John"
birth_date = "1990-01-01"
issue_date = "2024-01-01"
expiry_date = "2029-01-01"
}
}
mDocNameSpacesDataMappingConfig = {
"org.iso.18013.5.1" = {
entriesConfigMap = {
birth_date = { type = "string", conversionType = "stringToFullDate" }
issue_date = { type = "string", conversionType = "stringToFullDate" }
expiry_date = { type = "string", conversionType = "stringToFullDate" }
}
}
}
}
}
See Issuer Profiles Configuration for every available profile field, including notifications and credential status.
Step 3: Restart and Verify
Profiles are loaded once, at startup. Restart the Issuer2 container after editing either config file, then confirm the new profile is loaded.
curl -X 'GET' 'http://localhost:7002/issuer2/profiles/myMdl'
Then repeat the two steps from Quick Start above, substituting your own profileId.
Using the Authorization Code Flow Instead
The example above used PRE_AUTHORIZED — no user login required. Issuer2 also supports the Authorization Code Flow, where the holder authenticates against an external IdP (e.g. Keycloak) before claiming the credential, and the resulting ID token claims are mapped into the credential via idTokenClaimsMapping.
This requires registering an external OAuth/OIDC provider first. See Authorization Code Flow for the full sequence and setup.
mDoc Data Type Mapping
CBOR requires specific types for dates and binary data (e.g. portrait) that JSON can't represent natively. See mDocNameSpacesDataMappingConfig for the full conversion reference.
Next Steps
- Verify the credential you just issued — Try Verifying mDLs with the walt.id Verifier2 API
- Populate credentials dynamically — See Data Functions for webhooks, timestamps, and more
- Track issuance events — See Notifications for webhook and SSE options
- Issue other credential types — Try SD-JWT VC or W3C VC
