A library for interacting with ACA-py in Go.
You can create your own Self-Sovereign Identity solution using the Hyperledger Ursa, Indy, Aries stack. Learn more about the background by watching these videos:
- The Story of Open SSI Standards
- Decentralized Identifiers (DIDs) - The Fundamental Building Block of Self Sovereign Identity
To become an Aries developer, attend these courses by the Linux Foundation on edx.org:
- Introduction to Hyperledger Sovereign Identity Blockchain Solutions: Indy, Aries & Ursa
- Becoming a Hyperledger Aries Developer
$ go get -u github.com/ldej/go-acapy-client
Both ACA-py and go-acapy-client
are under active development and might be incompatible. Currently go-acapy-client
supports v0.6.0-pre of ACA-py.
Start a local Indy ledger network VON-network. Make a checkout of github.com/bcgov/von-network. Then run:
./manage start --logs
This starts 4 Indy nodes and a von-webserver. The von-webserver has a web interface at localhost:9000 which allows you to browse the transactions in the blockchain.
Start a Tails server for the revocation registry tails files: Make a checkout of github.com/bcgov/indy-tails-server. Then run:
./docker/manage start
Start an Aries-Cloud-Agent-Python (ACA-py) instance and configure the right command line parameters. Read about ACA-py and the command line parameters on my blog:
- Becoming a Hyperledger Aries Developer - Part 1: Terminology
- Becoming a Hyperledger Aries Developer - Part 2: Development Environment
- Becoming a Hyperledger Aries Developer - Part 3: Connecting using Swagger
- Becoming a Hyperledger Aries Developer - Part 3: Connecting using DIDComm Exchange
- Becoming a Hyperledger Aries Developer - Part 4: Connecting using go-acapy-client
- Becoming a Hyperledger Aries Developer - Part 5: Issue Credentials
- Becoming a Hyperledger Aries Developer - Part 6: Revocation
- Becoming a Hyperledger Aries Developer - Part 7: Present Proof
- Connecting ACA-py to Development Ledgers
- Aries Cloud Agent Python (ACA-py) Webhooks
Create a client, register a DID in the ledger and create an invitation.
package main
import "github.com/ldej/go-acapy-client"
func main() {
var ledgerURL = "http://localhost:9000"
var acapyURL = "http://localhost:8000"
client := acapy.NewClient(acapyURL)
didResponse, err := acapy.RegisterDID(ledgerURL, "Alice", "000000000000000000000000MySeed01", acapy.Endorser)
if err != nil {
// handle error
}
// Start aca-py with registered DID
invitation, err := client.CreateInvitation("Bob", false, false, false)
if err != nil {
// handle error
}
}
Examples can be found in the examples folder.
{id}
= connection identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
- | POST | /action-menu/{id}/close | ❗ |
- | POST | /action-menu/{id}/fetch | ❗ |
- | POST | /action-menu/{id}/perform | ❗ |
- | POST | /action-menu/{id}/request | ❗ |
- | POST | /action-menu/{id}/send-menu | ❗ |
{id}
= connection identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
SendBasicMessage | POST | /connections/{id}/send-message | ✔️ |
{id}
= connection identifier
{ref_id}
= inbound connection identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
QueryConnections | GET | /connections | ✔️ |
CreateInvitation | POST | /connections/create-invitation | ✔️ |
- | POST | /connections/create-static | ❗ |
ReceiveInvitation | POST | /connections/receive-invitation | ✔️ |
GetConnection | GET | /connections/{id} | ✔️ |
RemoveConnection | DELETE | /connections/{id} | ✔️ |
AcceptInvitation | POST | /connections/{id}/accept-invitation | ✔️ |
AcceptRequest | POST | /connections/{id}/accept-request | ✔️ |
- | POST | /connections/{id}/establish-inbound/{ref_id} | ❗ |
- | GET | /connections/{id}/metadata | ❗ |
- | POST | /connections/{id}/metadata | ❗ |
{id}
= credential definition identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
CreateCredentialDefinitions | POST | /credential-definitions | ✔️ |
QueryCredentialDefinitions | GET | /credential-definitions/created | ✔️ |
GetCredentialDefinition | GET | /credential-definitions/{id} | ✔️ |
{id}
= credential identifier, also known as referent
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
CredentialMimeTypes | GET | /credential/mime-types/{id} | ✔️ |
IsCredentialRevoked | GET | /credential/revoked/{id} | ✔️ |
GetCredential | GET | /credential/{id} | ✔️ |
RemoveCredential | DELETE | /credential/{id} | ✔️ |
GetCredentials | GET | /credentials | ✔️ |
{id}
= connection identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
DIDExchangeAcceptInvitation | POST | /didexchange/{id}/accept-invitation | ✔️ |
DIDExchangeAcceptRequest | POST | /didexchange/{id}/accept-request | ✔️ |
{id}
= connection identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
- | POST | /connections/{id}/start-introduction | ❗ |
{id}
= credential exchange identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
CreateCredentialExchange | POST | /issue-credential/create | ✔️ |
QueryCredentialExchange | GET | /issue-credential/records | ✔️ |
GetCredentialExchange | GET | /issue-credential/records/{id} | ✔️ |
RemoveCredentialExchange | DELETE | /issue-credential/records/{id} | ✔️ |
IssueCredentialByID | POST | /issue-credential/records/{id}/issue | ✔️ |
ReportCredentialExchangeProblem | POST | /issue-credential/records/{id}/problem-report | ✔️ |
SendCredentialOfferByID | POST | /issue-credential/records/{id}/send-offer | ✔️ |
SendCredentialRequestByID | POST | /issue-credential/records/{id}/send-request | ✔️ |
StoreReceivedCredential | POST | /issue-credential/records/{id}/store | ✔️ |
SendCredential | POST | /issue-credential/send | ✔️ |
SendCredentialOffer | POST | /issue-credential/send-offer | ✔️ |
SendCredentialProposal | POST | /issue-credential/send-proposal | ✔️ |
{id}
= credential exchange identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
CreateCredentialExchangeV2 | POST | /issue-credential-2.0/create | ✔️ |
QueryCredentialExchangeV2 | GET | /issue-credential-2.0/records | ✔️ |
GetCredentialExchangeV2 | GET | /issue-credential-2.0/records/{id} | ✔️ |
RemoveCredentialExchangeV2 | DELETE | /issue-credential-2.0/records/{id} | ✔️ |
IssueCredentialByIDV2 | POST | /issue-credential-2.0/records/{id}/issue | ✔️ |
ReportCredentialExchangeProblemV2 | POST | /issue-credential-2.0/records/{id}/problem-report | ✔️ |
SendCredentialOfferByIDV2 | POST | /issue-credential-2.0/records/{id}/send-offer | ✔️ |
SendCredentialRequestByIDV2 | POST | /issue-credential-2.0/records/{id}/send-request | ✔️ |
StoreReceivedCredentialV2 | POST | /issue-credential-2.0/records/{id}/store | ✔️ |
SendCredentialV2 | POST | /issue-credential-2.0/send | ✔️ |
SendCredentialOfferV2 | POST | /issue-credential-2.0/send-offer | ✔️ |
SendCredentialProposalV2 | POST | /issue-credential-2.0/send-proposal | ✔️ |
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
GetDIDEndpointFromLedger | GET | /ledger/did-endpoint | ✔️ |
GetDIDVerkeyFromLedger | GET | /ledger/did-verkey | ✔️ |
GetDIDRoleFromLedger | GET | /ledger/get-nym-role | ✔️ |
- | POST | /ledger/register-nym | ❗ |
- | PATCH | /ledger/rotate-public-did-keypair | ❗ |
- | GET | /ledger/taa | ❗ |
- | POST | /ledger/taa/accept | ❗ |
{id}
= connection identifier
{mid}
= mediation identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
- | GET | /mediation/default-mediator | ❗ |
- | DELETE | /mediation/default-mediator | ❗ |
- | GET | /mediation/keylists | ❗ |
- | POST | /mediation/keylists/{mid}/send-keylist-query | ❗ |
- | POST | /mediation/keylists{mid}/send-keylist-update | ❗ |
- | POST | /mediation/request/{id} | ❗ |
- | GET | /mediation/requests | ❗ |
- | GET | /mediation/requests/{mid} | ❗ |
DELETE | /mediation/requests/{mid} | ❗ | |
- | POST | /mediation/requests/{mid}/deny | ❗ |
- | POST | /mediation/requests/{mid}/grant | ❗ |
- | PUT | /mediation/{mid}/default-mediator | ❗ |
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
CreateOutOfBandInvitation | POST | /out-of-band/create-invitation | ✔️ |
ReceiveOutOfBandInvitation | POST | /out-of-band/receive-invitation | ✔️ |
{id}
= presentation exchange identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
CreatePresentationRequest | POST | /present-proof/create-request | ✔️ |
QueryPresentationExchange | GET | /present-proof/records | ✔️ |
GetPresentationExchangeByID | GET | /present-proof/records/{id} | ✔️ |
RemovePresentationExchangeByID | DELETE | /present-proof/records/{id} | ✔️ |
GetPresentationCredentialsByID | GET | /present-proof/records/{id}/credentials | ✔️ |
SendPresentationRequestByID | POST | /present-proof/records/{id}/send-request | ✔️ |
SendPresentationByID | POST | /present-proof/records/{id}/send-presentation | ✔️ |
VerifyPresentationByID | POST | /present-proof/records/{id}/verify-presentation | ✔️ |
SendPresentationProposal | POST | /present-proof/send-proposal | ✔️ |
SendPresentationRequest | POST | /present-proof/send-request | ✔️ |
{id}
= revocation registry identifier, {cred_def_id}
= credential definition identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
GetActiveRevocationRegistry | GET | /revocation/active-registry/{cred_def_id} | ✔️ |
ClearPendingRevocations | POST | /revocation/clear-pending-revocations | ✔️ |
CreateRevocationRegistry | POST | /revocation/create-registry | ✔️ |
GetCredentialRevocationStatus | GET | /revocation/credential-record | ✔️ |
PublishRevocations | POST | /revocation/publish-revocations | ✔️ |
QueryRevocationRegistries | GET | /revocation/registries/created | ✔️ |
GetRevocationRegistry | GET | /revocation/registry/{id} | ✔️ |
UpdateRevocationRegistryTailsURI | PATCH | /revocation/registry/{id} | ✔️ |
PublishRevocationRegistryDefinition | POST | /revocation/registry/{id}/definition | ✔️ |
PublishRevocationRegistryEntry | POST | /revocation/registry/{id}/entry | ✔️ |
GetNumberOfIssuedCredentials | GET | /revocation/registry/{id}/issued | ✔️ |
SetRevocationRegistryState | PATCH | /revocation/registry/{id}/set-state | ✔️ |
UploadRegistryTailsFile | PUT | /revocation/registry/{id}/tails-file | ✔️ |
DownloadRegistryTailsFile | GET | /revocation/registry/{id}/tails-file | ✔️ |
RevokeIssuedCredential | POST | /revocation/revoke | ✔️ |
{id}
= schema identifier
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
RegisterSchema | POST | /schemas | ✔️ |
QuerySchemas | GET | /schemas/created | ✔️ |
GetSchema | GET | /schemas/{id} | ✔️ |
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
Features | GET | /features | ✔️ |
Plugins | GET | /plugins | ✔️ |
Shutdown | GET | /shutdown | ✔️ |
Status | GET | /status | ✔️ |
IsAlive | GET | /status/live | ✔️ |
IsReady | GET | /status/ready | ✔️ |
ResetStatistics | POST | /status/reset | ✔️ |
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
SendPing | POST | /connections/{id}/send-ping | ✔️ |
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
QueryDIDs | GET | /wallet/did | ✔️ |
CreateLocalDID | POST | /wallet/did/create | ✔️ |
RotateKeypair | PATCH | /wallet/did/local/rotate-keypair | ✔️ |
GetPublicDID | GET | /wallet/did/public | ✔️ |
SetPublicDID | POST | /wallet/did/public | ✔️ |
GetDIDEndpointFromWallet | GET | /wallet/get-public-did | ✔️ |
SetDIDEndpointInWallet | POST | /wallet/set-public-did | ✔️ |
Function Name | Method | Endpoint | Implemented |
---|---|---|---|
SignJSONLD | GET | /jsonld/sign | ❗ |
VerifyJSONLD | GET | /jsonld/verify | ❗ |
When an event occurs in ACA-py, for example a connection request has been received, a webhook is called on your controller on a certain topic. go-acapy-client
provides a webhook handler where you can register your own functions to handle these events. Based on an event happening you can update your UI or inform the user about the event.
Read more about ACA-py webhooks on my blog.
func main() {
r := mux.NewRouter()
webhookHandler := acapy.CreateWebhooksHandler(acapy.WebhookHandlers{
ConnectionsEventHandler: ConnectionsEventHandler,
BasicMessagesEventHandler: BasicMessagesEventHandler,
ProblemReportEventHandler: ProblemReportEventHandler,
CredentialExchangeEventHandler: CredentialExchangeEventHandler,
CredentialExchangeV2EventHandler: CredentialExchangeV2EventHandler,
CredentialExchangeDIFEventHandler: CredentialExchangeDIFEventHandler,
CredentialExchangeIndyEventHandler: CredentialExchangeIndyEventHandler,
RevocationRegistryEventHandler: RevocationRegistryEventHandler,
PresentationExchangeEventHandler: PresentationExchangeEventHandler,
CredentialRevocationEventHandler: CredentialRevocationEventHandler,
PingEventHandler: PingEventHandler,
OutOfBandEventHandler: OutOfBandEventHandler,
})
r.HandleFunc("/webhooks/topic/{topic}/", webhookHandler).Methods(http.MethodPost)
// and so on
}
func ConnectionsEventHandler(event acapy.ConnectionsEvent) {
fmt.Printf("\n -> Connection %q (%s), update to state %q rfc23 state %q\n", event.Alias, event.ConnectionID, event.State, event.RFC23State)
}
func BasicMessagesEventHandler(event acapy.BasicMessagesEvent) {
fmt.Printf("\n -> Received message on connection %s: %s\n", event.ConnectionID, event.Content)
}
func ProblemReportEventHandler(event acapy.ProblemReportEvent) {
fmt.Printf("\n -> Received problem report: %+v\n", event)
}
func CredentialExchangeEventHandler(event acapy.CredentialExchange) {
fmt.Printf("\n -> Credential Exchange update: %s - %s\n", event.CredentialExchangeID, event.State)
}
func CredentialExchangeV2EventHandler(event acapy.CredentialExchangeRecordV2) {
fmt.Printf("\n -> Credential Exchange V2 update: %s - %s\n", event.CredentialExchangeID, event.State)
}
func CredentialExchangeDIFEventHandler(event acapy.CredentialExchangeDIF) {
fmt.Printf("\n -> Credential Exchange DIF Event: %s - %s", event.CredentialExchangeID, event.State)
}
func CredentialExchangeIndyEventHandler(event acapy.CredentialExchangeIndy) {
fmt.Printf("\n -> Credential Exchange Indy Event: %s - %s", event.CredentialExchangeID, event.CredentialExchangeIndyID)
}
func RevocationRegistryEventHandler(event acapy.RevocationRegistry) {
fmt.Printf("\n -> Revocation Registry update: %s - %s\n", event.RevocationRegistryID, event.State)
}
func PresentationExchangeEventHandler(event acapy.PresentationExchange) {
fmt.Printf("\n -> Presentation Exchange update: %s - %s\n", event.PresentationExchangeID, event.State)
}
func CredentialRevocationEventHandler(event acapy.IssuerCredentialRevocationEvent) {
fmt.Printf("\n -> Issuer Credential Revocation: %s - %s - %s\n", event.CredentialExchangeID, event.RecordID, event.State)
}
func PingEventHandler(event acapy.PingEvent) {
fmt.Printf("\n -> Ping Event: %q state: %q responded: %t\n", event.ConnectionID, event.State, event.Responded)
}
func OutOfBandEventHandler(event acapy.OutOfBandEvent) {
fmt.Printf("\n -> Out of Band Event: %q state %q\n", event.InvitationID, event.State)
}
You are free to choose the URL for your webhooks. Don't forget to set the command-line parameter for ACA-py: --webhook-url http://localhost:{port}/webhooks
. The URL you provide to ACA-py is the base URL which will be extended with /topic/{topic}
by default. So whatever URL you choose, make sure that:
- if the
--webhook-url
ishttp://myhost:{port}/webhooks
- then the webhooks handler should listen on
http://myhost:{port}/webhooks/topic/{topic}
The acapy.WebhookHandler
is web framework agnostic and reads the topic from the URL by itself. The handler returned by acapy.WebhookHandler
has the standard handler signature func (w http.ResponseWriter, r *http.Request) {}
.
- godoc
- Proper error handling
- Admin API Key
- Tracing via global config
- Automation of steps via global config
- Payment decorators https://github.com/hyperledger/aries-rfcs/tree/master/features/0075-payment-decorators
- Constructors for JSON-LD types
- Add types for roles, states, predicates
- Allow for a connection-less credential exchange
- Allow for a connection-less proof by making a QR code of a payload below. The base64 payload is the result of your call to
/present-proof/create-request
.
{
"@id": "3b67c4bf-3953-4ace-94ef-28e0969288c5",
"@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/request-presentation",
"request_presentations~attach": [
{
"@id": "libindy-request-presentation-0",
"mime-type": "application/json",
"data": {
"base64": "eyJuYW1lIjoiQ29udGFjdCBBZGRyZXNzIiwidmVyc2lvbiI6IjEuMC4wIiwibm9uY2UiOiIzOTIyNTEwMjk1NjY5MzcxNDIxNTIzMDgiLCJyZXF1ZXN0ZWRfYXR0cmlidXRlcyI6eyJHaXZlbiBOYW1lcyI6eyJuYW1lIjoiZ2l2ZW5fbmFtZXMiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIkZhbWlseSBOYW1lIjp7Im5hbWUiOiJmYW1pbHlfbmFtZSIsInJlc3RyaWN0aW9ucyI6W3sic2NoZW1hX2lzc3Vlcl9kaWQiOiI4NTQ1OUd4ak55U0o4SHdUVFE0dnE3Iiwic2NoZW1hX25hbWUiOiJ2ZXJpZmllZF9wZXJzb24iLCJzY2hlbWFfdmVyc2lvbiI6IjEuNC4wIn0seyJzY2hlbWFfbmFtZSI6InVudmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIwLjEuMCIsImlzc3Vlcl9kaWQiOiI4WXE3RWhLQk11amgyNU5rTEdHYjJ0In1dfSwiRGF0ZSBvZiBCaXJ0aCI6eyJuYW1lIjoiYmlydGhkYXRlIiwicmVzdHJpY3Rpb25zIjpbeyJzY2hlbWFfaXNzdWVyX2RpZCI6Ijg1NDU5R3hqTnlTSjhId1RUUTR2cTciLCJzY2hlbWFfbmFtZSI6InZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMS40LjAifSx7InNjaGVtYV9uYW1lIjoidW52ZXJpZmllZF9wZXJzb24iLCJzY2hlbWFfdmVyc2lvbiI6IjAuMS4wIiwiaXNzdWVyX2RpZCI6IjhZcTdFaEtCTXVqaDI1TmtMR0diMnQifV19LCJTdHJlZXQgQWRkcmVzcyI6eyJuYW1lIjoic3RyZWV0X2FkZHJlc3MiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIlBvc3RhbCBDb2RlIjp7Im5hbWUiOiJwb3N0YWxfY29kZSIsInJlc3RyaWN0aW9ucyI6W3sic2NoZW1hX2lzc3Vlcl9kaWQiOiI4NTQ1OUd4ak55U0o4SHdUVFE0dnE3Iiwic2NoZW1hX25hbWUiOiJ2ZXJpZmllZF9wZXJzb24iLCJzY2hlbWFfdmVyc2lvbiI6IjEuNC4wIn0seyJzY2hlbWFfbmFtZSI6InVudmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIwLjEuMCIsImlzc3Vlcl9kaWQiOiI4WXE3RWhLQk11amgyNU5rTEdHYjJ0In1dfSwiQ2l0eSI6eyJuYW1lIjoibG9jYWxpdHkiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIlByb3ZpbmNlIjp7Im5hbWUiOiJyZWdpb24iLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX0sIkNvdW50cnkiOnsibmFtZSI6ImNvdW50cnkiLCJyZXN0cmljdGlvbnMiOlt7InNjaGVtYV9pc3N1ZXJfZGlkIjoiODU0NTlHeGpOeVNKOEh3VFRRNHZxNyIsInNjaGVtYV9uYW1lIjoidmVyaWZpZWRfcGVyc29uIiwic2NoZW1hX3ZlcnNpb24iOiIxLjQuMCJ9LHsic2NoZW1hX25hbWUiOiJ1bnZlcmlmaWVkX3BlcnNvbiIsInNjaGVtYV92ZXJzaW9uIjoiMC4xLjAiLCJpc3N1ZXJfZGlkIjoiOFlxN0VoS0JNdWpoMjVOa0xHR2IydCJ9XX19LCJyZXF1ZXN0ZWRfcHJlZGljYXRlcyI6e319"
}
}
],
"comment": null,
"~service": {
"recipientKeys": [
"F2fFPEXABoPKt8mYjNAavBwbsmQKYqNTcv3HKqBgqpLw"
],
"routingKeys": null,
"serviceEndpoint": "https://my-url.test.org"
}
}