Offer a credential to a wallet holder


The following guide will show you how to take a Web credential created directly using the Create credential endpoint and offer it to someone using a digital wallet to hold.

This method assumes you have a secure way to obtain the subject identifier (for example using DID Auth for a new interaction or by using a previously issued credential) and are able to authenticate the user separately from the issuance flow, for example logging into a session on a website/portal or in a physical setting.

Another example use case is re-issuing previously issued credentials, by using the information held about the user. This may start by first revoking the previous credential, then issuing a new credential before sending in a secure DID message to the wallet.
As these messages are sent directly to the wallet it does not require the user to go to a website or perform any action other than choosing to store the new credential.

Offering an issued credential to a wallet holder requires the following steps to be performed, which this guide will take you through:

  1. Create a credential using a subject DID from the user's wallet

  2. Construct a secure DID message & encrypt using the subject DID as the recipient

  3. Send using either secure DID messaging or make it available through QR code/deep-link.


  1. Access to the MATTR VII APIs. If you’re experiencing any difficulties, contact us.

  2. A postman collection (optional): We have created a curated collection of API requests dedicated to our platform, you can try it out by following this guide.

High-level steps

Overall, you will be using the following endpoints from our platform, they are:

  1. Create credential

  2. Encrypt message (To encrypt the credential you created)

  3. Send message (To send the credential you just encrypted to a wallet)

Step 1:

Copy to clipboard.
1POST https://YOUR_TENANT_URL/v2/credentials/web-semantic/sign

Creating a DEDICATED credential that is ONLY catering to a particular holder

You can create and encrypt any Web Credential using MATTR VII, but for it to be successfully claimed by a wallet the credential needs to have an embedded association/binding to the wallet when it is created. The way you can achieve this is by utilising the DID provided by the holder’s wallet, and ensuring it is represented as the subject DID of the credential.


In order to create a credential that can be successfully sent to a holder, you need to first complete the following steps:

  1. Obtain the wallet DID from the holder – Let's call it RECEIVER_WALLET_DID. It looks like did:key:placeholder

  2. Retrieve the DID Document associated with the Issuer DID - The response payload from the issuing party, let's call it ISSUER_DID_DOCUMENT

You can obtain the DID Document by retrieving the response body from any of the following endpoints:

  1. POST - {{ tenantUrl }}/v1/dids (Endpoint for creating a DID):

  2. GET - {{ tenantUrl }}/v1/dids (Endpoint for retrieving a list of DIDs)

The ISSUER_DID_DOCUMENT should look like the below:

Copy to clipboard.
2    "did": "",
3    "localMetadata": {
4        "keys": [
5            {
6                "kmsKeyId": "a0cba537-ffe1-486d-aedd-6ead80e75519",
7                "didDocumentKeyId": ""
8            },
9            {
10                "kmsKeyId": "250c4e1f-bae3-44ca-9f4e-4f7ff15851e2",
11                "didDocumentKeyId": ""
12            }
13        ],
14        "registered": 1674421454614,
15        "initialDidDocument": {
16            "id": "",
17            "@context": [
18                "",
19                "",
20                ""
21            ],
22            "verificationMethod": [
23                {
24                    "id": "",
25                    "type": "Ed25519VerificationKey2018",
26                    "controller": "",
27                    "publicKeyBase58": "2vcj3MjR4dSKq5asFQ9oor7iZsqTKTfBpjLHgaP15Y24"
28                }
29            ],
30            "keyAgreement": [
31                {
32                    "id": "",
33                    "type": "X25519KeyAgreementKey2019",
34                    "controller": "",
35                    "publicKeyBase58": "CU6dJt9p8twE4hmyGVFbVpUMmu6G732bVgD1tNupwYY7"
36                }
37            ],
38            "authentication": [
39                ""
40            ],
41            "assertionMethod": [
42                ""
43            ],
44            "capabilityDelegation": [
45                ""
46            ],
47            "capabilityInvocation": [
48                ""
49            ]
50        }
51    }

Guidelines to construct the request body: 

With these steps complete, you can then follow these guidelines to construct the request body for this endpoint:

  1. subjectId -> Use RECEIVER_WALLET_DID as value

  2. name -> General name for the credential

  3. description -> Brief description of the credential 

  4. claims -> Any JSON body 


Once you have issued your Credential, your credential field of the request body will have a payload that looks similar to this example:

Copy to clipboard.
2    "payload": {
3        "@context": [
4            ""
5        ],
6        "type": [
7            "CourseCredential"
8        ],
9        "issuer": {
10            "id": "",
11            "name": "tenant"
12        },
13        "expirationDate": "2024-02-07T06:44:28.952Z",
14        "credentialSubject": {
15            "id": "did:key:z6MkfxQU7dy8eKxyHpG267FV23agZQu9zmokd8BprepfHALi",
16            "givenName": "Chris",
17            "familyName": "Shin",
18            "educationalCredentialAwarded": "Certificate Name"
19        },
20        "proof": {
21            "type": "Ed25519Signature2018",
22            "created": "2021-07-26T01:05:06Z",
23            "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..o6hnrrWpArG8LQz2Ex_u66_BtuPdp3Hkz18nhNdNhJ7J1k_2lmCCwsNdmo-kNFirZdSIMzqO-V3wEjMDphVEAA",
24            "proofPurpose": "assertionMethod",
25            "verificationMethod": "did:key:z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj#z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj"
26        }
27    }

The is critical to ensure that the credential is subject-bound to the wallet (It's the same DID as RECEIVER_WALLET_DID). This same DID needs to be used to construct a secure DID message intended only for that recipient.

In short, has to be the same as RECEIVER_WALLET_DID

We'll elaborate on why this has to be the case later on in this tutorial.

Step 2:

Copy to clipboard.
1POST https://YOUR_TENANT_URL/v1/messaging/encrypt

Encrypting the credential you just created as a message

You need to construct the message payload in a particular way to be able to use the secure DID messaging capabilities.

The payload of the message consists of the parameters:

  1. senderDidUrl -> Must be a valid DID URL (that includes a #ref to a specific key in the Issuer's DID document) available on your tenant and a key that supports keyAgreement. You can find the keyAgreement in the DID document by inspecting ISSUER_DID_DOCUMENT.localMetadata.initialDidDocument.keyAgreement[0].id

  2. recipientDidUrls -> Array<string>, where you add RECEIVER_WALLET_DID into the array. The reason we do it is that it's important the holder of the credential is in control of this DID as once it is encrypted only that recipient is able to view the message.

  3.> Randomly generated UUID (V4) 

  4. payload.type - > Use "" as value. The wallet needs this value to determine how to respond to the message.

  5. payload.from -> ISSUER_DID_DOCUMENT.did


  7. payload.created_time -> Unix timestamp provided as a number (not a string).

  8. payload.body.credentials -> Array<Credential>, where you provide an array of one or multiple credentials that are all bound to the DID_RECEIVER_WALLET you want to send them to.

  9. payload.body.domain -> {{ tenant_subdomain }} where tenant_subdomain is the same subdomain you used to create ISSUER_DID_DOCUMENT. If you have set up a custom domain, use that here (without https://)

Construct the message

You need to construct the message in a certain way to be able to use the secure DID messaging capabilities.

Message template

This is what the payload section of your request body should look like:

Copy to clipboard.
2    "id": "{{ uuid }}",
3    "type": "",
4    "to": [
5        "{{ RECEIVER_WALLET_DID }}"
6    ],
7    "from": "{{ ISSUER_DID_DOCUMENT.did }}",
8    "created_time": 1624509675690,
9    "body": {
10        "credentials": [
11            {
12                { credential
13                }
14            }
15        ],
16        "domain": "{{ YOUR_TENANT_URL}}"
17    }

Now, let's encrypt the message

Once the message payload has been constructed, you can use it as the payload section within the request body for the endpoint.

Full payload template - Request body

Copy to clipboard.
2    "senderDidUrl": "{{ ISSUER_DID_DOCUMENT.localMetadata.initialDidDocument.keyAgreement[0].id }}",
3    "recipientDidUrls": [
4        "{{ RECEIVER_WALLET_DID }}"
5    ],
6    "payload": {
7        "id": "{{ uuid }}",
8        "type": "",
9        "from": "{{ ISSUER_DID_DOCUMENT.did }}",
10        "to": [
11            {
12                { RECEIVER_WALLET_DID
13                }
14            }
15        ],
16        "created_time": 1624509675690,
17        "body": {
18            "credentials": [
19                {
20                    { credential
21                    }
22                }
23            ],
24            "domain": "{{ YOUR_TENANT_URL }}"
25        }
26    }

The DID used in messaging may need to be different from the DID used to issue the credential. For example, BLS key DIDs used to issue ZKP-enabled credentials cannot be used for messaging because they have no key valid for keyAgreement.

Full example

Endpoint: POST - {{ tenantUrl }}/core/v1/messaging/encrypt

Request body:

Copy to clipboard.
2    "senderDidUrl": "",
3    "recipientDidUrls": [
4        "did:key:z6MkfxQU7dy8eKxyHpG267FV23agZQu9zmokd8BprepfHALi"
5    ],
6    "payload": {
7        "id": "c80cf529-1449-42b0-a972-ee975720859d",
8        "type": "",
9        "to": [
10            "did:key:z6MkfxQU7dy8eKxyHpG267FV23agZQu9zmokd8BprepfHALi"
11        ],
12        "from": "did:key:z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj",
13        "created_time": 1624509675690,
14        "body": {
15            "credentials": [
16                {
17                    "@context": [
18                        "",
19                        ""
20                    ],
21                    "type": [
22                        "VerifiableCredential",
23                        "CourseCredential"
24                    ],
25                    "issuer": {
26                        "id": "",
27                        "name": "tenant"
28                    },
29                    "issuanceDate": "2021-07-26T01:05:05.152Z",
30                    "credentialSubject": {
31                        "id": "did:key:z6MkfxQU7dy8eKxyHpG267FV23agZQu9zmokd8BprepfHALi",
32                        "givenName": "Chris",
33                        "familyName": "Shin",
34                        "educationalCredentialAwarded": "Certificate Name"
35                    },
36                    "proof": {
37                        "type": "Ed25519Signature2018",
38                        "created": "2021-07-26T01:05:06Z",
39                        "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..o6hnrrWpArG8LQz2Ex_u66_BtuPdp3Hkz18nhNdNhJ7J1k_2lmCCwsNdmo-kNFirZdSIMzqO-V3wEjMDphVEAA",
40                        "proofPurpose": "assertionMethod",
41                        "verificationMethod": "did:key:z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj#z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj"
42                    }
43                }
44            ],
45            "domain": "YOUR_TENANT_URL"
46        }
47    }


The response body contains the JWE. Let's call this whole response payload MESSAGE_ENCRYPTED, as we'll be using it in the next section.

Copy to clipboard.
2    "jwe": {
3        "protected": "eyJhbGciOiJYQzIwUCJ9",
4        "recipients": [
5            {
6                "header": {
7                    "alg": "ECDH-1PU+A256KW",
8                    "kid": "did:key:z6MkfxQU7dy8eKxyHpG267FV23agZQu9zmokd8BprepfHALi#z6LSoYqvKWzd8faMroS4WMHRfzeDR22w5nrcGEi9MRV4BEYA",
9                    "epk": {
10                        "kty": "OKP",
11                        "crv": "x25519",
12                        "x": "ovKlBgAF969Mpa6XYhV6imLcX4ZyVQQTpU3FkjFKk2Y"
13                    },
14                    "skid": "did:key:z6MksHbxLQoQvsPRezXsJJiKXuaV9frAiuwKfbuHHTRn53jx#z6LSkHGWvAejiTJtKte98QAJmeSDaMtJMoupTba471nZRQhc"
15                },
16                "encrypted_key": "pZwsbPa7Vfq6KrKKLEg1jOFFkBRufsTOjrEZX6fwnu6rpQt8G_O42Q"
17            }
18        ],
19        "ciphertext": "wOiJL0zmZSaSdAk3Wn5m_XzeyiVvpJXRX3FTy0ivr3D3DTibge2I7m6DJ3kaDmXi17sy2cL0r3lsddxBcXEPDfrL8o6y5oIyodcQAo4tMY4IOXdsFHN4cTWjOyrsZhT-1GGb0QYyQ7LgCE7WgYdMX-fBetr8fhVxAoVeyYkBxRhXhF47elWlNqoLT7dfsUVYCPBjY0GN0ciQOzBvcplB8hrqVWaTvdbpgoPIGGKxcXl907gnIAX8rzFcRfh66t6M2SlGZ5pCeDvlne-StPxvIxvGJaQq02tWuA2Yykz5Gw6zz5xmPSrj7yyy26ABSM4yjQcu2q-payWQx1lkGaLrPpsbhKzq5KcXNlviz6r3aw3ERt4OO-NxmBu4ZCeK1Uvfo_wXwTawOpdjF6RB7RRjO5TJ1fGEWjpl1p84T0e-n6CE_Kxibklh4bucmx55F1rgQc1280C0k4DNJlplhoNGlFyOfaYBraT-vOJ0Fv-hKpv41npGf_uCr56Cjb4pKvMEngpAA2dglfMO0NBN5hf_FdoC6g17h4PWxcBnuCDRQcDfHvopCuCfU2H4saL07R-YRcokis2tBii7FZKS7F-eQozHzgYl68ZI3Cd5eo-4VUp3e1Xmd-b53mF3bRutV9JcY7KA1AQnwm2yyTFz0ss7a21KsYZHUi-eIhbaEf88BiMrblvp4ztDPuXUmwG4RowoRd5ZSJsdOHrkm2fniyISLGaPgcSeot22_HHsXf8bqhyxNbr6e4ghTuVZgTBBpv15DT2KSj3z3_2TgeD6VpIFwJQm1Dn_hZnSpFx-h57nsEyAAW5C9XoVJ5usnzn7TQJtZM6wsFFGd1Bgs8Xmf0p79J-QXkAWmhDi6mct5unsEnr52hnzGyLfsoH8YUjffkI55U86JZKrcMycV92IN6jF4cMoe8FbfVyu4pNrh4vKIkgVqJO0B50z0OIk8WEYIV1HoWxIzXiH6VLiC5QEZaCFyVUOnr4PFsNICLEedYwE6w0XmR2fpeMz529RPamJPYlBb9dZMGJ2RL-RnJYIH8BbhxN08EFS-4RXl27PjRoam2W6fR3fbyrObfh4H6JwXXi4ATelcBucE4zZUMrjsXMbW4CrduIis0c0f8eOPeGV4uW4W9lg4-DbsUOYY1frBSv6_1krmEvQQLgu087KfLgWMz7wqNO7UuF96ECIi7Z1yPCwx4vmtKOJN5lbFXZkzTCtV4-bltNr9PzLBJF4krqNJkwEgKe3kzrAnABKJQx1aDATk8gUJUu14635hduGBWPrY3b_isVr9tzflkCMFXq5SXV24YYCAjQsTXIRbJyV5756NwiT2L7FqzgzLmd3X6hZ5LCjv9KJwDEVCWTN9v2Zbmi8WFwrDz5LeokLGA4_Km48aJYMGpxPmL0wkEXynkIX0IhJrEu9uxEKHzEia_WjDIw80VwghZpXGVw3jYDs5R7O-zhv2lcR3UXJr_XMroe7jAV5pqWop_-ek1r7Qpt-rudjS3q_zC-uuG0SkXsL47Ni92e3MPyeDjukWAamMx7HqTx_azNL9JeeZ-w_8qd6x4wKo6qB7R0-WDFsOXHYM8HT5Aw6sKX48Dl0VfZaBE4JBXTVWTs4C9n4p3gu11bRB-tj01c3usw0vx3N-EIs5Y6cXcx3UN0O2ykWF3jCpEecUNCIzNKFTDrjALZXFvzlrdeyW5QO6sGdSVIsAZ4MDlTLRen0rzJkmw0cDmvN0OG6TvVgBoIv-Kc35g-4I8FQuUW-pIS9gQONYmCgXkQfxzjJdrcMCtQ1sV9kcg5CCirQ3KYH-5c3GhriDrNZIGJX3XkERFR9CqPFU6RSocReOr0iAx8LtssuiE1X2OeywQaAop8DJ1aQ3xaQwJQGCWSzKu_49ee6RhqSpq0EyPILxwqoHhP38NKlw5F5KGdWo6FqEzIcFVOOO160mv_yeYnIOIUnQEOGxcWSKVCiJmMcXadxtEWk2jqItbJiDxoOWs4d34eaWebVyoNOhcaim7UZQq0tE-GpFi8SZZmOmnIxL0lgZYYkvWH5WVY-n7Be6c9KoMYvQVFQ69EndcqS0zLV6unV0aZ-3CTXU1CvfSKZ1obsEozE1elYZawKiAUXQEoe2hhSmd63b3QSbduLDPs=",
20        "iv": "QKTq99Y9NjnWIDvU1XldP23j5eqXamrf",
21        "tag": "TXfEcE1wFMOOjE0GCCI7BA=="
22    }

Step 3: Send the message

Once you have an encrypted message containing the credential in a format that a wallet can understand, you need to make it available to the holder.

This can be achieved in different ways;

  • API: Secure DID messaging to the wallet, with push notification

  • QR code: Host the message and reference in a QR code or deeplink


Sending the encrypted credential to the wallet as a message

By using the MATTR VII platform messaging endpoint, you are able to send messages to recipients and if they have registered their MATTR mobile wallet to accept messages using an inbox, they will receive a push notification or be alerted the next time they open the app.

Specify the RECEIVER_WALLET_DID as the to value and use MESSAGE_ENCRYPTED.jwe body of the encrypted message as the message body to send using the endpoint for sending messages

Copy to clipboard.
1POST https://YOUR_TENANT_URL/v1/messaging/send

Request body:

Copy to clipboard.
2  "to": "{{ RECEIVER_WALLET_DID }}",
3  "message": {{ MESSAGE_ENCRYPTED.jwe }}


200 response indicates that the message payload has been sent to the service endpoint of the dereferenced DID Document (or the default MATTR service endpoint).

QR code:

The MATTR mobile wallet will follow valid 302 redirects to online resources, this allows you to make the secure message available to wallet holders and construct a URL that can be included in a QR code or deep link.

To host the message you would need to first base64url encode the jwe to be URL safe.

The jwe you'll be using can be extracted from MESSAGE_ENCRYPTED.jwe

Copy to clipboard.

Then create a redirect to this hosted message.

Copy to clipboard.
1Redirect                 Content

QR code

For a QR code you would need to construct a URL to be;

Copy to clipboard.

Using an online service the QR code would look like this

For a deep-link you would need to perform another base64url encoding of the didcomm:// URL;

Copy to clipboard.
1echo -n 'didcomm://' | base64url

Then construct the URL to include the bundle id of the wallet;

Copy to clipboard.

By following this URL whilst on your device, it will open the MATTR mobile wallet, follow the redirect you have configured and navigate to the Credential Offer screen in the wallet, where you can store the credential.

Try it out

Make sure you have the MATTR Wallet app installed and have accepted the notification request during the onboarding steps.

  1. First, obtain a DID from the Mobile Wallet either a using DID Auth or by copying the 'Public DID' available in the Settings menu

  2. Create a Credential and construct the message payload

  3. Encrypt the payload

  4. Send the encrypted payload or construct a QR code/deep-link redirect URL

On your mobile device, you should see a notification message appear. Tap on the message and authenticate using biometrics or PIN. The app should navigate you to the Credential offer screen where you can view the credential you have issued.

As long as the domain checks are valid and the credential isn't a duplicate then you will be able to Store the credential in your wallet.

The MATTR mobile wallet will check for duplicate credentials, these are credentials that contain the exact same proofs.

Viewing the credential is as simple as tapping on the Credential card. Each time the credential is opened a validity check is performed.