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
| Term | Description |
|---|---|
| WMP Entity | A 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). |
| Invitation | A signed URL generated by the issuer/verifier wallet. Sharing this URL with a holder wallet bootstraps the connection. |
| Identity Assertion | A Verifiable Presentation exchanged during session setup so both sides can verify each other's identity before traffic flows. |
| Session | A 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 Request | A pending credential offer or verification request queued for the holder wallet. Processed by calling WmpClientProcessRequest with type: process. |
Overview
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.
- Go
- TypeScript
// 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)
}
// Issuer side: create invitation
const { data: { invitationUrl, invitationId } } =
await issuerClient.wmpCreateNewInvitation(issuerWalletId)
// Holder side: accept the invitation
await holderClient.wmpAcceptInvitation(holderWalletId, { invitationUrl })
// Issuer side: wait until the holder has accepted
await issuerClient.walletNotificationGetByState(
issuerWalletId, 'wmp.invitation_accepted', invitationId)
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:
- Receives a
wmp.identity_assertion_requestnotification and responds with the credential to present. - Receives a
wmp.identity_assertion_receivednotification confirming the peer's assertion arrived. - Sends the
authenticateaction to open the session.
Both sides run this handshake concurrently.
- Go
- TypeScript
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()
async function authenticateWmpSession(
client: WalletClient,
walletId: string,
credentialId: string,
): Promise<void> {
// 1. Wait for the identity assertion request and provide a credential to present.
let iaId: string | undefined
while (!iaId) {
const { data: notifications } = await client.walletNotifications(walletId)
for (const e of notifications ?? []) {
if (e.eventType !== 'wmp.identity_assertion_request') continue
iaId = e.eventDetails.id
await client.wmpClientProcessRequest(iaId, walletId, {
type: 'identity_assertion',
credentialId,
})
}
}
// 2. Wait for the peer's identity assertion to arrive.
let assertionId: string | undefined
while (!assertionId) {
const { data: notifications } = await client.walletNotifications(walletId)
for (const e of notifications ?? []) {
if (e.eventType !== 'wmp.identity_assertion_received') continue
assertionId = e.eventDetails.id
}
}
// 3. Open the session.
await client.wmpClientProcessRequest(assertionId, walletId, {
type: 'authenticate',
authenticated: true,
})
}
// Run both sides concurrently.
await Promise.all([
authenticateWmpSession(issuerClient, issuerWalletId, issuerCredId),
authenticateWmpSession(holderClient, holderWalletId, holderCredId),
])
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:
- Go
- TypeScript
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
const { data: entities } = await issuerClient.wmpEntityList(issuerWalletId, 'client')
const holderEntityId = entities[0].id
Then create the credential and send the offer via WMP:
- Go
- TypeScript
// 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)
}
const { data: { id: credId } } = await issuerClient.credentialCreate(issuerWalletId, {
credentialSubject: {
id: holderDid,
familyName: 'Doe',
firstName: 'John',
},
credentialDraftMetadata: {
expirationDate: new Date(Date.now() + 365 * 86400_000).toISOString(),
format: 'sd_jwt_vc',
name: 'Identity Card',
type: 'VerifiableCredential,VerifiableId',
},
})
await issuerClient.credentialIssuanceInit(credId, issuerWalletId, {
issuerId: 'id_card',
clientId: holderDid,
})
// Deliver the offer via WMP (204 response — no offer URL).
await issuerClient.issuerInitiateAuthOffer(issuerWalletId, {
issuerId: 'id_card',
wmpEntityId: holderEntityId,
})
The holder polls notifications for wmp.credential_offer and then processes it through the standard
holder interaction flow:
- Go
- TypeScript
// 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)
}
// Poll until the credential offer notification arrives.
let wmpOfferId: string | undefined
while (!wmpOfferId) {
const { data: notifications } = await holderClient.walletNotifications(holderWalletId)
for (const e of notifications ?? []) {
if (e.eventType !== 'wmp.credential_offer') continue
wmpOfferId = e.eventDetails.id
}
}
// Process the offer — returns authorization requirements.
const { data: { interactionId } } = await holderClient.wmpClientProcessRequest(
wmpOfferId, holderWalletId, { type: 'process' })
// Accept the offer (id_token consent for in-time authorized issuance).
await holderClient.holderOfferProcessAfterConsent(interactionId, holderWalletId, {})
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.
- Go
- TypeScript
// 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)
}
// Verifier side: initiate verification via WMP.
const { data: { verifierState } } = await issuerClient.verifierInitUrlCreate(issuerWalletId, {
verifierId: 'id_card',
wmpEntityId: holderEntityId,
})
// Holder side: poll for the verification request notification.
let wmpVerReqId: string | undefined
while (!wmpVerReqId) {
const { data: notifications } = await holderClient.walletNotifications(holderWalletId)
for (const e of notifications ?? []) {
if (e.eventType !== 'wmp.credential_verification_request') continue
wmpVerReqId = e.eventDetails.id
}
}
// Process the verification request — returns presentation requirements.
const { data: { interactionId, presentationCandidates } } =
await holderClient.wmpClientProcessRequest(wmpVerReqId, holderWalletId, { type: 'process' })
// Present selected credentials.
await holderClient.holderCredentialsPresentAfterConsent(interactionId, holderWalletId, {
credentialsToPresent: presentationCandidates,
})
// Verifier side: retrieve verified credentials by state.
const { data: { credentials } } =
await issuerClient.walletVerifiedCredentialsByState(issuerWalletId, verifierState)
Managing WMP Entities
Listing entities
- Go
- TypeScript
// 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"})
// List holder wallets connected to this issuer/verifier wallet.
const { data: clients } = await client.wmpEntityList(walletId, 'client')
// List issuer/verifier wallets this holder is connected to.
const { data: servers } = await client.wmpEntityList(walletId, '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.
- Go
- TypeScript
statusResp, err := client.WmpEntityConnectionGetWithResponse(ctx, entityId,
&api.WmpEntityConnectionGetParams{WalletId: walletId})
if !statusResp.JSON200.Connected {
client.WmpEntityServerConnectWithResponse(ctx, entityId,
&api.WmpEntityServerConnectParams{WalletId: walletId})
}
const { data: { connected } } = await client.wmpEntityConnectionGet(entityId, walletId)
if (!connected) {
await client.wmpEntityServerConnect(entityId, walletId)
}
Removing a connection
- Go
- TypeScript
client.WmpEntityDeleteWithResponse(ctx, entityId,
&api.WmpEntityDeleteParams{WalletId: walletId})
await client.wmpEntityDelete(entityId, walletId)
WMP Notification Reference
| Event type | Who receives it | When |
|---|---|---|
wmp.invitation_accepted | Issuer/verifier | A holder accepted the invitation URL |
wmp.identity_assertion_request | Both sides | Session setup: the peer is requesting an identity assertion |
wmp.identity_assertion_received | Both sides | Session setup: the peer's assertion has been verified |
wmp.credential_offer | Holder | Issuer pushed a credential offer over WMP |
wmp.credential_verification_request | Holder | Verifier pushed a verification request over WMP |
wmp.error | Both sides | A protocol-level error occurred on the connection |
wmp.message | Both sides | A generic WMP message |
All WMP notifications carry a WmpNotification payload. The id field is the request/session ID
passed to WmpClientProcessRequest or WalletNotificationGetByState.