Skip to main content
Version: 0.7

Connections

A DIDComm connection links two agents through a pair of DIDs, one for each side. Most DIDComm protocols run over a connection. This page covers invitations, connection states and events, and reusing connections.

How a connection is made​

One agent, the inviter, creates an out-of-band invitation and shares it as a URL or QR code. The other agent, the invitee, receives it and runs the DID Exchange protocol with the inviter.

The examples on this page run Acme and Bob as two Node.js agents that reach each other over HTTP:

connections.ts
import { Agent } from '@credo-ts/core'
import {
DidCommConnectionEventTypes,
type DidCommConnectionStateChangedEvent,
DidCommDidExchangeState,
DidCommHttpOutboundTransport,
DidCommModule,
} from '@credo-ts/didcomm'
import { agentDependencies, DidCommHttpInboundTransport } from '@credo-ts/node'

function createAgent(port: number, storage: ExampleStorage) {
return new Agent({
dependencies: agentDependencies,
modules: {
...storage, // Storage and key management, see Agent setup
didcomm: new DidCommModule({
endpoints: [`http://localhost:${port}`],
transports: {
inbound: [new DidCommHttpInboundTransport({ port })],
outbound: [new DidCommHttpOutboundTransport()],
},
connections: { autoAcceptConnections: true },
}),
},
})
}

const acme = createAgent(4042, acmeStorage)
const bob = createAgent(4043, bobStorage)
await acme.initialize()
await bob.initialize()

With autoAcceptConnections: true, the inviter accepts DID Exchange requests automatically. On the invitee side, receiving an invitation accepts it by default. Accept requests manually shows how to approve each request yourself.

Which invitation should I use?​

InvitationUse it when
Out-of-band invitationYou show a link or QR code to one user, for example during a web session. This is the default.
Multi-use invitationYou print or publish one QR code for everyone. Each invitee gets its own connection.
Connectionless, with handshake: false and messagesYou only want to issue one credential or request one proof, without keeping a connection. See Issuing credentials and Requesting proofs.
Invitation with a public DIDReturning users should reuse their existing connection instead of creating a new one.
Implicit invitationThe invitee already knows your public DID and connects without an invitation. You accept each request manually.

With DID Exchange, each side of the connection uses a new did:peer that only the other party knows, so connections with different parties can't be linked. A public DID, such as a did:web, only identifies the inviter in the invitation. You need one only to reuse connections or to accept implicit invitations.

Create an invitation​

acme.ts
const outOfBandRecord = await acme.didcomm.oob.createInvitation({ label: 'Acme' })
const invitationUrl = outOfBandRecord.outOfBandInvitation.toUrl({ domain: 'https://acme.example.com/invite' })

toUrl() encodes the invitation in the oob query parameter of the URL you pass as domain. Use a domain you control. Render the URL as a QR code with any QR code library.

Long invitations make dense QR codes. You can also share a short URL: when an invitation URL has no oob parameter, Credo fetches it and accepts either a JSON response with the invitation, or a redirect to the full invitation URL.

createInvitation() options you may need:

OptionDescription
labelName shown to the invitee.
aliasName for the connection in your own records.
imageUrlImage shown to the invitee.
goalCode, goalWhat the connection is for.
multiUseInvitationLet more than one agent use the invitation. See Multi-use invitations.
autoAcceptConnectionOverride autoAcceptConnections for this invitation.
messagesAttach a message, such as a credential offer or proof request, to the invitation. See Issuing credentials and Requesting proofs.
handshakeSet to false to only send the attached messages, without creating a connection.
invitationDidUse an existing DID instead of a new key in the invitation. See Reuse a connection.
routingRoute the connection through a specific mediator. See Mediation.

Receive an invitation​

bob.ts
const invitation = await bob.didcomm.oob.parseInvitation(invitationUrl)
console.log(`Bob received an invitation from ${invitation.label}`)

const { connectionRecord } = await bob.didcomm.oob.receiveInvitation(invitation, { label: 'Bob' })
if (!connectionRecord) throw new Error('The invitation has no handshake protocols')

const bobToAcme = await bob.didcomm.connections.returnWhenIsConnected(connectionRecord.id)

parseInvitation() decodes the URL so you can show the invitation to the user before accepting it. receiveInvitation() then starts DID Exchange. To do both in one call, use receiveInvitationFromUrl(url, config).

label is required: it is the name the inviter sees for you. Other options:

OptionDefaultDescription
aliasName for the connection in your own records.
autoAcceptInvitationtrueStart DID Exchange right away. If false, call agent.didcomm.oob.acceptInvitation(outOfBandRecord.id, { label }) later.
autoAcceptConnectiontrueComplete the connection when the response arrives. If false, call agent.didcomm.connections.acceptResponse(connectionId).
reuseConnectionfalseUse an existing connection with the inviter if there is one. See Reuse a connection.
routingdefault mediatorRoute the connection through a specific mediator.

returnWhenIsConnected() waits until the connection reaches the completed state, for up to 20 seconds unless you pass { timeoutMs }.

Credo parses oob invitation URLs, c_i URLs from the deprecated Connections protocol, and legacy connectionless d_m URLs. An agent can't receive the same invitation twice: the second attempt throws.

Connection states and events​

Each connection is a DidCommConnectionRecord with a state from DidCommDidExchangeState:

StateSideMeaning
invitation-receivedInviteeConnection created, request not sent yet.
request-sentInviteeWaiting for the inviter's response.
request-receivedInviterWaiting for you to accept the request, unless auto-accept is on.
response-sentInviterResponse sent, waiting for the invitee to complete.
response-receivedInviteeWaiting for you to accept the response, unless auto-accept is on.
completedBothThe connection is ready.

The inviter has no connection record until the first request arrives. Listen for connection state changes and filter on the out-of-band record to react to connections from a specific invitation:

acme.ts
acme.events.on<DidCommConnectionStateChangedEvent>(
DidCommConnectionEventTypes.DidCommConnectionStateChanged,
({ payload: { connectionRecord } }) => {
if (connectionRecord.outOfBandId !== outOfBandRecord.id) return

if (connectionRecord.state === DidCommDidExchangeState.Completed) {
console.log(`Acme is connected with ${connectionRecord.theirLabel}`)
}
}
)

To look up the connections an invitation created, use findAllByOutOfBandId():

acme.ts
const [acmeToBob] = await acme.didcomm.connections.findAllByOutOfBandId(outOfBandRecord.id)
await acme.didcomm.connections.returnWhenIsConnected(acmeToBob.id)

The out-of-band record has its own state (DidCommOutOfBandState: initial, await-response, prepare-response, done) and emits DidCommOutOfBandEventTypes.OutOfBandStateChanged.

Accept requests manually​

Create the invitation with autoAcceptConnection: false, or leave autoAcceptConnections off in the module config. Then accept each request when it arrives:

acme.ts
acme.events.on<DidCommConnectionStateChangedEvent>(
DidCommConnectionEventTypes.DidCommConnectionStateChanged,
async ({ payload: { connectionRecord } }) => {
if (connectionRecord.state !== DidCommDidExchangeState.RequestReceived) return
// Requests from implicit invitations, or from invitations created with autoAcceptConnection: false
if (connectionRecord.autoAcceptConnection) return

await acme.didcomm.connections.acceptRequest(connectionRecord.id)
}
)

Requests from implicit invitations always need this step.

Multi-use invitations​

A regular invitation creates one connection. A multi-use invitation stays valid, and every agent that uses it gets its own connection. Use it for invitations you print or publish, like a QR code on a website.

acme.ts
const multiUseRecord = await acme.didcomm.oob.createInvitation({ label: 'Acme', multiUseInvitation: true })
const multiUseUrl = multiUseRecord.outOfBandInvitation.toUrl({ domain: 'https://acme.example.com/invite' })

After Carol and Dave each receive the invitation:

acme.ts
const multiUseConnections = await acme.didcomm.connections.findAllByOutOfBandId(multiUseRecord.id)
console.log(`The multi-use invitation created ${multiUseConnections.length} connections`)

You can't attach messages to a multi-use invitation.

Reuse a connection​

When an inviter puts the same DID in all of its invitations, an invitee that is already connected can reuse that connection instead of creating a new one. The inviter sets invitationDid, usually a public DID with a DIDComm service in its DID document (see DIDs):

acme.ts
// A DID that Acme created, with a DIDComm service in its DID document
const publicDid = 'did:web:acme.example.com'

const outOfBandRecord = await acme.didcomm.oob.createInvitation({ label: 'Acme', invitationDid: publicDid })
const invitationUrl = outOfBandRecord.outOfBandInvitation.toUrl({ domain: 'https://acme.example.com/invite' })

The invitee passes reuseConnection: true. If it has a connection with that DID, Credo sends a handshake reuse message over it and returns the existing connection:

bob.ts
const { connectionRecord } = await bob.didcomm.oob.receiveInvitationFromUrl(invitationUrl, {
label: 'Bob',
reuseConnection: true,
})

The inviter learns about the reuse through an event:

acme.ts
acme.events.on<DidCommHandshakeReusedEvent>(DidCommOutOfBandEventTypes.HandshakeReused, ({ payload }) => {
console.log(`${payload.connectionRecord.theirLabel} reused connection ${payload.connectionRecord.id}`)
})

Invitations without invitationDid contain a new key each time, so Credo never matches them to an existing connection.

Implicit invitations​

If you know an agent's public DID, you can connect without an invitation. Credo builds the invitation from the DIDComm services in the DID document:

bob.ts
const { connectionRecord: bobToAcme } = await bob.didcomm.oob.receiveImplicitInvitation({
did: publicDid,
label: 'Bob',
})

did can also be a DID URL that points to one service, for example did:web:acme.example.com#didcomm. The inviter must have created the DID in its own agent, and it accepts the request manually: Credo never accepts requests from implicit invitations automatically.

Legacy connection invitations​

Deprecated

The Connections 1.0 protocol (RFC 0160) is deprecated and removed in Credo 0.8. Use out-of-band invitations with DID Exchange.

createInvitation() lists both DID Exchange and Connections 1.0 as handshake protocols, with DID Exchange first. To offer only DID Exchange, pass handshakeProtocols: [DidCommHandshakeProtocol.DidExchange].

Agents that only support the Connections protocol need a c_i invitation:

acme.ts
const { invitation } = await acme.didcomm.oob.createLegacyInvitation({ label: 'Acme' })
const invitationUrl = invitation.toUrl({ domain: 'https://acme.example.com/invite' })

Manage connections​

Method on agent.didcomm.connectionsDescription
sendPing(connectionId)Send a trust ping to check that the other agent is reachable.
rotate({ connectionId })Switch to a new DID for this connection and tell the other agent (DID Rotate).
hangup({ connectionId, deleteAfterHangup })Tell the other agent the connection is over. With deleteAfterHangup: true, Credo also deletes the record.
deleteById(connectionId)Delete the connection record without notifying the other agent.
findAllByQuery(query)Find connections, for example by state or theirDid.

Next steps​