Skip to main content

05 Wallet Messaging Protocol (WMP)

WMP (Wallet Messaging Protocol) is a peer-to-peer communication channel between two wallets. Once connected, an issuer or verifier wallet can deliver credential offers and verification requests directly to a holder wallet — without exchanging QR codes or redirect URLs on every interaction.

Common Concepts & Terms

TermDescription
WMP EntityA remote wallet that has established a WMP connection with the local wallet. Entities have a role: server (the wallet that created the invitation) or client (the wallet that accepted it).
InvitationA signed URL generated by the issuer/verifier wallet. Sharing this URL with a holder wallet bootstraps the connection.
Identity AssertionA Verifiable Presentation exchanged during session setup so both sides can verify each other's identity before traffic flows.
SessionA fully authenticated WMP connection. A session must be opened (both sides exchange identity assertions and call authenticate) before offers or verification requests can be sent.
WMP RequestA pending credential offer or verification request queued for the holder wallet. Processed by calling WmpClientProcessRequest with type: process.

Overview

sequenceDiagram participant Issuer participant Holder Issuer ->> Issuer: WmpCreateNewInvitation Issuer -->> Holder: Share invitationUrl (out of band) Holder ->> Holder: WmpAcceptInvitation note over Issuer,Holder: Mutual identity assertion (session setup) Issuer ->> Holder: wmp.identity_assertion_request Holder ->> Issuer: WmpClientProcessRequest (identity_assertion) Issuer ->> Holder: wmp.identity_assertion_request Issuer ->> Holder: WmpClientProcessRequest (identity_assertion) note over Issuer,Holder: Both sides receive wmp.identity_assertion_received Issuer ->> Issuer: WmpClientProcessRequest (authenticate) Holder ->> Holder: WmpClientProcessRequest (authenticate) note over Issuer,Holder: Credential offer via WMP Issuer ->> Issuer: IssuerInitiateAuthOffer (wmpEntityId) Holder ->> Holder: poll wmp.credential_offer notification Holder ->> Holder: WmpClientProcessRequest (process) Holder ->> Holder: HolderOfferProcessAfterConsent note over Issuer,Holder: Credential verification via WMP Issuer ->> Issuer: VerifierInitUrlCreate (wmpEntityId) Holder ->> Holder: poll wmp.credential_verification_request notification Holder ->> Holder: WmpClientProcessRequest (process) Holder ->> Holder: HolderCredentialsPresentAfterConsent

Step 1 — Establishing a Connection

The issuer or verifier wallet creates an invitation URL. This URL is shared with the holder wallet out-of-band (e.g. email, push notification, deep link). The holder then accepts it to register the issuer/verifier as a WMP entity.

// Issuer side: create invitation
inviteResp, err := issuerClient.WmpCreateNewInvitationWithResponse(
ctx, &api.WmpCreateNewInvitationParams{WalletId: issuerWalletId})
if err != nil {
log.Panicf("failed to create invitation: %v", err)
}
if inviteResp.StatusCode() != 200 {
log.Panicf("create invitation error: %d %s", inviteResp.StatusCode(), inviteResp.Body)
}

invitationUrl := inviteResp.JSON200.InvitationUrl
invitationId := inviteResp.JSON200.InvitationId

// Holder side: accept the invitation
acceptResp, err := holderClient.WmpAcceptInvitationWithResponse(
ctx,
&api.WmpAcceptInvitationParams{WalletId: holderWalletId},
api.WmpAcceptInvitationPayload{InvitationUrl: invitationUrl})
if err != nil {
log.Panicf("failed to accept invitation: %v", err)
}
if acceptResp.StatusCode() != 200 {
log.Panicf("accept invitation error: %d %s", acceptResp.StatusCode(), acceptResp.Body)
}

// Issuer side: wait until the holder has accepted
notifResp, err := issuerClient.WalletNotificationGetByStateWithResponse(
ctx, issuerWalletId,
string(api.WalletNotificationEventTypeWmpInvitationAccepted),
invitationId)
if err != nil {
log.Panicf("notification error: %v", err)
}

Step 2 — Opening the Session (Mutual Identity Assertion)

Before any credential offer or verification request can flow, both wallets must complete a mutual identity assertion. Each side:

  1. Receives a wmp.identity_assertion_request notification and responds with the credential to present.
  2. Receives a wmp.identity_assertion_received notification confirming the peer's assertion arrived.
  3. Sends the authenticate action to open the session.

Both sides run this handshake concurrently.

func authenticateWmpSession(
ctx context.Context,
client *api.ClientWithResponses,
walletId string,
credentialId string,
) {
// 1. Wait for the identity assertion request and provide a credential to present.
var iaID string
for iaID == "" {
notifications, _ := client.WalletNotificationsWithResponse(ctx, walletId)
if notifications.JSON200 != nil {
for _, e := range *notifications.JSON200 {
if e.EventType != api.WalletNotificationEventTypeWmpIdentityAssertionRequest {
continue
}
details, _ := e.EventDetails.AsWmpNotification()
iaID = details.Id

var body api.WmpClientRequestBody
_ = body.FromWmpProvideIdentityAssertion(api.WmpProvideIdentityAssertion{
Type: api.IdentityAssertion,
CredentialId: credentialId,
})
client.WmpClientProcessRequestWithResponse(
ctx, iaID, &api.WmpClientProcessRequestParams{WalletId: walletId}, body)
}
}
}

// 2. Wait for the peer's identity assertion to arrive.
iaID = ""
for iaID == "" {
notifications, _ := client.WalletNotificationsWithResponse(ctx, walletId)
if notifications.JSON200 != nil {
for _, e := range *notifications.JSON200 {
if e.EventType != api.WalletNotificationEventTypeWmpIdentityAssertionReceived {
continue
}
details, _ := e.EventDetails.AsWmpNotification()
iaID = details.Id
}
}
}

// 3. Open the session.
var body api.WmpClientRequestBody
_ = body.FromWmpAuthenticateSession(api.WmpAuthenticateSession{
Type: api.Authenticate,
Authenticated: true,
})
client.WmpClientProcessRequestWithResponse(
ctx, iaID, &api.WmpClientProcessRequestParams{WalletId: walletId}, body)
}

// Run both sides concurrently — each side blocks until its own handshake is complete.
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); authenticateWmpSession(ctx, issuerClient, issuerWalletId, issuerCredId) }()
go func() { defer wg.Done(); authenticateWmpSession(ctx, holderClient, holderWalletId, holderCredId) }()
wg.Wait()

Step 3 — Issuing a Credential over WMP

Once the session is open, the issuer can send a credential offer directly to the holder by providing a wmpEntityId when initiating the offer. The holder receives a wmp.credential_offer notification instead of a QR code / redirect URL.

First, look up the connected holder entity:

entitiesResp, err := issuerClient.WmpEntityListWithResponse(
ctx, &api.WmpEntityListParams{WalletId: issuerWalletId, EntityType: "client"})
if err != nil {
log.Panicf("failed to list WMP entities: %v", err)
}

entities := *entitiesResp.JSON200
holderEntityId := entities[0].Id

Then create the credential and send the offer via WMP:

// Create the credential draft and add it to the issuance queue.
credSubject := api.CredentialSubject{}
_ = credSubject.FromCredentialSubjectItem(api.CredentialSubjectItem{
Id: &holderDid,
AdditionalProperties: map[string]interface{}{
"familyName": "Doe",
"firstName": "John",
},
})

expDate := time.Now().AddDate(1, 0, 0)
credCreateResp, err := issuerClient.CredentialCreateWithResponse(ctx,
&api.CredentialCreateParams{WalletId: issuerWalletId},
api.CredentialPayload{
CredentialDraftMetadata: api.CredentialDraftMetadata{
ExpirationDate: &expDate,
Format: api.SdJwtVc,
Name: "Identity Card",
Type: "VerifiableCredential,VerifiableId",
},
CredentialSubject: credSubject,
})
if err != nil {
log.Panicf("failed to create credential: %v", err)
}
credId := credCreateResp.JSON200.Id

_, err = issuerClient.CredentialIssuanceInitWithResponse(ctx, credId,
&api.CredentialIssuanceInitParams{WalletId: issuerWalletId},
api.CredentialIssuanceInit{IssuerId: "id_card", ClientId: &holderDid})
if err != nil {
log.Panicf("failed to init issuance: %v", err)
}

// Deliver the offer via WMP (returns 204 — no offer URL generated).
_, err = issuerClient.IssuerInitiateAuthOfferWithResponse(ctx,
&api.IssuerInitiateAuthOfferParams{WalletId: issuerWalletId},
api.InitAuthOffer{
IssuerId: "id_card",
WmpEntityId: &holderEntityId,
})
if err != nil {
log.Panicf("failed to initiate auth offer: %v", err)
}

The holder polls notifications for wmp.credential_offer and then processes it through the standard holder interaction flow:

// Poll until the credential offer notification arrives.
var wmpOfferId string
for wmpOfferId == "" {
notifications, _ := holderClient.WalletNotificationsWithResponse(ctx, holderWalletId)
if notifications.JSON200 != nil {
for _, e := range *notifications.JSON200 {
if e.EventType != api.WalletNotificationEventTypeWmpCredentialOffer {
continue
}
details, _ := e.EventDetails.AsWmpNotification()
wmpOfferId = details.Id
}
}
}

// Process the offer — returns authorization requirements.
var processBody api.WmpClientRequestBody
_ = processBody.FromWmpProcessClientRequest(api.WmpProcessClientRequest{Type: api.Process})

processResp, err := holderClient.WmpClientProcessRequestWithResponse(
ctx, wmpOfferId, &api.WmpClientProcessRequestParams{WalletId: holderWalletId}, processBody)
if err != nil {
log.Panicf("failed to process WMP offer: %v", err)
}

// Accept the offer (id_token consent for in-time authorized issuance).
offerAcceptResp, err := holderClient.HolderOfferProcessAfterConsentWithResponse(
ctx, processResp.JSON200.InteractionId,
&api.HolderOfferProcessAfterConsentParams{WalletId: holderWalletId},
api.InteractionAuthorizationConsent{})
if err != nil {
log.Panicf("failed to accept offer: %v", err)
}

Step 4 — Verifying a Credential over WMP

The verifier creates a verification request with a wmpEntityId to deliver it via WMP. The holder receives a wmp.credential_verification_request notification.

// Verifier side: initiate verification via WMP.
verInitResp, err := issuerClient.VerifierInitUrlCreateWithResponse(ctx,
&api.VerifierInitUrlCreateParams{WalletId: issuerWalletId},
api.VerifyInitRequest{
VerifierId: "id_card",
WmpEntityId: &holderEntityId,
})
if err != nil {
log.Panicf("failed to init verification: %v", err)
}
verifierState := verInitResp.JSON200.VerifierState

// Holder side: poll for the verification request notification.
var wmpVerReqId string
for wmpVerReqId == "" {
notifications, _ := holderClient.WalletNotificationsWithResponse(ctx, holderWalletId)
if notifications.JSON200 != nil {
for _, e := range *notifications.JSON200 {
if e.EventType != api.WalletNotificationEventTypeWmpCredentialVerificationRequest {
continue
}
details, _ := e.EventDetails.AsWmpNotification()
wmpVerReqId = details.Id
}
}
}

// Process the verification request — returns presentation requirements.
var processBody api.WmpClientRequestBody
_ = processBody.FromWmpProcessClientRequest(api.WmpProcessClientRequest{Type: api.Process})

processResp, err := holderClient.WmpClientProcessRequestWithResponse(
ctx, wmpVerReqId, &api.WmpClientProcessRequestParams{WalletId: holderWalletId}, processBody)
if err != nil {
log.Panicf("failed to process verification request: %v", err)
}

// Present selected credentials.
_, err = holderClient.HolderCredentialsPresentAfterConsentWithResponse(
ctx, processResp.JSON200.InteractionId,
&api.HolderCredentialsPresentAfterConsentParams{WalletId: holderWalletId},
api.InteractionAuthorizationConsent{
CredentialsToPresent: processResp.JSON200.PresentationCandidates,
})
if err != nil {
log.Panicf("failed to present credentials: %v", err)
}

// Verifier side: retrieve verified credentials by state.
verifiedResp, err := issuerClient.WalletVerifiedCredentialsByStateWithResponse(
ctx, issuerWalletId, verifierState)
if err != nil {
log.Panicf("failed to get verified credentials: %v", err)
}

Managing WMP Entities

Listing entities

// List holder wallets connected to this issuer/verifier wallet.
clientsResp, err := client.WmpEntityListWithResponse(
ctx, &api.WmpEntityListParams{WalletId: walletId, EntityType: "client"})

// List issuer/verifier wallets this holder is connected to.
serversResp, err := client.WmpEntityListWithResponse(
ctx, &api.WmpEntityListParams{WalletId: walletId, EntityType: "server"})

Checking connection status and reconnecting

The underlying WebSocket may drop after a restart. Use WmpEntityConnectionGet to check connectivity and WmpEntityServerConnect to re-establish the WebSocket.

statusResp, err := client.WmpEntityConnectionGetWithResponse(ctx, entityId,
&api.WmpEntityConnectionGetParams{WalletId: walletId})

if !statusResp.JSON200.Connected {
client.WmpEntityServerConnectWithResponse(ctx, entityId,
&api.WmpEntityServerConnectParams{WalletId: walletId})
}

Removing a connection

client.WmpEntityDeleteWithResponse(ctx, entityId,
&api.WmpEntityDeleteParams{WalletId: walletId})

WMP Notification Reference

Event typeWho receives itWhen
wmp.invitation_acceptedIssuer/verifierA holder accepted the invitation URL
wmp.identity_assertion_requestBoth sidesSession setup: the peer is requesting an identity assertion
wmp.identity_assertion_receivedBoth sidesSession setup: the peer's assertion has been verified
wmp.credential_offerHolderIssuer pushed a credential offer over WMP
wmp.credential_verification_requestHolderVerifier pushed a verification request over WMP
wmp.errorBoth sidesA protocol-level error occurred on the connection
wmp.messageBoth sidesA generic WMP message

All WMP notifications carry a WmpNotification payload. The id field is the request/session ID passed to WmpClientProcessRequest or WalletNotificationGetByState.