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:
- End-to-end encrypted — Only sender and recipient can read the message
- Authenticated — You know the message came from the claimed DID
- Asynchronous — Works like email (store-and-forward), not just request/response
- Transport-agnostic — Send via HTTP, WebSocket, Bluetooth, QR code, etc.
- 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
| Aspect | HTTPS API | Message Broker (Kafka) | DIDComm |
|---|---|---|---|
| Encryption | TLS only (transport) | None (unless added) | End-to-end (native) |
| Authentication | API keys/JWT | None (unless added) | DID signatures (native) |
| Infrastructure | Both expose endpoints | Shared broker required | No shared infrastructure |
| Routing | DNS/IP addresses | Topics/partitions | DID resolution |
| Metadata privacy | Headers visible | Headers visible | Encrypted 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:
- Decentralized Identifiers — DIDs are the sender/recipient identifiers in DIDComm
- Verifiable Credentials — Often attached to DIDComm messages as proofs
Related Solutions:
- Cross-Org Agent Trust — DIDComm enables encrypted communication across boundaries