Issuing Mobile Driver's Licenses (mDL) via OID4VCI
This guide shows you how to issue an ISO 18013-5 mDL (mDoc) credential end to end with the Enterprise Issuer2 Service. You'll create a minimal credential profile, turn it into a credential offer, and claim it in a wallet.
mDL / mDoc: The ISO/IEC 18013-5 mobile document format, CBOR-encoded and signed with an X.509 certificate.
OID4VCI: The protocol used to deliver the credential to a wallet (mDoc issuance follows OID4VCI per ISO 18013-7).
mDoc signing differs from JWT credentials. mDocs are signed with an X.509 certificate chain (x5Chain), not a DID — issuerDid is not supported for mDoc. Any date or binary field also needs a mDocNameSpacesDataMappingConfig entry so it is encoded to the correct CBOR type.
Prerequisites
Before you begin, ensure you have:
- An Issuer2 service with an mDoc credential configuration (format
mso_mdoc, with adoctype). See Setup. - A KMS key for signing, in a KMS Service under the same tenant.
- An X.509 certificate chain for the issuer key, from the X509 Service.
- A wallet to receive into — an Enterprise Stack Wallet Service (see Receiving an mDL), or any OID4VCI-compatible wallet app with mDoc support.
Throughout this guide:
- orgID is your organization ID, e.g.
test.enterprise-sandbox.waltid.dev. - target is your issuer service,
{organizationID}.{tenantID}.{issuerServiceID}, e.g.waltid.tenant1.issuer1.
Flow Overview
- Create a profile — Bind the
org.iso.18013.5.1.mDLtype to your key, X.509 chain, and a data template. - Create an offer — Generate an OID4VCI offer URL from the profile.
- Claim the offer — Hand the URL to a wallet.
Step 1: Create a Profile
Endpoint: POST /v2/{target}.{profileId}/issuer-service-api/credentials/profiles | API Reference
The new profile's own ID is the last path segment of the target (like service creation), i.e. {target}.{profileId} — here waltid.tenant1.issuer1.profile-mdl789.
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{target}.profile-mdl789/issuer-service-api/credentials/profiles' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"name": "Mobile Driving License Profile",
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"issuerKeyId": "waltid.tenant1.kms1.key1",
"x5Chain": [
{
"type": "pem-encoded-x509-certificate-descriptor",
"pemEncodedCertificate": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n"
}
],
"credentialData": {
"org.iso.18013.5.1": {
"family_name": "Doe",
"given_name": "John",
"birth_date": "1990-01-15",
"issue_date": "2024-01-01",
"expiry_date": "2029-01-01",
"document_number": "D1234567"
}
},
"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" }
}
}
}
}'
Body Parameters
- credentialConfigurationId: String (required) - Must match an mDoc (
mso_mdoc) configuration on your issuer service. - issuerKeyId: resourceIdentifier (required) - A key in your KMS.
- x5Chain: Array (required for mDoc) - The issuer's X.509 certificate chain, issuer certificate first. Each entry is a descriptor object
{ "type": "pem-encoded-x509-certificate-descriptor", "pemEncodedCertificate": "<PEM>" }. (A bare PEM string is also accepted for backwards compatibility, but the API always stores and returns the object form.) - credentialData: Object (required) - Claims nested under the mDoc namespace (
org.iso.18013.5.1). - mDocNameSpacesDataMappingConfig: Object (required for date/binary fields) - Type conversions for CBOR encoding. Here, the three dates are converted to CBOR full-date.
See Create a Profile for every field and all conversion types.
Example Response
{
"profileId": "profile-mdl789",
"name": "Mobile Driving License Profile",
"version": 1,
"credentialConfigurationId": "org.iso.18013.5.1.mDL",
"createdAt": 1704067200000,
"updatedAt": 1704067200000
}
The example above is shortened — the response also echoes your submitted issuerKeyId, x5Chain, credentialData, and mDocNameSpacesDataMappingConfig (mDoc profiles sign with an X.509 chain, so there is no issuerDid or mapping).
Response Fields
- profileId: String - The profile's own ID (the last path segment you chose), e.g.
profile-mdl789. The offer in Step 2 targets the full path{target}.{profileId}, e.g.waltid.tenant1.issuer1.profile-mdl789.
Response Codes
201— Profile created.400— Invalid request body.401— Invalid or missing authentication token.
Step 2: Create a Credential Offer
Create the offer against the profile as the target — the profileId from Step 1.
Endpoint: POST /v2/{profileTarget}/issuer-service-api/credentials/offers | API Reference
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/waltid.tenant1.issuer1.profile-mdl789/issuer-service-api/credentials/offers' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"authMethod": "PRE_AUTHORIZED"
}'
Body Parameters
- authMethod: String (required) -
PRE_AUTHORIZEDrequires no user login. See Protocol Flows for the alternative.
See Create an Offer for PIN, expiration, delivery mode, and runtime overrides.
Example Response
{
"offerId": "mdoc123-456-789",
"profileId": "profile-mdl789",
"profileVersion": 1,
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1704067500000,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fwaltid.enterprise-sandbox.waltid.dev%2Fv2%2Fwaltid.tenant1.issuer1%2Fissuer-service-api%2Fopenid4vci%2Fcredential-offer%3Fid%3Dmdoc123-456-789"
}
Response Fields
- credentialOffer: String - The OID4VCI credential offer URL. Turn it into a QR code or deep link for the wallet.
Claiming the Offer
Hand the credentialOffer URL to a wallet to receive the credential. With the Enterprise Stack Wallet Service this is a single API call — see Receiving an mDL, which passes the offer URL to the wallet's OID4VCI receive endpoint (Wallet API v2), resolves the offer, requests the credential, and stores it.
To claim it in any other wallet: generate a QR code from the credentialOffer URL which the wallet can scan or open it as a deep link.
mDoc Data Type Mapping
CBOR requires specific types for dates and binary data (e.g. a portrait) that JSON can't represent natively. Every such field needs an entry in mDocNameSpacesDataMappingConfig. Available conversions include stringToFullDate, stringToTDate, base64StringToByteString, and base64UrlStringToByteString — see Create a Profile → mDoc Data Mapping for the full reference.
Using the Authorization Code Flow Instead
The example above used PRE_AUTHORIZED — no user login. To have the holder authenticate against an external IdP first, use authMethod: "AUTHORIZED". See Authorization Code Flow.
Next Steps
- Verify the credential you just issued — Try Verifying mDL Credentials.
- Populate credentials dynamically — See Data Functions.
- Track issuance events — See Notifications & Session Events.
- Issue other credential types — Try SD-JWT VC or W3C VC.
