Protocols / Intermediate / 10 min

DIDComm Messaging

Encrypted agent-to-agent communication using DIDs—no shared infrastructure required

The Problem DIDComm Solves

When agents need to communicate across organizations, how do they exchange messages securely?

Traditional approaches:

  • VPN tunnels — Merge networks (complex, exposes infrastructure)
  • HTTPS APIs — Requires both sides to expose endpoints (firewall issues, TLS management)
  • Message brokers — Shared Kafka/RabbitMQ (tight coupling, single point of failure)
  • Email — Not encrypted end-to-end, no authentication guarantees

Problems:

  • No native encryption — TLS only protects transport, not end-to-end
  • Tight coupling — Both sides must agree on infrastructure
  • No authentication — Hard to prove “this message really came from Agent A”
  • Routing complexity — How does Agent A find Agent B’s endpoint?

DIDComm solves this: encrypted, authenticated messaging using DIDs—agents communicate peer-to-peer without shared infrastructure.


What Is DIDComm?

DIDComm (Decentralized Identifier Communication) is a protocol for secure, private messaging between entities identified by DIDs.

Key properties:

  1. End-to-end encrypted — Only sender and recipient can read the message
  2. Authenticated — You know the message came from the claimed DID
  3. Asynchronous — Works like email (store-and-forward), not just request/response
  4. Transport-agnostic — Send via HTTP, WebSocket, Bluetooth, QR code, etc.
  5. Metadata-private — Routing happens without revealing message content

Analogy: DIDComm is to agents what Signal/WhatsApp is to humans—but decentralized, no central server required.


How DIDComm Works

1. Discovering the Recipient’s Endpoint

Agent A wants to send a message to Agent B:

// Resolve Agent B's DID to get their service endpoint
const didDocB = await resolve('did:webvh:bankB.com:agent-99')

// DID Document contains service endpoint
const endpoint = didDocB.service.find(s => s.type === 'DIDCommMessaging').serviceEndpoint
// → "https://bankB.com/didcomm"

2. Encrypting the Message

Agent A encrypts the message for Agent B:

// Message payload
const message = {
  type: 'https://didcomm.org/trade/1.0/execute',
  from: 'did:webvh:bankA.com:agent-42',
  to: ['did:webvh:bankB.com:agent-99'],
  body: {
    symbol: 'AAPL',
    quantity: 100,
    price: 150.00
  }
}

// Encrypt using Agent B's public key
const encryptedMessage = await encrypt(message, didDocB.keyAgreement)

// Now only Agent B can decrypt it

3. Sending the Message

Agent A sends the encrypted message to Agent B’s endpoint:

await fetch(endpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/didcomm-encrypted+json' },
  body: JSON.stringify(encryptedMessage)
})

4. Decrypting the Message

Agent B receives and decrypts:

// Agent B decrypts with their private key
const decryptedMessage = await decrypt(encryptedMessage, privateKeyB)

console.log(decryptedMessage.from) // 'did:webvh:bankA.com:agent-42'
console.log(decryptedMessage.body) // { symbol: 'AAPL', quantity: 100, ... }

// Agent B verifies the sender's signature
const isAuthentic = await verify(decryptedMessage, didDocA.verificationMethod)
// → true

DIDComm Message Format

Plaintext Message

{
  "type": "https://didcomm.org/trade/1.0/execute",
  "id": "urn:uuid:abc-123",
  "from": "did:webvh:bankA.com:agent-42",
  "to": ["did:webvh:bankB.com:agent-99"],
  "created_time": "2026-07-21T19:00:00Z",
  "body": {
    "symbol": "AAPL",
    "quantity": 100,
    "price": 150.00
  }
}

Encrypted Message (JWE)

{
  "protected": "eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkEyNTZHQ00i...",
  "recipients": [{
    "header": { "kid": "did:webvh:bankB.com:agent-99#key-agreement-1" },
    "encrypted_key": "..."
  }],
  "iv": "...",
  "ciphertext": "...",
  "tag": "..."
}

Only Agent B can decrypt the ciphertext—even intermediary routers can’t read it.


DIDComm vs Traditional Messaging

AspectHTTPS APIMessage Broker (Kafka)DIDComm
EncryptionTLS only (transport)None (unless added)End-to-end (native)
AuthenticationAPI keys/JWTNone (unless added)DID signatures (native)
InfrastructureBoth expose endpointsShared broker requiredNo shared infrastructure
RoutingDNS/IP addressesTopics/partitionsDID resolution
Metadata privacyHeaders visibleHeaders visibleEncrypted routing

Real-World Use Cases

1. Cross-Org Agent Communication

Scenario: Bank A’s agent negotiates with Bank B’s agent.

Flow:

// Agent A (Bank A) sends encrypted trade proposal
const proposal = {
  type: 'https://didcomm.org/trade/1.0/propose',
  from: 'did:webvh:bankA.com:agent-42',
  to: ['did:webvh:bankB.com:agent-99'],
  body: {
    symbol: 'AAPL',
    quantity: 100,
    proposedPrice: 150.00
  }
}

await didcomm.send(proposal)

// Agent B receives, decrypts, and responds
const response = {
  type: 'https://didcomm.org/trade/1.0/accept',
  from: 'did:webvh:bankB.com:agent-99',
  to: ['did:webvh:bankA.com:agent-42'],
  thid: proposal.id, // Thread ID (links to original proposal)
  body: { accepted: true, finalPrice: 150.00 }
}

await didcomm.send(response)

Key benefit: No VPN, no shared message broker—just encrypted peer-to-peer messaging.

2. Agent Handoff (Multi-Org Workflow)

Scenario: Insurance claim passed from Agent A (underwriter) → Agent B (adjuster) → Agent C (payer).

Flow:

// Underwriter agent hands off to adjuster
const handoff = {
  type: 'https://didcomm.org/claim/1.0/handoff',
  from: 'did:webvh:insurer.com:underwriter-agent',
  to: ['did:webvh:insurer.com:adjuster-agent'],
  body: {
    claimId: 'claim-12345',
    status: 'approved-for-adjustment',
    attachments: [vcProofOfApproval]
  }
}

await didcomm.send(handoff)

// Adjuster processes, then hands off to payer
const payment = {
  type: 'https://didcomm.org/claim/1.0/pay',
  from: 'did:webvh:insurer.com:adjuster-agent',
  to: ['did:webvh:bank.com:payment-agent'],
  thid: handoff.id,
  body: {
    claimId: 'claim-12345',
    amount: 5000,
    recipient: 'patient-iban-...'
  }
}

await didcomm.send(payment)

Key benefit: End-to-end audit trail—every handoff is authenticated, encrypted, and linkable via thread IDs.

3. Metadata-Private Routing

Scenario: Agent A sends a message through Mediator M to Agent B (Agent B is behind a firewall).

Flow:

1. Agent A encrypts message for Agent B
2. Agent A wraps it in a forward message for Mediator M
3. Mediator M sees only the outer envelope (can't read inner message)
4. Mediator M forwards to Agent B
5. Agent B decrypts and reads the message

Code:

// Inner message (encrypted for Agent B)
const innerMessage = await encrypt(message, agentB.publicKey)

// Outer envelope (for Mediator M)
const forwardMessage = {
  type: 'https://didcomm.org/routing/2.0/forward',
  to: ['did:webvh:mediator.com'],
  body: {
    next: 'did:webvh:bankB.com:agent-99',
    payloads: [innerMessage]
  }
}

await didcomm.send(forwardMessage)

Key benefit: Mediator can route without reading content—metadata privacy.


DIDComm in the Affinidi Stack

Affinidi Messaging provides DIDComm infrastructure:

import { DIDComm } from '@affinidi/affinidi-messaging'

// Initialize
const comm = new DIDComm({
  did: 'did:webvh:yourcompany.com:agent-42',
  privateKey: '...'
})

// Send encrypted message
await comm.send({
  to: 'did:webvh:partner.com:agent-99',
  type: 'https://example.com/protocol/1.0/message',
  body: { data: 'secret payload' }
})

// Receive messages
const messages = await comm.receive()
console.log(messages[0].body) // Decrypted automatically

Agent Gateway routes DIDComm messages between agents:

// Agent Gateway acts as mediator
agentGateway.on('didcomm-message', async (envelope) => {
  // Verify sender
  const senderDID = envelope.from
  const authorized = await trustRegistry.query({ agent: senderDID })

  if (!authorized) throw new Error('Unauthorized sender')

  // Forward to destination agent
  await didcomm.forward(envelope)
})

Protocol Patterns

Request-Response

// Request
const request = {
  type: 'https://example.com/protocol/1.0/request',
  id: 'urn:uuid:req-123',
  body: { query: 'What is the price of AAPL?' }
}

await didcomm.send(request)

// Response (linked via pthid)
const response = {
  type: 'https://example.com/protocol/1.0/response',
  pthid: 'urn:uuid:req-123', // Parent thread ID
  body: { answer: '$150.00' }
}

Multi-Step Workflow (Thread)

// Step 1: Proposal
const proposal = {
  type: 'https://example.com/trade/1.0/propose',
  id: 'urn:uuid:thread-1',
  body: { symbol: 'AAPL', quantity: 100 }
}

// Step 2: Counter (linked via thid)
const counter = {
  type: 'https://example.com/trade/1.0/counter',
  thid: 'urn:uuid:thread-1',
  body: { proposedPrice: 151.00 }
}

// Step 3: Accept (linked via thid)
const accept = {
  type: 'https://example.com/trade/1.0/accept',
  thid: 'urn:uuid:thread-1',
  body: { finalPrice: 151.00 }
}

Getting Started

For Developers

Send a DIDComm message:

npm install @affinidi/affinidi-messaging
import { DIDComm } from '@affinidi/affinidi-messaging'

const comm = await DIDComm.create({
  did: 'did:webvh:yourcompany.com:agent-42',
  privateKey: process.env.AGENT_PRIVATE_KEY
})

await comm.send({
  to: 'did:webvh:partner.com:agent-99',
  type: 'https://example.com/protocol/1.0/hello',
  body: { message: 'Hello from Agent 42!' }
})

For Architects

When to use DIDComm:

  • ✅ Cross-org agent communication (no shared infrastructure)
  • ✅ End-to-end encryption required
  • ✅ Asynchronous workflows (store-and-forward)
  • ✅ Metadata privacy (routing without revealing content)

When NOT to use:

  • ❌ High-frequency streaming (DIDComm is message-based, not streaming)
  • ❌ Internal systems where HTTPS APIs are simpler
  • ❌ Broadcasting to many recipients (DIDComm is peer-to-peer)

Further Reading

Specification:

Affinidi Docs:

Related Deep Dives:

Related Solutions:

Cookie Preferences

We use cookies to enhance your experience. You can manage your preferences below. For more information, read our Cookie Policy.

Strictly Necessary Always Active

These cookies are essential for core website functions such as security, session integrity, and cookie preference storage. They cannot be disabled.

  • _cf_bm: Distinguishes humans from bots (Cloudflare) · 30m
  • _cfuvid: Ensures secure browsing (Cloudflare) · Session
  • __hs_initial_opt_in: Prevents HubSpot's banner · 7 days
  • _gtm_debug: GTM debug mode (testing only) · Session
Analytics

These cookies help us understand how visitors interact with the site so we can improve content and performance. All data is aggregated and anonymous.

  • _ga, _gid, _gat: Google Analytics · Session – 2 years
  • __hstc, hubspotutk, __hssrc: HubSpot visitor tracking · 13 months
  • __hs_opt_out: HubSpot opt-out preference · 6 months
Marketing & Targeting

These cookies allow us and our partners to serve personalised ads and measure campaign performance.

  • _gcl_au, _gcl_dc: Google Ads conversion tracking · 90 days
  • IDE: Google Display Network personalisation · 1 year
  • _fbp: Meta / Facebook remarketing · 90 days
  • li_gc, _li_fat_id, bcookie: LinkedIn tracking · 1–24 months
  • guest_id, personalization_id: Twitter/X analytics · 2 years