Skip to content

Out-of-band credential offer invitation not recognized correctly by the receiving sub-wallet (multi-tenant ACA-Py) #3966

Description

@nb-pratik-bhimani

Summary

When Agent A creates an issue-credential-v2 offer and embeds it inside an Out-of-Band (OOB) invitation (connectionless), Agent B receives the OOB URL but ACA-Py does not correctly identify it as a credential offer.

The formats, or requests~attach information is not parsed correctly on the receiving agent, causing incorrect request-type detection.


Environment

  • ACA-Py version: 1.3.0
  • Multi-tenant mode: Enabled
  • Wallets involved: Sub-wallet A → sub-wallet B (Same multi-tenant Agent, different 2 wallets)
  • Message protocol: AIP 2.0 (Issue-Credential-v2)
  • DID method used: did:peer:4
  • Transport: HTTP

Steps to Reproduce

1. Agent A creates an Issue-Credential-V2 offer

POST /issue-credential-2.0/create-offer

Payload sent:

{
    "comment": "comment",
    "credential_preview": {
        "attributes": [
            {
                "name": "first_name",
                "value": "John"
            },
            {
                "name": "last_name",
                "value": "Doe"
            },
            {
                "name": "dob_dateint",
                "value": "1990-01-01"
            }
        ]
    },
    "filter": {
        "indy": {
            "cred_def_id": "2w1Eux9fobtPbXAbPJpjip:3:CL:3012385:Id_card",
            "issuer_did": "2w1Eux9fobtPbXAbPJpjip",
            "schema_issuer_did": "2w1Eux9fobtPbXAbPJpjip",
            "schema_name": "Id_card",
            "schema_version": "1.0"
        }
    },
    "auto_issue": true
}

2. Agent A wraps the offer into an OOB invitation

POST /out-of-band/create-invitation

Payload sent:

{
    "accept": [
        "AIP2_0"
    ],
    "handshake_protocols": [
        "https://didcomm.org/didexchange/1.0"
    ],
    "attachments": [
        {
            "id": "65f56f23-083a-4ba6-83e6-40dba01dd993",
            "type": "credential-offer"
        }
    ],
    "goal_code": "issue-vc",
    "goal": "issue a credential",
    "use_did_method": "did:peer:4"
}

This produces the OOB URL.

3. Agent B receives the OOB invitation URL

Receiver extracts:

{
    "@type": "https://didcomm.org/out-of-band/1.1/invitation",
    "@id": "34d31174-e7d2-40f3-86c7-2c85d03d4ce7",
    "label": "tenant agent",
    "handshake_protocols": [
        "https://didcomm.org/didexchange/1.0"
    ],
    "accept": [
        "AIP2_0"
    ],
    "requests~attach": [
        {
            "@id": "request-0",
            "mime-type": "application/json",
            "data": {
                "json": {
                    "@type": "https://didcomm.org/issue-credential/2.0/offer-credential",
                    "@id": "8c426c96-7e6c-4c7c-9839-5392d21e293a",
                    "~thread": {
                        "pthid": "34d31174-e7d2-40f3-86c7-2c85d03d4ce7"
                    },
                    "comment": "comment",
                    "credential_preview": {
                        "@type": "https://didcomm.org/issue-credential/2.0/credential-preview",
                        "attributes": [
                            {
                                "name": "first_name",
                                "value": "John"
                            },
                            {
                                "name": "last_name",
                                "value": "Doe"
                            },
                            {
                                "name": "dob_dateint",
                                "value": "1990-01-01"
                            }
                        ]
                    },
                    "formats": [
                        {
                            "attach_id": "indy",
                            "format": "hlindy/cred-abstract@v2.0"
                        }
                    ],
                    "offers~attach": [
                        {
                            "@id": "indy",
                            "mime-type": "application/json",
                            "data": {
                                "base64": " ... Base64 ..."
                            }
                        }
                    ]
                }
            }
        }
    ],
    "services": [
        "did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj:zUU97Vo6H3YqMGZRx6Z7MD3fmnzFX1wUCBfnoGBouTWLBzKL6HrhSMi7mNNiyuAJuhRaPCizPjpyuL7kycFSckZWVWu4bW2kQpk3apMmvU9JYXoDNytacTHTXtJUzegEv5Rh9PmFmPwfPgzv8igx3yhyj3DkVEf974Nawgi1HAtXhZytdzpdhcikq6syS8ieGUE6d7n7zaUMSS1fh5CejBrpYCqGMyzu8qEjkiP6SKyWUG7ZtHrLYffxgaw5kQrjRQcMy114xo5SbKXb3KPKkEUcU2zZfrduqeNtkDFd6TyhERtHdHqvSCWPLHRFpD4gTbzbUqzm31AqajRipnNP8coQFrPMQ3SCtTMJs99pjjLAYWA5qBEduiNpMQjnF3fef7cKKeS9m7gBZoFdAsYVe6chmBaYJ5w5RsCGrGPKcMzQt1VZDc9C2zfFAQeLEikWGMGjDegGaAmfhvvomun39qiyEMxeNHoy8UMiH3xDMKjrHoiQWteZE2AwnegJEJWje8NZPFV7wmdNZJr4SWpJG2b9ZM6DVJfHrKsS93dvvdV8kYy1WeAr2Zb8YEZjaTqQabNYv9dyT1s3h3pFjLL69dqQ3v4uwi"
    ]
}

4. Agent B calls

POST /out-of-band/receive-invitation

Expected Result

Agent B should classify this as:

  • Credential Offer
  • Goal code → issue-vc
  • formats → hlindy/cred-abstract@v2.0
  • Type → Issue-Credential-V2 flow

Actual Result

  • Receiver cannot determine the request type reliably.
  • formats not consistently passed into the final OobRecord.
  • ACA-Py returns incomplete OobRecord for invitations that include credential offers.

Actual Logs

Output from:

POST /out-of-band/receive-invitation
Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 444, in resolve_didcomm_services
    doc_dict: dict = await resolver.resolve(self._profile, did, service_accept)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 87, in resolve
    _, doc = await self._resolve(profile, did, service_accept, timeout=timeout)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 76, in _resolve
    raise DIDNotFound(f"DID {did} could not be resolved")
acapy_agent.resolver.base.DIDNotFound: DID did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj could not be resolved

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 314, in __step_run_and_handle_result
    result = coro.send(None)
             ^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 222, in upgrade_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp_apispec/middlewares.py", line 51, in validation_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/decorators/auth.py", line 84, in tenant_auth
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/routes.py", line 372, in invitation_receive
    result = await oob_mgr.receive_invitation(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 755, in receive_invitation
    oob_record = await self._perform_handshake(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 1091, in _perform_handshake
    conn_record = await didx_mgr.receive_invitation(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/didexchange/v1_0/manager.py", line 159, in receive_invitation
    targets = await self.resolve_connection_targets(conn_rec.their_public_did)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 541, in resolve_connection_targets
    doc, didcomm_services = await self.resolve_didcomm_services(did)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 447, in resolve_didcomm_services
    raise BaseConnectionManagerError("Failed to resolve DID services") from error
acapy_agent.connections.base_manager.BaseConnectionManagerError: Failed to resolve DID services
2025-12-01 11:50:30,806 acapy_agent.admin.server ERROR Handler error with exception:
Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 444, in resolve_didcomm_services
    doc_dict: dict = await resolver.resolve(self._profile, did, service_accept)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 87, in resolve
    _, doc = await self._resolve(profile, did, service_accept, timeout=timeout)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 76, in _resolve
    raise DIDNotFound(f"DID {did} could not be resolved")
acapy_agent.resolver.base.DIDNotFound: DID did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj could not be resolved

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 146, in ready_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 238, in debug_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 360, in setup_context
    return await task
           ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 289, in __await__
    yield self  # This tells Task to wait for completion.
    ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 385, in __wakeup
    future.result()
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 202, in result
    raise self._exception.with_traceback(self._exception_tb)
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 314, in __step_run_and_handle_result
    result = coro.send(None)
             ^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 222, in upgrade_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp_apispec/middlewares.py", line 51, in validation_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/decorators/auth.py", line 84, in tenant_auth
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/routes.py", line 372, in invitation_receive
    result = await oob_mgr.receive_invitation(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 755, in receive_invitation
    oob_record = await self._perform_handshake(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 1091, in _perform_handshake
    conn_record = await didx_mgr.receive_invitation(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/didexchange/v1_0/manager.py", line 159, in receive_invitation
    targets = await self.resolve_connection_targets(conn_rec.their_public_did)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 541, in resolve_connection_targets
    doc, didcomm_services = await self.resolve_didcomm_services(did)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 447, in resolve_didcomm_services
    raise BaseConnectionManagerError("Failed to resolve DID services") from error
acapy_agent.connections.base_manager.BaseConnectionManagerError: Failed to resolve DID services
2025-12-01 11:50:30,808 aiohttp.server ERROR Error handling request from 192.168.1.13
Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 444, in resolve_didcomm_services
    doc_dict: dict = await resolver.resolve(self._profile, did, service_accept)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 87, in resolve
    _, doc = await self._resolve(profile, did, service_accept, timeout=timeout)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 76, in _resolve
    raise DIDNotFound(f"DID {did} could not be resolved")
acapy_agent.resolver.base.DIDNotFound: DID did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj could not be resolved

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp/web_protocol.py", line 510, in _handle_request
    resp = await request_handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp/web_app.py", line 569, in _handle
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp/web_middlewares.py", line 117, in impl
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 146, in ready_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 238, in debug_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 360, in setup_context
    return await task
           ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 289, in __await__
    yield self  # This tells Task to wait for completion.
    ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 385, in __wakeup
    future.result()
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 202, in result
    raise self._exception.with_traceback(self._exception_tb)
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 314, in __step_run_and_handle_result
    result = coro.send(None)
             ^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 222, in upgrade_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp_apispec/middlewares.py", line 51, in validation_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/decorators/auth.py", line 84, in tenant_auth
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/routes.py", line 372, in invitation_receive
    result = await oob_mgr.receive_invitation(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 755, in receive_invitation
    oob_record = await self._perform_handshake(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 1091, in _perform_handshake
    conn_record = await didx_mgr.receive_invitation(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/didexchange/v1_0/manager.py", line 159, in receive_invitation
    targets = await self.resolve_connection_targets(conn_rec.their_public_did)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 541, in resolve_connection_targets
    doc, didcomm_services = await self.resolve_didcomm_services(did)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 447, in resolve_didcomm_services
    raise BaseConnectionManagerError("Failed to resolve DID services") from error
acapy_agent.connections.base_manager.BaseConnectionManagerError: Failed to resolve DID services


Problem Summary

The OOB invitation contains a valid Issue-Credential-V2 offer, but ACA-Py’s internal handling of OOB 1.1 → credential offer mapping appears inconsistent:

  • formats inside requests~attach are not reflected in the returned OOB record.
  • Makes it impossible to reliably detect credential-offer vs proof-request on the receiving agent.

This behavior breaks multi-tenant workflows where OOB invitations are used to pass issue-credential-v2 offers.


Possible Causes

  1. requests~attachcredJson not parsed fully
  2. Issue-Credential-V2 “offer-credential” is not mapped to ACA-Py goal codes internally

Request

Please confirm whether:

  • This is an intended ACA-Py behavior
    OR
  • A bug in OOB v1.1 behavior when wrapping Issue-Credential-v2 offer messages.

If this is a bug, guidance or a fix would be appreciated.


Additional Context

This use-case is sub-wallet to sub-wallet (same agent instance) .

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions