---
title: "Issuing Mobile Driver's Licenses (mDL) via OID4VCI"
description: "Learn how to issue ISO 18013-5 mDL / mDoc credentials end to end using the walt.id Enterprise Issuer2 Service."
stack: "Enterprise Stack (commercial) — version 1.1.1"
stack_version: "1.1.1"
stack_comparison: https://docs.walt.id/community-vs-enterprise.md
canonical_url: https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-issuance/mdl
generated: 2026-10-01
---
# Issuing Mobile Driver's Licenses (mDL) via OID4VCI

This guide shows you how to issue an [ISO 18013-5 mDL](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md) (mDoc) credential end to end with the Enterprise Issuer2 Service. You'll create a minimal [credential profile](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-profiles/overview.md), turn it into a [credential offer](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-offers/overview.md), and claim it in a wallet.

[**mDL / mDoc**](https://docs.walt.id/concepts/digital-credentials/mdoc-mdl-iso.md): The ISO/IEC 18013-5 mobile document format, CBOR-encoded and signed with an X.509 certificate.

[**OID4VCI**](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md): The protocol used to deliver the credential to a wallet (mDoc issuance follows OID4VCI per ISO 18013-7).

**Note:**

**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.

**Info:**

If the mDoc must later present OpenID4VP `transaction_data` (for example `urn:eudi:sca:payment:1`), set [`authorizedTransactionDataTypes`](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-profiles/create-profile.md#authorized-transaction-data-types) on the profile so the MSO authorizes those types in `KeyAuthorizations`.

## Prerequisites

Before you begin, ensure you have:

- **An Issuer2 service** with an mDoc credential configuration (format `mso_mdoc`, with a `doctype`). See [Setup](https://docs.walt.id/enterprise-stack/services/issuer2-service/setup.md).
- **A KMS key** for signing, in a [KMS Service](https://docs.walt.id/enterprise-stack/services/key-management-service/overview.md) under the same tenant.
- **An X.509 certificate chain** for the issuer key, from the [X509 Service](https://docs.walt.id/enterprise-stack/services/x509-service/overview.md).
- **A wallet to receive into** — an Enterprise Stack [Wallet2 Service](https://docs.walt.id/enterprise-stack/services/wallet2-service/overview.md) (see [Receiving an mDL](https://docs.walt.id/enterprise-stack/services/wallet2-service/credential-receiving/mdl.md)), 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

1. **Create a profile** — Bind the `org.iso.18013.5.1.mDL` type to your key, X.509 chain, and a data template.
2. **Create an offer** — Generate an OID4VCI offer URL from the profile.
3. **Claim the offer** — Hand the URL to a wallet.

---

## Step 1: Create a Profile

**Option: CURL**

**Endpoint:** `POST /v2/{target}.{profileId}/issuer-service-api/credentials/profiles` | [API Reference](https://enterprise.sandbox.walt.id/swagger/index.html)

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

```bash
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" }
      }
    }
  },
  "msoData": {
    "validFrom": "<timestamp>",
    "validUntil": "<timestamp-in:365d>",
    "expectedUpdate": "<timestamp-in:180d>"
  }
}'
```

**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.
- **msoData**: _Object_ (optional) - MSO `ValidityInfo` for the issued mDoc (`validFrom`, `validUntil`, `expectedUpdate`). Each field accepts a static ISO-8601 timestamp or a timestamp [data function](https://docs.walt.id/enterprise-stack/services/issuer2-service/data-functions.md), resolved at claim time. `mapping` does not set these fields. Defaults: `validFrom` equals the MSO signed time, `validUntil` is 365 days after claim, and `expectedUpdate` is omitted. See [Create a Profile](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-profiles/create-profile.md#mso-validity-msodata).

See [Create a Profile](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-profiles/create-profile.md) for every field and all conversion types.

---

##### Example Response

```json
{
  "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.

**Option: CURL**

**Endpoint:** `POST /v2/{profileTarget}/issuer-service-api/credentials/offers` | [API Reference](https://enterprise.sandbox.walt.id/swagger/index.html)

##### Example Request

```bash
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_AUTHORIZED` requires no user login. See [Protocol Flows](https://docs.walt.id/enterprise-stack/services/issuer2-service/protocol-flows/overview.md) for the alternative.

See [Create an Offer](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-offers/create-offer.md) for PIN, expiration, delivery mode, and runtime overrides.

---

##### Example Response

```json
{
  "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 [Wallet2 Service](https://docs.walt.id/enterprise-stack/services/wallet2-service/overview.md) this is a single API call — see **[Receiving an mDL](https://docs.walt.id/enterprise-stack/services/wallet2-service/credential-receiving/mdl.md)**, which passes the offer URL to the wallet's OID4VCI receive endpoint (Wallet2), 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](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-profiles/create-profile.md#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](https://docs.walt.id/enterprise-stack/services/issuer2-service/protocol-flows/authorization-code-flow.md).

---

## Next Steps

- **Verify the credential you just issued** — Try [Verifying mDL Credentials](https://docs.walt.id/enterprise-stack/services/verifier2-service/credential-verification/mdl-oid4vp.md).
- **Populate credentials dynamically** — See [Data Functions](https://docs.walt.id/enterprise-stack/services/issuer2-service/data-functions.md).
- **Track issuance events** — See [Notifications & Session Events](https://docs.walt.id/enterprise-stack/services/issuer2-service/notifications.md).
- **Issue other credential types** — Try [SD-JWT VC](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-issuance/sd-jwt-vc.md) or [W3C VC](https://docs.walt.id/enterprise-stack/services/issuer2-service/credential-issuance/w3c-vc.md).
