---
title: "Tutorial v2 (30 min)"
description: "Issue, receive, and verify a Verifiable Credential end-to-end using Issuer2, Wallet2, and Verifier2."
stack: "Community Stack (open source) — 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/community-stack/home/tutorial-30-min-v2
generated: 2026-10-01
---
# Tutorial v2 (30 min)

Video: https://youtu.be/lWSF_S-ID9Y

In this 30 min tutorial, we will take a look at the **Issuer2**, **Wallet2**, and **Verifier2** APIs of the walt.id
Community Stack — the latest generation of the issuer, wallet, and verifier services, implementing
**[OID4VCI 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md)** and
**[OID4VP 1.0](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md)**.

**Note:**

Still building against the original **Issuer**, **Wallet**, and **Verifier**? Use the
[v1 tutorial](https://docs.walt.id/community-stack/home/tutorial-30-min.md) instead.

The tutorial is split into three sections:

1. Create a wallet
2. Issue a credential into the wallet
3. Request & verify the credential from the wallet

With that, we are covering a complete digital credential use case end-to-end using the walt.id v2 API
components:

- [Issuer2 API](https://docs.walt.id/community-stack/issuer2/getting-started.md)
- [Wallet2 API](https://docs.walt.id/community-stack/wallet2/getting-started.md)
- [Verifier2 API](https://docs.walt.id/community-stack/verifier2/getting-started.md)

### The Credential

For this tutorial, we will issue ourselves an **Open Badge Credential** — a
[W3C Verifiable Credential](https://docs.walt.id/concepts/digital-credentials/verifiable-credentials-w3c.md) with a JWT signature
(`jwt_vc_json`), issued from a profile that ships out of the box with Issuer2, so there's no configuration to write
before your first credential.

**Note:**

If you are curious to learn more about other credential formats, go [here](https://docs.walt.id/concepts/digital-credentials.md).
Issuer2, Wallet2, and Verifier2 also support SD-JWT VC and mdoc (ISO 18013-5) — this tutorial sticks to W3C VC to
keep the walkthrough focused.

#### Example Open Badge Credential (as issued)

```json
{
  "@context": [
    "https://www.w3.org/ns/credentials/v2",
    "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
    "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json"
  ],
  "id": "urn:uuid:6bc9b310-4ef2-4d0a-97e7-c577b4c944d4",
  "type": ["VerifiableCredential", "OpenBadgeCredential"],
  "issuer": {
    "id": "did:jwk:...",
    "name": "Jobs for the Future (JFF)",
    "type": ["Profile"],
    "url": "https://www.jff.org/"
  },
  "credentialSchema": [
    {
      "type": "1EdTechJsonSchemaValidator2019",
      "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json"
    }
  ],
  "credentialSubject": {
    "id": "did:key:...",
    "type": ["AchievementSubject"],
    "achievement": {
      "id": "https://example.com/achievements/21st-century-skills/teamwork",
      "type": ["Achievement"],
      "name": "Teamwork",
      "description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
      "criteria": {
        "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
      }
    }
  },
  "validFrom": "2026-07-22T12:50:10.122342336Z"
}
```

---

## Setup

To bring up Issuer2, Wallet2, and Verifier2 locally, we'll use our
[docker compose](https://github.com/walt-id/waltid-identity/tree/main/docker-compose) setup.

**Clone [walt.id identity](https://github.com/walt-id/waltid-identity)**

```bash
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity/docker-compose
```

**Launch the services**

```bash
docker compose up
```

#### Access APIs

- **Issuer2 API** — [http://localhost:7005/](http://localhost:7005/)
- **Verifier2 API** — [http://localhost:7004/](http://localhost:7004/)
- **Wallet2 API** — [http://localhost:7006/](http://localhost:7006/)

---

## Create a Wallet

By default, Wallet2's `auth` feature is off — there's no account, no registration, no login, and no bearer token to
manage. A wallet is just a container for keys, a DIDs, and credentials, and you get its ID back the moment you create
it. Authentication can be enabled — see [Authentication](https://docs.walt.id/community-stack/wallet2/configurations/config-files/auth.md).

A new wallet doesn't have any key or DID yet. The **key** is what the wallet signs with; the **DIDs** are
identifier derived from that key, which credentials get bound to when receiving them (new to DIDs? see
[Decentralised Identifiers](https://docs.walt.id/concepts/decentralised-identifiers.md)). We generate both next, as two explicit calls
right after creating the wallet.

### Create the Wallet

**Option: CURL**

**Endpoint:** `POST /wallet` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)

```bash
curl -X POST http://localhost:7006/wallet \
  -H 'Content-Type: application/json' \
  -d '{}'
```

**Body Parameters**

An empty body is enough — all fields are optional for the default, fully-managed wallet. See
[Managing Wallets](https://docs.walt.id/community-stack/wallet2/wallets.md) for `noDidStore` and the store-less-wallet options.

**Example Response**

```json
{
  "walletId": "39c44fcf-63bf-41fb-a7de-d99d09458940"
}
```

**Note:**

  Save the `walletId` — every subsequent call is scoped to it.

### Generate a Key

**Option: CURL**

**Endpoint:** `POST /wallet/{walletId}/keys/generate` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)

```bash
curl -X POST http://localhost:7006/wallet/{walletId}/keys/generate \
  -H 'Content-Type: application/json' \
  -d '{ "backend": "jwk", "keyType": "Ed25519" }'
```

**Body Parameters**

- `backend`: _String_ (required) - Discriminator for where the key is generated/stored. Use `jwk` for a
  software-managed key (no external KMS required). Other supported values (`tse`, `azure-rest-api`, `oci-rest-api`,
  `aws-rest-api`) require an additional `config` object for that backend — see
  [Key Management](https://docs.walt.id/community-stack/wallet2/key-management).
- `keyType`: _String_ (optional) - One of `Ed25519`, `secp256r1`, `secp256k1` `RSA`. Defaults to `secp256r1`.

**Example Response**

```json
{
  "keyId": "onxChakLCfOVM_YyZOOJ09fO1hgPrXxKxiBIPwTeack",
  "keyType": "Ed25519"
}
```

This is the wallet's first (and so far only) key — it automatically becomes the **default key**, used for signing
whenever a later call doesn't specify `keyId` explicitly.

### Create a DID

W3C VCs bind the credential subject to a DID, not just a key, so we need one before we can receive one.

**Option: CURL**

**Endpoint:** `POST /wallet/{walletId}/dids/create` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)

```bash
curl -X POST http://localhost:7006/wallet/{walletId}/dids/create \
  -H 'Content-Type: application/json' \
  -d '{ "method": "key" }'
```

**Body Parameters**

- `method`: _String_ (required) - The DID method to create. Supported: `key`, `jwk`.
- `keyId`: _String_ (optional) - Key to derive the DID from. Defaults to the wallet's default key — the one we just
  generated.

**Example Response**

```json
{
  "did": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
  "document": {
    "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
    "id": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
    "verificationMethod": [
      {
        "id": "did:key:z6MksLjz...#z6MksLjz...",
        "type": "JsonWebKey2020",
        "controller": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
        "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
      }
    ],
    "assertionMethod": ["did:key:z6MksLjz...#z6MksLjz..."],
    "authentication": ["did:key:z6MksLjz...#z6MksLjz..."]
  }
}
```

This is now the wallet's **default DID** too. Both defaults mean every call from here on can omit `keyId` and `did`
entirely.

---

## Issue Credential

### Confirm the Profile Is Loaded (optional)

Issuer2 issues from **[profiles](https://docs.walt.id/community-stack/issuer2/credential-profiles/overview.md)** — reusable credential
configurations declared ahead of time (issuer key, issuer DID, credential template, etc.). `openBadgeCredential`
ships pre-configured:

**Option: CURL**

**Endpoint:** `GET /issuer2/profiles/{profileId}` | [API Reference](https://docs.walt.id/community-stack/issuer2/full-api-reference.md)

```bash
curl http://localhost:7005/issuer2/profiles/openBadgeCredential
```

**Example Response** (shortened)

```json
{
  "profileId": "openBadgeCredential",
  "name": "OpenBadgeCredential",
  "credentialConfigurationId": "OpenBadgeCredential_jwt_vc_json",
  "issuerKey": { "type": "jwk", "jwk": { "kty": "EC", "crv": "P-256", "...": "..." } },
  "issuerDid": "did:jwk:...",
  "credentialData": {
    "@context": ["https://www.w3.org/ns/credentials/v2", "..."],
    "type": ["VerifiableCredential", "OpenBadgeCredential"],
    "credentialSubject": { "achievement": { "name": "Teamwork", "...": "..." } }
  },
  "mapping": {
    "id": "<uuid>",
    "issuer": { "id": "<issuerDid>" },
    "credentialSubject": { "id": "<subjectDid>" },
    "validFrom": "<timestamp>"
  }
}
```

### Create a Credential Offer

Now let's actually issue a credential from that profile. This call creates a **credential offer** — a one-time,
short-lived pointer to a not-yet-issued credential that a wallet can redeem.

**Option: CURL**

**Endpoint:** `POST /issuer2/credential-offers` | [API Reference](https://docs.walt.id/community-stack/issuer2/full-api-reference.md)

```bash
curl -X POST http://localhost:7005/issuer2/credential-offers \
  -H 'Content-Type: application/json' \
  -d '{
    "profileId": "openBadgeCredential",
    "authMethod": "PRE_AUTHORIZED"
  }'
```

**Body Parameters**

- `profileId`: _String_ (required) - The profile to issue from.
- `authMethod`: _String_ (required) - `PRE_AUTHORIZED` requires no user login and is the simplest way to get
  started. Issuer2 also supports `AUTHORIZED` (authorization-code flow against an external IdP) — see
  [Authorization Code Flow](https://docs.walt.id/community-stack/issuer2/protocol-flows/authorization-code-flow.md).

**Example Response**

```json
{
  "offerId": "e124ba49-df08-45e1-8caa-810a712122e6",
  "profileId": "openBadgeCredential",
  "authMethod": "PRE_AUTHORIZED",
  "expiresAt": 1784724878505,
  "credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fhost.docker.internal%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3De124ba49-df08-45e1-8caa-810a712122e6"
}
```

`credentialOffer` is the [OID4VCI](https://docs.walt.id/concepts/data-exchange-protocols/openid4vci.md) offer URL — hand it directly to
Wallet2 in the next step. Notice it already resolves
via `host.docker.internal`, which every container in the compose network can reach, so it works whether the caller is
your terminal, the demo wallet, or (as we'll do next) another container.

---

## Receive Credential in Wallet

This single call looks up the offer's details, signs a proof that the wallet owns the key the credential will be bound to,
then fetches the credential and stores it.

**Option: CURL**

**Endpoint:** `POST /wallet/{walletId}/credentials/receive` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)

```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive \
  -H 'Content-Type: application/json' \
  -d '{
    "offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fhost.docker.internal%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3De124ba49-df08-45e1-8caa-810a712122e6"
  }'
```

**Body Parameters**

- `offerUrl`: _String_ - The `credentialOffer` value from the previous step.
  
**Example Response**

```json
{
  "credentialIds": ["3d14e4b9-9478-41ed-bb52-bcf76b7dd36c"],
  "deferredTransactionIds": {}
}
```

### Confirm It Was Stored

**Option: CURL**

**Endpoint:** `GET /wallet/{walletId}/credentials` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)

```bash
curl http://localhost:7006/wallet/{walletId}/credentials
```

**Example Response**

```json
[
  {
    "id": "3d14e4b9-9478-41ed-bb52-bcf76b7dd36c",
    "format": "jwt_vc_json",
    "issuer": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Ii...",
    "subject": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
    "addedAt": "2026-07-22T12:50:10.138097127Z"
  }
]
```

🎉 You've received an Open Badge Credential into the wallet. To see the full parsed contents (not just metadata), use
`GET /wallet/{walletId}/credentials/{credentialId}` — see
[Credential Management](https://docs.walt.id/community-stack/wallet2/credential-management.md).

---

## Request & Verify Credential

We now switch to Verifier2 to build an [OID4VP](https://docs.walt.id/concepts/data-exchange-protocols/openid4vp.md) authorization request.
Verifier2 uses
**[DCQL](https://docs.walt.id/concepts/data-exchange-protocols/dcql.md)** (Digital Credentials Query Language) to describe the
credentials it wants — the credential type/format is expressed as a `dcql_query` directly in the request body.

**Option: CURL**

**Endpoint:** `POST /verification-session/create` | [API Reference](https://docs.walt.id/community-stack/verifier2/full-api-reference.md)

```bash
curl -X POST http://localhost:7004/verification-session/create \
  -H 'Content-Type: application/json' \
  -d '{
    "flow_type": "cross_device",
    "core_flow": {
      "dcql_query": {
        "credentials": [
          {
            "id": "credential_1",
            "format": "jwt_vc_json",
            "meta": { "type_values": [["VerifiableCredential", "OpenBadgeCredential"]] }
          }
        ]
      },
      "policies": {
        "vc_policies": [{ "policy": "signature" }]
      }
    }
  }'
```

**Body Parameters**

- `flow_type`: _String_ (required) - `cross_device` (QR code), `same_device` (deep link), `dc_api` / `dc_api-annex-c`
  (browser-native Digital Credentials API). We use `cross_device` here; it needs no extra flow-specific fields.
- `core_flow.dcql_query.credentials`: _Array_ (required) - one entry per credential to request. `id` is a
  free-choice label used later to key policy results; `format` + `meta.type_values` describe what to accept — here,
  any credential whose `type` array matches `["VerifiableCredential", "OpenBadgeCredential"]`.
- `core_flow.policies.vc_policies`: _Array_ (optional) - validation rules applied to the received credential. See
  [Policies](https://docs.walt.id/community-stack/verifier2/policies/overview.md) for the full list.
  **Omitting `policies` entirely defaults to signature-only verification** — the same as what we set explicitly here.

**Example Response**

```json
{
  "sessionId": "d20e3de1-0695-4c54-bb47-4104d85c41d7",
  "bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=redirect_uri%3Ahttp%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Fresponse&request_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Frequest",
  "fullAuthorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=redirect_uri%3Ahttp%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Fresponse&state=1c7706bd-8e23-41a6-8bd9-291c51e8010a&response_mode=direct_post&nonce=1ccec52e-f0d0-48fc-b51e-fc1115c5d930&response_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22jwt_vc_json%22%2C%22meta%22%3A%7B%22type_values%22%3A%5B%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%5D%7D%7D%5D%7D&client_metadata=..."
}
```

- `sessionId`: use this to check the result later.
- `bootstrapAuthorizationRequestUrl`: the short, `request_uri`-by-reference form — this is what we hand to the
  wallet next (and what you'd turn into a QR code for a real cross-device flow).

---

## Present Credential

A single call resolves the verifier's request, runs the DCQL match against the wallet's stored credentials, signs
with the default key/DID, and submits the response.

**Option: CURL**

**Endpoint:** `POST /wallet/{walletId}/credentials/present` | [API Reference](https://docs.walt.id/community-stack/wallet2/full-api-reference.md)

```bash
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present \
  -H 'Content-Type: application/json' \
  -d '{
    "requestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Frequest"
  }'
```

**Body Parameters**

- `requestUrl`: _String_ - the `bootstrapAuthorizationRequestUrl` (or `fullAuthorizationRequestUrl`) from the
  previous step.

**Example Response**

```json
{
  "transmission_success": true,
  "verifier_response": {
    "status": "received",
    "message": "Presentation received and is being processed."
  }
}
```

**Note:**

  `transmission_success: true` only confirms the wallet delivered the presentation — it says nothing about whether the
  credential actually passed verification. Check that on the verifier side next.

---

## Check the Verification Result

Back on Verifier2, poll (or subscribe via [SSE/callback](https://docs.walt.id/community-stack/verifier2/callback-and-sse.md)) the session to
see the final status and policy results.

**Option: CURL**

**Endpoint:** `GET /verification-session/{sessionId}/info` | [API Reference](https://docs.walt.id/community-stack/verifier2/full-api-reference.md)

```bash
curl http://localhost:7004/verification-session/{sessionId}/info
```

**Example Response** (shortened)

```json
{
  "id": "d20e3de1-0695-4c54-bb47-4104d85c41d7",
  "status": "SUCCESSFUL",
  "attempted": true,
  "reattemptable": true,
  "policy_results": {
    "vp_policies": {
      "credential_1": {
        "jwt_vc_json/audience-check": { "success": true },
        "jwt_vc_json/nonce-check": { "success": true },
        "jwt_vc_json/envelope_signature": { "success": true }
      }
    },
    "vc_policies": [
      {
        "policy": { "policy": "signature", "id": "signature" },
        "success": true,
        "result": {
          "verification_result": true,
          "verified_data": {
            "iss": "did:jwk:...",
            "sub": "did:key:z6MksLjz...",
            "type": ["VerifiableCredential", "OpenBadgeCredential"],
            "credentialSubject": {
              "id": "did:key:z6MksLjz...",
              "achievement": { "name": "Teamwork", "...": "..." }
            }
          }
        }
      }
    ],
    "overallSuccess": true
  },
  "presented_credentials": {
    "credential_1": [
      { "type": "vc-w3c_2", "credentialData": { "...": "the presented credential" } }
    ]
  }
}
```

`status: "SUCCESSFUL"` and `policy_results.overallSuccess: true` confirm the credential was received and the
signature policy passed. 🎉

You've issued, received, requested, and verified an Open Badge Credential end-to-end
using Issuer2, Wallet2, and Verifier2.

---

## Next Steps

**Note:**

If you enjoy our tools, please leave us a star ⭐ [on GitHub](https://github.com/walt-id/waltid-identity).

You can learn more about the different APIs in detail below:

- [Issuer2](https://docs.walt.id/community-stack/issuer2/getting-started.md){:target="_blank"}
- [Wallet2](https://docs.walt.id/community-stack/wallet2/getting-started.md){:target="_blank"}
- [Verifier2](https://docs.walt.id/community-stack/verifier2/getting-started.md){:target="_blank"}
