Confusing their_role value in ACA-Py connection request – shows inviter for request-sender
- In the current ACA-Py implementation, there's potential confusion around the their_role field across all connection states — not just during the request phase.
Definitions (as understood from documentation):
- Inviter → The party that sends the invitation.
- Invitee → The party that receives the invitation and sends a connection request.
Observation:
- When a invitation request is sent (by the Inviter), the their_role is returned as inviter—this seems counterintuitive, especially for client applications trying to determine who initiated the connection.
Steps to Reproduce
- Holder (Inviter) creates an invitation using ACA-Py:
- POST /didexchange/create-invitation
- Response includes the invitation details:
{
"state": "request",
"created_at": "2025-09-29T14:02:21.243697Z",
"updated_at": "2025-09-29T14:02:21.252565Z",
"connection_id": "2cc619e9-89fb-4f79-8595-e1fba0d7e995",
"my_did": "JE2seYAKJVkuGYvdurbJwA",
"their_did": "did:sov:hMdL27Sh3voQzhr1vvGJ3",
"their_role": "inviter",
"connection_protocol": "didexchange/1.0",
"rfc23_state": "request-sent",
"request_id": "2649c13e-4d55-4e8e-a5c8-edcb4b088927",
"accept": "manual",
"invitation_mode": "once",
"alias": "John Holder",
"their_public_did": "did:sov:hMdL27Sh3voQzhr1vvGJ3"
}
- Issuer (Invitee) receives the invitation request:
- GET /connections
- Response includes the invitation details:
{
"state": "request",
"created_at": "2025-09-29T14:02:21.391297Z",
"updated_at": "2025-09-29T14:02:21.391297Z",
"connection_id": "17daa1b0-3ab2-4528-81e4-aff89a932a0f",
"their_did": "JE2seYAKJVkuGYvdurbJwA",
"their_label": "Holder Agent",
"their_role": "invitee",
"connection_protocol": "didexchange/1.0",
"rfc23_state": "request-received",
"invitation_key": "Nzip9urmchNE2JGsjnp66ZRNYNSDibimrAMZvCvk1sK",
"request_id": "2649c13e-4d55-4e8e-a5c8-edcb4b088927",
"accept": "manual",
"invitation_mode": "once"
}
Environment
- ACA-Py version: 1.3.0
- Protocol: didexchange/1.0
- Transport: HTTP / Admin API
I agree that it's confusing. Might be a mistake, but I'm not sure.
What's particularly confusing to me is that the DID Exchange protocol roles should actually be "requester" or "responder".
And the ConnRecord class caters for the distinct roles depending on the protocol:
class ConnRecord(BaseRecord):
"""Represents a single pairwise connection."""
...
class Role(Enum):
"""RFC 160 (inviter, invitee) = RFC 23 (responder, requester)."""
REQUESTER = ("invitee", "requester") # == RFC 23 initiator, RFC 434 receiver
RESPONDER = ("inviter", "responder") # == RFC 160 initiator(!), RFC 434 sender
@property
def rfc160(self): # -> Literal['invitee', 'inviter']
"""Return RFC 160 (connection protocol) nomenclature."""
return self.value[0]
@property
def rfc23(self): # -> Literal['requester', 'responder']
"""Return RFC 23 (DID exchange protocol) nomenclature."""
return self.value[1]
rfc23 is used throughout the DIDXManager. e.g.:
The POST /didexchange/create-request endpoint calls DIDXManager.create_request_implicit, which creates the connection record:
conn_rec = ConnRecord(
..., their_role=ConnRecord.Role.RESPONDER.rfc23, ...
)
That means, according to the code, their_role should be "responder" (rfc23 version of "inviter"). But it must be overwritten / converted somewhere, and after digging for a bit I can't see where that change is happening...
That's kind of secondary to the question whether it should be invitee/requester. But just wanted to share that as an additional uncertainty. Not sure why/how it's switching from rfc23 to rfc160, somewhere between the record being created and returned.
Definitely sounds like a bug. The RFC23 / RFC160 issue is bad enough -- especially since RFC23 is the NEW approach, and RFC160 is the deprecated approach. Spec'ing while implementing is painful at times...
It would be good to see where the value is changed, as @ff137 mentions.
@swcurran I found the rfc23/rfc160 mismatch is in the ConnRecord init constructor, where it overrides their_role to use the rfc160 version. Change was last applied here, where stored values use the legacy values:
- https://github.com/openwallet-foundation/acapy/pull/790
That was 5 years ago 👀 and it was legacy then... so it's probably good to make a change now?
The issue with changing how roles are stored, is that many methods retrieve connection records based on their_role, e.g.:
class ConnRecord(BaseRecord):
"""Represents a single pairwise connection."""
...
@classmethod
async def retrieve_by_request_id(
cls, session: ProfileSession, request_id: str, their_role: Optional[str] = None
) -> "ConnRecord":
"""Retrieve a connection record from our previous request ID.
Args:
session: The active profile session
request_id: The ID of the originating connection request
their_role: Filter by their role
"""
tag_filter = {"request_id": request_id}
if their_role:
tag_filter["their_role"] = their_role
return await cls.retrieve_by_tag_filter(session, tag_filter)
Changing inviter -> requester (same role, different protocol) in how records are stored is not too bad. Because Role is actually an Enum, and it's being stored and read as a string. This would be better if Role is always treated as an Enum. So the retrieve methods fetch by Role, not str -- and the string value that finally gets displayed depends on the connection_protocol. That way the stored record values don't need to change.
However, changing invitee -> inviter/requester is of course a much bigger change, because it would entail migrating old records to also follow the correct pattern. Or just having the caveat that ConnRecord "their_role" metadata is reversed, if it was created before a certain version. As far as I can tell it does seem to be reversed. (I always understood it as "the issuer giving their public did is like initiating the invite", but that was just a guess, trying to rationalise the observation! Not based on any docs.)
I can help with the former change, using Enums instead of strings for the role. But the latter one I'll leave for others to discuss and decide upon.