# Platform Overview

Welcome to our developer docs

## We are here to help you build smarter, safer products

The Sudo Platform is a powerful collection of identity-centric SDKs that can be seamlessly integrated into your products to help your users regain control over their data, their identity, and their privacy.

## Why use the Sudo Platform?

‌We believe that everyone should have control over their personal data. They should decide what details they share, with whom, and when. The Sudo Platform makes it easy to empower your users, giving them back control over their personal information.

‌Consumers are exposed to a growing and increasingly sophisticated collection of privacy and cyber-safety vulnerabilities, putting their personal identity and data at great risk. There has also been a power-shift, with consumers trusting a small number of tech giants to be stewards over their identities and personal data.

‌Many organizations have realized that protecting user data is not just a matter of breach prevention. There is now an increasing obligation to build apps and products that place privacy at their heart.

## The Sudo Digital Identity

To empower users to better protect their personal identity, we’ve created the Sudo Digital Identity, also simply called a Sudo. A Sudo is a digital identity that lets your users interact with other people and organizations while protecting their personal data.

Most Sudo Platform features are built around the Sudo. Sudos lie at the very heart of the Sudo Platform. Your users can create multiple Sudos by leveraging our Sudo Platform products, protecting them from inadvertently disclosing their personal details.

People are already used to the idea that they shouldn’t mix their work and personal identities. Sudos take this approach one step further, allowing users to create a different identity for every activity they do. We refer to this as compartmentalization, and it’s a great way to improve safety and security online.

Learn more about [Sudo Digital Identities](/concepts/sudo-digital-identities) on the Sudo Platform.

## Built for Decentralized Identity

The identity landscape is rapidly changing. Emerging Decentralized Identity standards are starting to provide powerful tools for users to take control over their identities.

Sudos are a bridge between the traditional and Decentralized Identity worlds. Sudos are designed to operate with traditional services while being fully interoperable with Decentralized Identity. Sudos are designed to fully support key elements of Decentralized Identity, such as DIDs, verifiable credentials, decentralized key management, passwordless login and peer-to-peer communications.

In addition, as Decentralized Identity technologies mature, the Sudo Platform will continue to provide greater integration with Decentralized Identity, through technologies such as cloud agents, secure storage hubs, peer-to-peer messaging, etc.

As a [Sovrin Steward](https://sovrin.org/stewards/) and member of the [Decentralized Identity Foundation (DIF)](https://identity.foundation), [Anonyome Labs](https://anonyome.com) is strongly committed to integrating Decentralized Identity standards and protocols into the Sudo Platform and making Sudo Digital Identities fully compatible with Decentralized Identity.

Learn more about [Decentralized Identities](/guides/decentralized-identity) on the Sudo Platform.

## How to Get Started

For the best introduction to the Sudo Platform, check out our [Getting Started](/guides/getting-started) guide, which will get you up and running in just a few simple steps.

Interested in learning how Sudo Platform would best integrate into your existing product? [Request a demo](https://sudoplatform.com/#lets-chat) with one of our solution architects.


# Getting Started

Build privacy-centric apps with the power of the Sudo Platform

There are three simple steps to get started building Sudo Platform-enabled apps.

1. [Request a Sudo Platform account](#step-1-request-a-sudo-platform-account)
2. [Download the SDK configuration file](#step-2-download-the-sdk-configuration-file)
3. [Integrate the SDKs into your application](#step-3-integrate-the-sdks-into-your-application)

## Step 1: Request a Sudo Platform account

Before you can develop on the Sudo Platform, you need an account. This account will provide you access to the Sudo Platform services.

If you do not currently have a Sudo Platform account, [send us a request](https://sudoplatform.com/#lets-chat) and we will create one for you.

## Step 2: Download the SDK configuration file

Every application needs an SDK configuration file before you can integrate it with the Sudo Platform. Each Sudo Platform project has a unique SDK configuration file which the SDKs load at run-time. This configuration tells the SDKs which API endpoints to use. Think of it as your project's API key to access the Sudo Platform services.

To download your SDK configuration file:

1. Log into the Sudo Platform **Admin Console**. A link and temporary login was emailed to you when your account was activated.
2. Go to **Project Settings** and look for the SDK Config section.
3. Click on **Download** to save the file to your computer.

![](/files/-M5bHxkS3Ye8tJNi5MUm)

{% hint style="info" %}
**Admin Console Access.** Check your inbox for the *"Welcome to the Sudo Platform"* email to find the correct Admin Console URL for your account. [Report an issue](https://sudoplatform.com/#lets-chat) if you are having trouble accessing the Admin Console.
{% endhint %}

## Step 3: Integrate the SDKs into your application

The quickest way to explore the capabilities of the Sudo Platform is by starting with one of our sample apps. Option #1 below explains what you need to do. These sample apps are designed to highlight the functionality of the Sudo Platform in an application. The only change you need to make is adding your configuration file, as described above.

Alternatively, you can try integrating the SDKs into an existing app or create a new app that incorporates the Sudo Platform SDKs.

### Option #1: Start with a Sample App

Want to see the Sudo Platform in action? The quickest way is to clone one of our [iOS sample apps](https://github.com/sudoplatform/samples-ios). Then just follow the README to start exploring how the Sudo Platform adds privacy-centric competitive advantages to your products.

{% hint style="info" %}
**Are there SDKs for web?** Web support is coming soon! We are currently building web SDKs to compliment our mobile SDKs. [Let us know](https://sudoplatform.com/#lets-chat) if you would be interested in being an early beta tester of the web SDKs.
{% endhint %}

Once you have a feel for how a Sudo Platform-enabled application is structured, you can integrate the SDKs into your own application. Read on to learn how to integrate Sudo Platform capabilities into your existing applications. Or jump to Option #3 to see how to build a new application with the Sudo Platform SDKs.

### Option #2: Integrate into an Existing App

Do you already have a product that you want to fortify with privacy and cyber safety features? We designed the Sudo Platform from the ground up to integrate seamlessly into existing applications.

For iOS projects, if you have not previously setup Cocoapods in your project, you will need to in order to integrate the Sudo Platform SDKs into your project. See below for detailed instructions.

For Android projects, the Sudo Platform SDKs are installed via Gradle. No additional setup is needed.

### Option #3: Integrate into a New App

Do you have an idea for a privacy-first application? The Sudo Platform can save you months of development time by leveraging its powerful collection of privacy and cyber safety capabilities.

For iOS projects, you will need to setup Cocoapods in your project to integrate the Sudo Platform SDKs. See below for detailed instructions.

For Android projects, the Sudo Platform SDKs are installed via Gradle. No additional setup is needed.

### Install the SDK Configuration File

As we explained above, the first step in any project is to install the SDK Configuration File into your application. You must do this before you can start using the Sudo Platform.

On iOS, the SDK configuration file should be added to the project in the main app folder. For example, if your application is called MyPrivacyApp then you would add it to the project in Xcode under the `MyPrivacyApp` folder. This will make it automatically discoverable by the SudoUser SDK.

On Android, the SDK configuration file should be added to the assets folder of the app module.

### Verifying the App Compatibility with Backend Services

There's an API in Sudo Config Manager SDK that can help you determine your app's compatibility with the current set of deployed backend services. You should invoke this API on launch of the app and decide whether or not you want to inform the user to upgrade their app based on the information returned.

{% tabs %}
{% tab title="Swift" %}

```swift
import SudoConfigManager

let configManager = DefaultSudoConfigManager()

do {
    try configManager?.validateConfig { (result) in
        switch result {
        case let .failure(cause):
            switch cause {
            case SudoConfigManagerError.compatibilityIssueFound(let incompatible, let deprecated):
                // Inform the user to update the app or allow a specific
                // subset of features to be used based on `incompatible`
                // list. `deprecated` list can be use to warn the user
                // that they will need to update app in future.
            default:
                // Handle other errors.
            }
        case .success:
            break
        }
    }
} catch {
    // Handle error.
} 
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
import com.sudoplatform.sudoconfigmanager.DefaultSudoConfigManager

val configManager = DefaultSudoConfigManager(appContext)
val result = configManager.validateConfig()
// Inform the user to update the app or allow a specific
// subset of features to be used based on `incompatible`
// list. `deprecated` list can be use to warn the user
// that they will need to update app in future.
val incompatible = result.incompatible
val deprecated = result.deprecated
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { DefaultConfigurationManager } from '@sudoplatform/sudo-common'

const configManager = DefaultConfigurationManager.getInstance().setConfig(
  JSON.stringify(config),
)
const result = await configManager.validateConfig()
// Inform the user to update the app or allow a specific
// subset of features to be used based on `incompatible`
// list. `deprecated` list can be use to warn the user
// that they will need to update app in future.
const incompatible = result.incompatible
const deprecated = result.deprecated
```

{% endtab %}
{% endtabs %}

### What next?

By now you hopefully have explored how the Sudo Platform works using our sample apps. You have also installed the SDK Configuration File into your own project. Now you are ready to explore all the tools and services that Sudo Platform provides you. In the next section, we look in detail at the SDKs. If you want to learn more about Sudos and how Sudo Platform actually works, jump to the Concepts section.


# Users

The glue between a user and their Sudo identities

The **User SDK** makes it seamless for you to integrate Sudo capabilities into your applications with minimal effort. For applications that support Open ID Connect, the **User SDK** enables applications to map their existing users to Sudo Platform users without the need for end users to create new accounts.

## Key Features

|                            |                                                                                                                                                                                                          |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Private Key Authentication | The **User SDK** allows your users to on-board with minimal effort and sharing of personal information. Private Key Authentication offers a modern, passwordless approach for signing up and logging in. |
| Federated Authentication   | The **User SDK** offers multiple ways to register a user, including Federated Sign In (via OIDC).                                                                                                        |

## Next Steps

* Integrate the **User SDK** into your [JavaScript](/guides/users/integrate-the-user-sdk#integrate-the-js-sdk), [iOS](/guides/users/integrate-the-user-sdk#integrate-the-ios-sdk) and [Android](/guides/users/integrate-the-user-sdk#integrate-the-android-sdk) applications
* Learn more about [Sudo digital identities](/concepts/sudo-digital-identities) and other core concepts


# Integrate the User SDK

Integrate the User SDK into your application

## Prerequisites

1. [Complete the Getting Started Guide](/guides/getting-started)

## Get Started

* [Integrate the iOS SDK](#integrate-the-ios-sdk)
* [Integrate the Android SDK](#integrate-the-android-sdk)
* [Integrate the JS SDK](#integrate-the-js-sdk)

## Integrate the iOS SDK

To use User SDK in an iOS app, you need to install the SudoUser package via Swift Package Manager

Open your project settings in XCode, and go to the `Package Dependencies` tab. Click on the `+` sign to add a dependency.&#x20;

Enter the repository URL [`https://github.com/sudoplatform/sudo-user-ios`](https://github.com/sudoplatform/sudo-user-ios) in the top right search box and select the `sudo-user-ios` repository.&#x20;

Select the required version and `Add Package`.&#x20;

Sudo Platform SDKs conform to semantic versioning so in most cases you will leave the Dependency Rule as `Up to Next Major Version` to receive regular updates without introducing any breaking changes.

This will resolve the local package dependency and install the latest version of the User SDK.

### SDK Configuration

Before starting to use `SudoUserClient` you must configure it using the configuration information provided to you for your instance of the Sudo Platform. [Download](/guides/getting-started#step-2-download-the-sdk-configuration-file) and [install](/guides/getting-started#install-the-sdk-configuration-file) your project's SDK configuration file before proceeding.

> Instantiating and initializing a client:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let client: SudoUserClient = try DefaultSudoUserClient(keyNamespace: "<your-application-name>")
catch let error {
    // Handle initialization error. An error might be thrown due to invalid
    // or missing confiugration file.
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

## Integrate the Android SDK

To use User SDK in an Android app you need to add `SudoUser` library as a dependency to your project.

> Edit the app module Gradle script (build.gradle) and add SudoUser library as a dependency.

{% tabs %}
{% tab title="Gradle" %}

```
dependencies {
    api 'com.sudoplatform:sudouser:$latest_version'
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The latest version of the SDK can be found at [SDK Releases](/guides/users/changlog).
{% endhint %}

> Edit AndroidManifest.xml to add the following app permissions:

{% tabs %}
{% tab title="Diff" %}

```diff
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+   xmlns:tools="http://schemas.android.com/tools"
    package="com.sudoplatform.sampleapp">

+   <uses-permission android:name="android.permission.INTERNET" />
+   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+   <uses-permission android:name="android.permission.WAKE_LOCK" />
+   <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

+   <queries>
+       <intent>
+           <action android:name="android.intent.action.VIEW" />
+           <data android:scheme="https" />
+       </intent>
+       <intent>
+           <action android:name=
+               "android.support.customtabs.action.CustomTabsService" />
+       </intent>
+   </queries>

    <application
+       tools:replace="android:allowBackup"
+       android:allowBackup="false"
```

{% endtab %}
{% endtabs %}

The Sudo Platform enforces that the client passes Google Play Integrity validation in order to successfully register a new user. A part of this validation is checking the attestation result is produced by an app/module that's signed by a known signing certificate. In order to pass this validation your signing certificate's fingerprint must be uploaded via the admin console.

To obtain the signing certificate's fingerprint, run the following command:

{% tabs %}
{% tab title="shell" %}

```
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep SHA256 | tr -d : | awk '{print($2)}' | xxd -r -p | base64
```

{% endtab %}
{% endtabs %}

The output of the above command must be uploaded to the admin console (Settings -> Android Signing Certificate).

### SDK Configuration

Before starting to use `SudoUserClient` you must configure it using the configuration information provided to you for your instance of the Sudo Platform. [Download](/guides/getting-started#step-2-download-the-sdk-configuration-file) and [install](/guides/getting-started#install-the-sdk-configuration-file) your project's SDK configuration file before proceeding.

> Instantiating and initializing a client:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val client = SudoUserClient.builder(appContext)
    .setNamespace("<your-application-name>")
    .build()
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

## Integrate the JS SDK

To use the **Sudo User SDK** in a Web or Node.js project, you must add `@sudoplatform/sudo-user` as a dependency to your project.

```bash
yarn add '@sudoplatform/sudo-user'
# or
npm install --save '@sudoplatform/sudo-user'
```

### SDK Configuration

Before starting to use `SudoUserClient` you must configure it using the configuration information provided to you for your instance of the Sudo Platform. [Download](/guides/getting-started#step-2-download-the-sdk-configuration-file) and [install](/guides/getting-started#install-the-sdk-configuration-file) your project's SDK configuration file before proceeding.

> Instantiating and initializing a client:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
import { DefaultConfigurationManager } from '@sudoplatform/sudo-common'

// Set configuration (content of file downloaded in previous step) in the configuration manager
DefaultConfigurationManager.getInstance().setConfig(configJson)

// Instantiate and initialize the client
const client = new DefaultSudoUserClient()
```

{% endtab %}
{% endtabs %}


# Registration

In order to access various services in Sudo Platform, you must first register a new user and sign in using the User SDK.

## OIDC Federated Sign In

If you are using an external identity provider that supports [OpenID Connect (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) standard with Sudo Platform then an explicit user registration is not required. An existing user of your identity provider can perform a federated sign in to start using various services provided by Sudo Platform. For more details, please refer to [Authentication](/guides/users/authentication) section.

## Custom Federated Sign In

The OIDC standard based Federated Sign In is the preferred method to integrate your external identity provider with Sudo Platform however, if you prefer a programmatic method to user sign in, this can be achieved by implementing a Custom Federated Sign In by providing a a signed JWT to the User SDK.

To register a new user based on the custom federated sign in:

1. Generate a RSA key pair of size 2048 bit. For example, you can use the following OpenSSL commands to generated the required key pair.

   ```
   openssl genrsa -out key.pem 2048
   openssl rsa -in key.pem -outform PEM -pubout -out public.pem -RSAPublicKey_out
   ```
2. Send your public key, issuer name and key ID to your account representative or to <support@sudoplatform.com>. The issuer name and key ID is used to uniquely identify the public key that we will use to validate the authentication token generated by you.
3. Implement `AuthenticationProvider` and `AuthenticationInfo` interface defined in the User SDK to issue a signed JWT in the following format:

   ```
   // Header
   {
     "alg": "RS256",
     "kid": "<key_id>"
   }

   // Payload
   {
     "aud": "identity-service",
     "sub": "<user_id>",
     "jti": "98BCA65D-CAA1-4E91-A23F-FAB819C7C53D",
     "iss": "<issuer_name>",
     "iat": 1608102082,
     "exp": 1608105682 
   }
   // Signature.
   ```

   * The digital signature algorithm used to sign the JWT must be RSA with SHA-256 ("RS256").
   * The key ID ("kid") must uniquely match the key ID that you provided in the previous step.
   * The audience ("aud") must always be "identity-service".
   * The subject (“sub”) denotes the unique identifier of the identity being federated. The username of the newly created Sudo Platform user will be set to “sub” contained in the token.
   * The token ID ("jti") should be a unique ID such as UUID.
   * The issuer (“iss”) denotes the issuer of the token, i.e. the external identity provider. This must uniquely match the issuer name that you provided in the previous step.
   * The issued at time ("iss") is milliseconds since epoch representation of when the token was issued.
   * The expiry ("exp") is milliseconds since epoch representation of when the token should expire. This time should be short but long enough to allow for the time it will take for the client to submit the token to Sudo Platform.
   * Optionally any number of custom attributes can be added and if you need those attributes to be persisted in the user record then please contact your account representative or send an email to <support@sudoplatform.com>.

   The string encoded JWT should be returned by `toString()` (iOS) or `encode()` (Android and TypeScript) method of `AuthenticationInfo` with `type` set to `FSSO`. For more details, please refer to [API Reference](/guides/users/api-reference) and `LocalAuthenticationProvider` in our GitHub project.

   * iOS example: [LocalAuthenticationProvider](https://github.com/sudoplatform/sudo-user-ios/blob/master/SudoUser/LocalAuthenticationProvider.swift)
   * Android example: [LocalAuthenticationProvider](https://github.com/sudoplatform/sudo-user-android/blob/master/sudouser/src/main/java/com/sudoplatform/sudouser/LocalAuthenticationProvider.kt)
   * TypeScript example: [LocalAuthenticationProvider](https://github.com/sudoplatform/sudo-user-js/blob/main/src/user/auth-provider.ts)
4. Invoke `registerWithAuthenticationProvider` with your `AuthenticationProvider` implementation as an input.

## Sign in Key Registration

Sign in key registration is used to create a new user if you don't have users in a registry that supports federated sign in and you want Sudo Platform to manage your users. It uses Apple’s DeviceCheck token or Google Play Integrity token to ensure the registration request is coming from a legitimate device.

{% hint style="info" %}
During the registration, the client creates a private/public key pair and the public key is stored in the Sudo Platform. The private key is used to digitally sign a token during subsequent sign in and the token is validated using the previously registered public key.
{% endhint %}

> On iOS, to obtain a DeviceCheck token:

{% tabs %}
{% tab title="Swift" %}

```swift
// Obtain a DeviceCheck token from iOS via AppDelegate interface.
import DeviceCheck

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  let currentDevice = DCDevice.current
  if currentDevice.isSupported {
    currentDevice.generateToken(completionHandler: { (data, error) in
      if let error = error {
          // Handle error.
      }

      let deviceCheckToken = data
    })
  }
  return true
}
```

{% endtab %}
{% endtabs %}

> Complete the iOS registration process by calling the register API:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let uid = try await client.registerWithDeviceCheck(
        token: deviceCheckToken,
        buildType: "release",
        vendorId: UIDevice.current.identifierForVendor,
        registrationId: UUID().uuidString
    )
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}
{% endtabs %}

> On Android, to obtain a Google Play Integrity token:

{% tabs %}
{% tab title="Kotlin" %}

<pre class="language-swift"><code class="lang-swift"><strong>val integrityTokenProvider: StandardIntegrityTokenProvider = suspendCoroutine { cont ->
</strong>            standardIntegrityManager.prepareIntegrityToken(
                PrepareIntegrityTokenRequest.builder()
                    .setCloudProjectNumber(cloudProjectNumber)
                    .build()
            )
                .addOnSuccessListener { tokenProvider ->
                    run {
                        cont.resume(tokenProvider)
                    }
                }
                .addOnFailureListener { exception -> cont.resumeWithException(exception) }
        }

<strong>val deviceId = Settings.Secure.getString(
</strong>    appContext.contentResolver,
    Settings.Secure.ANDROID_ID
)

val token: StandardIntegrityToken =
    suspendCoroutine { cont ->
        integrityTokenProvider.request(
            StandardIntegrityTokenRequest.builder()
                .setRequestHash(deviceId)
                .build()
        )
            .addOnSuccessListener { token ->
                run {
                    cont.resume(token)
                }
            }
            .addOnFailureListener { exception -> cont.resumeWithException(Exception("$exception")) }
    }
</code></pre>

{% endtab %}
{% endtabs %}

> Complete the Android registration process by calling the register API:

{% tabs %}
{% tab title="Kotlin" %}

```swift
CoroutineScope(Dispatchers.IO).launch {
    try {
        client.registerWithGooglePlayIntegrity(
            "<app_package_name>",
            deviceId,
            token.token(),
            UUID.randomUUID().toString()
        )
        // Registration was successful.
    } catch (e: RegisterException) {
        // Handle error. A failure result may be returned if the backend is unable
        // to perform the registration due to availability or security issues.
    }
}
```

{% endtab %}
{% endtabs %}

If you have enabled signing certificate validation for Android registrations then you must also complete the following steps:

1. In Google Play Console, navigate to Release -> Setup -> App signing and copy "SHA-256 certificate fingerprint" of your app.
2. In Sudo Platform Admin Console, navigate to Overview -> Settings and click on "Add" button to add the signing certificate fingerprint that you copied in the previous step.

{% hint style="warning" %}
The signing certificate fingerprint is owned by the admin user who created it. If the owner is deleted then the fingerprint will also be deleted for security reasons. If the owner is being deleted for off-boarding then make sure a new signing certificate's fingerprint is created by another admin user and re-signed app released before deleting the original owner.&#x20;
{% endhint %}

## Test Registration Keys

To bypass various security and entitlement restrictions for running integration tests, you can use test registration keys. Use test registration keys when other registration techniques are unavailable, or when writing integration tests.

{% hint style="info" %}
A test registration key allows you to register a device multiple times. Additionally, you can manipulate the registered user's entitlements so that more resources can be provisioned than what is allowed by default.
{% endhint %}

### Generate a Test Registration Key

Test registration keys can be generated and downloaded in the Admin Console. Keys are specific to the logged-in Admin Console user and the project (e.g. dev, qa, prod). A generated key can only be downloaded once. If you cannot locate a previously generated key, you will need to revoke they key and generate a new one.

To download a test registration key:

1. Log into the Sudo Platform **Admin Console**. A link and temporary login was emailed to you when your account was activated.
2. Go to **Project Settings** and locate the *Test Registration Keys* section.
3. Click on **Generate Test Registration Key** and follow the prompt to download the newly generated key.

![](/files/-M9yToXhnL_RolVRtP34)

{% hint style="warning" %}
A PEM encoded test registration key can be passed to the test via a number of ways, e.g. via environment variable, temporary file etc. Regardless of the mechanism chosen, keep the key data secure and do not include it in your app build published to end users.
{% endhint %}

We don't prescribe the method in which to pass the test registration key and key ID as the approach will likely vary depending on whether you are writing integration tests vs setting up CI, etc.

{% hint style="success" %}
For an example approach, please take a look at the [open source iOS sample app](https://github.com/sudoplatform/samples-ios) and the [open source Android sample app](https://github.com/sudoplatform/samples-android) on Github.
{% endhint %}

Initialize a TEST authentication provider.

{% tabs %}
{% tab title="Swift" %}

```swift
// "testKey" is PEM encoded TEST key obtained from the previous step. "name" parameter should be unique for each test suite.
// "KeyManager" instance should use a unique namesapce as well so resetting it does not cause other keys to be removed.
do {
    let testAuthenticationProvider = try TESTAuthenticationProvider(
        name: "mytest",
        key: testKey,
        keyId: "Key ID of your TEST key from the admin console.",
        keyMananger: SudoKeyManagerImpl(
            serviceName: "com.sudoplatform.appservicename",
            keyTag: "com.sudoplatform",
            namespace: "mytest"
        )
    )
} catch {
    // Handle error. An error might be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are 
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// "name" parameter should be unique for each test suite.
// "privateKey" and "keyId" are PEM encoded TEST registration key and key ID obtained from 
// the previous step.
val keyManager = KeyManagerFactory(appContext).createAndroidKeyManager()
// Note: The public key is optional as it can be derived from the private key. However, on
// Android API 23 this is not possible due to limitations in Java security provider so 
// when running tests with Android API 23 emulator generate the public key using
// "openssl rsa -in <private_key> -outform PEM -RSAPublicKey_out" and pass the resulting
// public key as publicKey parameter.
val testAuthenticationProvider = TESTAuthenticationProvider("mytest", privateKey, null, keyManager, keyId)
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
// "name" parameter should be unique for each test suite.
// "privateKey" and "keyId" are PEM encoded TEST registration key and key ID obtained from 
// the previous step.
const testAuthenticationProvider = new TESTAuthenticationProvider(
  'mytest',
  privateKey,
  keyId,
)
```

{% endtab %}
{% endtabs %}

> Registering with TEST authentication provider.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let uid = try await client.registerWithAuthenticationProvider(authenticationProvider: auttestAuthenticationProviderhProvider, registrationId: UUID().uuidString)
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might be also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        client.registerWithAuthenticationProvider(
            testAuthenticationProvider,
            UUID.randomUUID().toString()
        )
        // Registration was successful.
    } catch (e: RegisterException) {
        // Handle error. A failure result may be returned if the backend is unable
        // to perform the registration due to availability or security issues.
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  await client.registerWithAuthenticationProvider(
    testAuthenticationProvider,
    uuid.v4(),
  )
  // Registration was successful.
} catch (err) {
  // Handle error. A failure result may be returned if the backend is unable
  // to perform the registration due to availability or security issues.
}
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [iOS](https://github.com/sudoplatform/samples-ios) and [Android](https://github.com/sudoplatform/samples-android) sample apps on GitHub, which demonstrate how to use a test registration key.
{% endhint %}

## De-registration (aka Account Deletion)

De-registering the user deletes the user and all associated resources from Sudo Platform.

{% hint style="warning" %}
If you are using federated sign-in and you wish to delete the user do not call the deregister API. Use the appropriate API provided by your identity provider instead.
{% endhint %}

{% hint style="info" %}
As with most platform APIs, de-registering requires the client to be signed in. Please see [Authentication](/guides/users/authentication) section for more details.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let uid = try await client.deregister()
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might be also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        client.deregister()
        // User de-registered successfully.
    } catch (e: DeregisterException) {
        // Handle error. A failure result may be returned if the backend is unable
        // to perform the de-registration due to availability or security issues.
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  await client.deregister()
  // User de-registered successfully.
} catch (err) {
  // Handle error. A failure result may be returned if the backend is unable
  // to perform the de-registration due to availability or security issues.
}
```

{% endtab %}
{% endtabs %}

## Resetting Client State

You can reset the internal state information maintained by `SudoUserClient` by calling `reset` API.

{% hint style="danger" %}
Resetting the client state will cause all persistent data to be lost including sign in key, authentication tokens and username. You would no longer be able to sign in as the previously registered user so a new user must be registered.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    try await client.reset()
} catch {
    // Handle error. An error might be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
client.reset()
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
client.reset()
```

{% endtab %}
{% endtabs %}


# Authentication

The Sudo Platform allows users to sign in from client apps using various mechanisms. Once signed in, a user obtains authentication tokens (OpenID Connect ID and access tokens) that can be used to access other services within the platform.

## OIDC Federated Sign In

{% hint style="danger" %}
In order to use federated sign in, you must have completed the federated sign in setup for your project by contacting <support@sudoplatform.com>.
{% endhint %}

If you are using an external identity provider that supports [OpenID Connect (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) standard then Sudo Platform access can be granted via federated sign in. When the user signs into your identity provider they will be federated and authenticated into Sudo Platform via OpenID Connect web flow. The user is not required to sign into your identity provider again until the refresh token expires. The refresh token lifetime can be configured by contacting <support@sudoplatform.com>.

The federated sign in requires the app to be configured with a URL scheme so that the authentication tokens from your identity provider can be passed into your app when the user performs federated sign in via a web view launched by the app. The URL scheme that you set up for your app must match one of the callback and logout URLs configured in the federated sign in configuration. To setup these URLs, contact <support@sudoplatform.com>.

{% hint style="info" %}
If you have more than one URL scheme configured for callback and logout URLs then make sure you edit "sudoplatformconfig.json" file to keep only the URL scheme that's relevant for your app for "signInRedirectUri" and "signOutRedirectUri".
{% endhint %}

> Setting up a URL scheme for iOS app:

{% embed url="<https://developer.apple.com/documentation/uikit/inter-process:communication/allowing:apps:and:websites:to:link:to:your:content/defining:a:custom:url:scheme:for:your:app>" %}

For example, add the following entry in Info.plist file of your app.

```markup
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
            </array>
        </dict>
    </array>
```

> Setting up a URL scheme for Android app:

{% embed url="<https://developer.android.com/training/app-links/deep-linking>" %}

For example, add the following to your app manifest in the main activity section:

```markup
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="com.mycompany.myapp"/>
</intent-filter>
```

> Signing in via federated sign in.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let tokens = try await client.presentFederatedSignInUI(presentationAnchor: window)
    // "tokens.idToken" can be used to initialize other service specific clients to authenticate to the backend API.
    // ID token and access token will expire after number of seconds specified in "lifetime".  The tokens will be
    // refreshed automatically or can be manually refreshed by calling the "refreshTokens" API.
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might be also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Your main activity must pass the authentication tokens from your
// identity provider into SudoUserClient when the activity is resumed
// via the URL scheme.
override fun onResume() {
    super.onResume()

    val data = this.intent.data
    if (data != null) {
        client.processFederatedSignInTokens(data)
    }
}

// Present the federated sign in UI assuming it's being called from
// an activity.
client.presentFederatedSignInUI(this) { result ->
    when (result) {
        is ApiResult.Success -> {
            // User registered and signed in successfully.        
        }
        is ApiResult.Failure -> {
            // Handle error. A failure result may be returned if the backend is unable
            // perform the registration due to availability or security issues.
        }
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  client.presentFederatedSignInUI()
  // User registered and signed in successfully.
catch (err) {
  // Handle error. A failure result may be returned if the backend is unable
  // perform the registration due to availability or security issues.
}

// In addition to the above, you will need to add code to process the redirect url.
processRedirectUrl = async () => {
  const redirectUrl = window.location.href
  await client.processFederatedSignInTokens(redirectUrl)
}
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [iOS](https://github.com/sudoplatform/samples-ios) and [Android](https://github.com/sudoplatform/samples-android) sample apps on GitHub, which demonstrate how to use federated sign in.
{% endhint %}

## Custom Federated Sign In

{% hint style="danger" %}
In order to use custom federated sign in, you must have completed the custom federated sign in setup described in the [Registration](/guides/users/registration#custom-federated-sign-in) section.
{% endhint %}

In order to sign in using a custom authentication provider, you must invoke `signInWithAuthenticationProvider` API of `SudoUserClient` with your `AuthenticationProvider` implementation as an input similar to how `registerWithAuthenticationProvider` was invoked to register a new Sudo Platform user. For details of `AuthenticationProvider` implementation, please refer to the [Registration](/guides/users/registration#custom-federated-sign-in) section and [API Reference](/guides/users/api-reference).

## Sign In via Private Key

If a user was registered using [Sign in Key Registration](/guides/users/registration#registration-via-private-key), they can use the private key from the public/private key pair generated at registration time to digitally sign an authentication token to sign-in.

> To sign-in using private key based authentication:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let tokens = try await client.signInWithKey()
    // "tokens.idToken" can be used to initialize other service specific clients to authenticate to the backend API.
    // ID token and access token will expire after number of seconds specified in "lifetime" so use "refreshToken"
    // to refresh these tokens via "refreshTokens" API.
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might be also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        val result = client.signInWithKey()
        // "idToken" can be used to initialize other service specific clients to authenticate to the backend API.
        // ID token and access token will expire after number of seconds specified in "lifetime" so use "refreshToken"
        // to refresh these tokens via "refreshTokens" API.
        println("idToken: ${result.idToken}, accessToken: ${result.accessToken}, refreshToken: ${result.refreshToken}")
    } catch (e: AuthenticationException) {
        // Handle error. A failure result may be returned if the backend is unable
        // to perform the sign in due to availability or security issues.
        println("${e.localizedMessage}")
    }
}
```

{% endtab %}
{% endtabs %}

## Refreshing ID and Access Tokens

Authentication tokens will expire after 1 hour so in order to avoid needing to sign in periodically you should refresh those tokens.

{% hint style="info" %}
Each Sudo Platform client such as Telephony client will automatically refresh ID and access tokens as needed but this will incur a delay in the API call so it is recommended to refresh the tokens at a scheduled interval or on app launch.
{% endhint %}

> To refresh ID and access tokens:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let tokens = try await client.refreshTokens(refreshToken: refreshToken)
    // "tokens.idToken" can be used to initialize other service specific clients to authenticate to the backend API.
    // ID token and access token will expire after number of seconds specified in "lifetime" so use "refreshToken"
    // to refresh these tokens via "refreshTokens" API.
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might be also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        val result = client.refreshTokens(refreshToken)
        // "idToken" can be used to initialize other service specific clients to authenticate to the backend API.
        // ID token and access token will expire after number of seconds specified in "lifetime" so use "refreshToken"
        // to refresh these tokens via "refreshTokens" API.
        println("idToken: ${result.idToken}, accessToken: ${result.accessToken}, refreshToken: ${result.refreshToken}")
    } catch (e: AuthenticationException) {
        // Handle error. A failure result may be returned if the backend is unable
        // to perform the sign in due to availability or security issues.
        println("${e.localizedMessage}")
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  const result = await client.refreshTokens(refreshToken)
  // "idToken" can be used to initialize other service specific clients to authenticate to the backend API.
  // ID token and access token will expire at the time indicated in tokenExpiry so use "refreshToken"
  // to refresh these tokens via the "refreshTokens" API.
} catch (err) {
  // Handle error. A failure result may be returned if the backend is unable
  // to perform the sign in due to availability or security issues.
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
It is not necessary to perform the sign in again until the refresh token expires as long as the ID token is getting refreshed via the mechanisms mentioned above. It is the consumer's responsibility to perform the sign in at a fixed interval or reactively when `notAuthorized` error is returned from a platform service client API call. The default expiry for refresh token is 30 days.
{% endhint %}

## Sign Out

> To sign out a user:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    try await client.globalSignOut()
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might be also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        client.globalSignOut()
        // Sign out successful.
    } catch (e: SignOutException) {
        // Handle error. A failure result may be returned if the backend is unable
        // perform the sign out due to availability or security issues.
        println("${e.localizedMessage}")
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  await client.globalSignOut()
  // Sign out successful.  
} catch (err) {
  // Handle error. A failure result may be returned if the backend is unable
  // to perform the sign out due to availability or security issues.     
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Sign out will invalidate authentication tokens from all devices regardless of the sign in method used. For the current device, all tokens will be invalidated immediately but on other devices the API access will still be granted to up to the lifetime of ID token. The ID token lifetime is 1 hour by default but this can be changed by submitting a support request.
{% endhint %}

> To invalidate authentication tokens for a single device.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    try client.signOut()
} catch {
    // Handle error. An error might be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly, the client is not signed in,
    // or basic system resources are unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
client.clearAuthTokens()
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
client.clearAuthenticationTokens()
```

{% endtab %}
{% endtabs %}

> Sign out from hosted UI for OIDC federated sign in.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    try await client.presentFederatedSignOutUI(presentationAnchor: window)
} catch {
    // Handle error. An error may be thrown if the backend is unable to perform
    // requested operation due to availability or security issues.
    // An error might be also be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly or basic system resources are
    // unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
client.presentFederatedSignOutUI { result ->
    when (result) {
        is ApiResult.Success -> {
            // User signed out successfully.        
        }
        is ApiResult.Failure -> {
            // Handle error. A failure result may be returned if the backend is unable
            // perform the sign out due to availability or security issues.
        }
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
client.presentSignOutUI()
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
This will invalidated any cached information from the web session used to contact Sudo Platform but does not invalidate any authentication tokens. You can call `clearAuthTokens` or `globalSignOut` subsequently to either invalidate authentication tokens from the current device or sign out the user from all devices.
{% endhint %}


# SDK Releases

Additional resources for User SDKs

Use the links below to learn about significant new features and fixes added in each release of the User SDKs and to access the source code for the SDKs and sample apps.

## User SDK for iOS

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-user-ios/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-user-ios/)
* [Sample app source code](https://github.com/sudoplatform/samples-ios)

## User SDK for Android

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-user-android/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-user-android/)
* [Sample app source code](https://github.com/sudoplatform/samples-android)

## User SDK for JS

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-user-js/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-user-js)
* Sample app source code (*coming soon*)


# API Reference

API references for the User SDKs

The links below provides a detailed API reference for the User SDKs.

* [iOS API Reference](https://sudoplatform.github.io/sudo-user-ios/index.html)
* [Android API Reference](https://sudoplatform.github.io/sudo-user-android/)
* [JS API Reference](https://sudoplatform.github.io/sudo-user-js/)


# Entitlements

Managing consumption of Sudo capabilities

The Sudo Platform entitlements service enables you to manage your users' consumption of Sudo capabilities.

An entitlement specifies how much of a Sudo capability a user is entitled to consume. For example, the maximum number of phone numbers able to be allocated simultaneously to a Sudo.

Entitlements are assigned to users either directly or indirectly by entitlements set.

An entitlements set is a collection of entitlements that can be assigned to many users. Changing the entitlements assigned to an entitlements set, changes the entitlements for all users assigned to that entitlements set.

Entitlements sets can also be assigned either directly to each user or indirectly by entitlements sequence.

An entitlements sequence describes a time based sequence of transitions from an initial entitlements set, through other entitlements sets at defined intervals.

## Entitlement Types

Sudo Platform services define two classes of entitlements: Boolean entitlements and numeric entitlements.

Boolean entitlements represent an overall capability to access a service or feature of a service.

Numeric entitlements represent a  limit on some resource that a user may consume. Numeric entitlements may either be expendable or not.

Expendable entitlements, once consumed, can never be recovered and are used to entitle operations that incur fees each time they are performed. Payment of the fee would be represented by increasing the expendable entitlement, performing the operation then consumed the entitlement.

Non-expendable entitlements can be recovered by the user deleting or cancelling instances of the resource they correspond to.

## Entitlements Sets

Entitlements are managed by integrating with your organization's existing entitlements system and defined as groups of entitlements called **Entitlements Sets**.

An entitlements set typically corresponds to a product subscription level and specifies all of the entitlements conferred to a user at that subscription level.

Entitlements sets can be changed to increase, decrease, add or remove entitlements for existing users entitled by that entitlements set allowing easy broad changes to entitlements across all of your users.

Entitlements can also be managed on a per-user basis, allowing for fine grained control of entitlements available to individual users.

Whether you manage user entitlements by direct specification of individual users' entitlements or by entitlements sets or both will depend on your existing product entitlement system, if any, and how your users will initially register to the Sudo Platform.

## Entitlements Sequences

Entitlements sequences describe a sequence of transitions from one entitlements set to another after specific periods of time. They can be used to model product subscriptions. For example, failure to renew a subscription after a certain amount of time may result in transition to an entitlements set with reduced entitlements. Each user records a timestamp from which entitlements set transitions are calculated, updating this timestamp on subscription renewal extends the time a user would remain entitled by the initial entitlements set in the sequence.

## Federated Single Sign-on Registration

If your users will register with Federated Single Sign-on, you will be able to configure a mapping from claims in the token issued by your identity provider to an initial entitlements set or sequence to allow for automatic entitlement of your users on first sign-on.

Contact <support@sudoplatform.com> to enable the appropriate mapping for your identity provider.

The initial establishment of entitlements in a federated single sign-on registration is performed by the client application calling the [`redeemEntitlements`](/guides/entitlements/end-user-api/redeeming-entitlements) API.

If you are unable to include a claim that can map to initial entitlements set during Federated Single Sign-on then entitlements must be pre-defined for each user. The [`redeemEntitlements`](/guides/entitlements/end-user-api/redeeming-entitlements) API must still be called to establish the mapping between your users and Sudo Platform users in the Sudo Platform entitlements system.

### Private Key Registration

Contact <support@sudoplatform.com> to enable entitlements for users registering by private key.

## Next Steps

* Integrate the **Entitlements SDK** into your [JavaScript](/guides/entitlements/end-user-api/integrate-the-entitlements-sdk#integrate-the-web-sdk), [iOS](/guides/entitlements/end-user-api/integrate-the-entitlements-sdk#integrate-the-ios-sdk) and [Android](/guides/entitlements/end-user-api/integrate-the-entitlements-sdk#integrate-the-android-sdk) applications
* Integrate the [**Administrative API**](/guides/entitlements/administrative-api/integrating-the-administrative-api) with your own entitlements system
* Learn more about [Sudo digital identities](/concepts/sudo-digital-identities) and other core concepts


# Administrative API

Administrative API for managing user entitlements

The Sudo Platform entitlements service administrative API allows you to

* Define the entitlements conferred by entitlements sets
* Define the entitlements set transitions represneted by entitlements sequences
* Set the entitlements for a user explicitly, by entitlements set or by entitlements sequence
* Add or remove expendable entitlements for a user
* Retrieve entitlements information for users
* Retrieve entitlements consumption information for users
* Retrieve defined entitlements in the system

Integrate this API with your existing entitlements system to automatically manage the entitlements of your users to Sudo Platform capabilities.


# Integrating the Administrative API

Integrate the Entitlements Administrative API with your own entitlements system

The Sudo Platform Entitlements administrative API is provided as a [GraphQL](https://graphql.org) API.

Invoke these APIs using a GraphQL client. You must specify an API key in a `x-api-key` header when making requests.

You can also use Sudo Entitlements Admin SDK to invoke these APIs in JavaScript, TypeScript, Swift or Kotlin. In order to use the SDK you must obtain the Sudo Platform client configuration file by following the instructions in [Complete the Getting Started Guide](/guides/getting-started).

## Integrate the iOS SDK

To use Entitlements Admin SDK in an iOS app, you need to install the Sudo Entitlements Admin package via Swift Package Manager

Open your project settings in XCode, and go to the `Package Dependencies` tab. Click on the `+` sign to add a dependency.&#x20;

Enter the repository URL [`https://github.com/sudoplatform/sudo-entitlements-admin-ios`](https://github.com/sudoplatform/sudo-entitlements-admin-ios) in the top right search box and select the `sudo-entitlements-admin-ios` repository.&#x20;

Select the required version and `Add Package`.&#x20;

Sudo Platform SDKs conform to semantic versioning so in most cases you will leave the Dependency Rule as `Up to Next Major Version` to receive regular updates without introducing any breaking changes.

This will resolve the local package dependency and install the latest version of the Entitlements Admin SDK.

## Integrate the Android SDK

To use Sudo Entitlements Admin SDK in an Android app you need to add `SudoEntitlementsAdmin` library as a dependency to your project.

Edit the app module Gradle script (build.gradle) and add `SudoEntitlementsAdmin` library as a dependency.

{% tabs %}
{% tab title="gradle" %}

```java
dependencies {
    api 'com.sudoplatform:sudoentitlementsadmin:$latest_version'
}
```

{% endtab %}
{% endtabs %}

Edit AndroidManifest.xml to add the following app permissions:

{% tabs %}
{% tab title="Diff" %}

```diff
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+   xmlns:tools="http://schemas.android.com/tools"
    package="com.sudoplatform.sampleapp">

+   <uses-permission android:name="android.permission.INTERNET" />
+   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+   <uses-permission android:name="android.permission.WAKE_LOCK" />
+   <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
+       tools:replace="android:allowBackup"
+       android:allowBackup="false"
```

{% endtab %}
{% endtabs %}

## Integrate the JS SDK

To use the Sudo Entitlements Admin SDK in a Web or Node.js project, you must add `@sudoplatform/sudo-entitlements-admin` as a dependency to your project.

```
yarn add '@sudoplatform/sudo-entitlements-admin'
# or
npm install --save '@sudoplatform/sudo-entitlements-admin'
```

## Initializing SudoEntitlementsAdminClient

{% tabs %}
{% tab title="Swift" %}

```swift
let client = try DefaultSudoEntitlementsAdminClient(apiKey: apiKey)

```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val client = SudoEntitlementsAdminClient.builder(
                appContext,
                apiKey
             ).build()
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const sudoEntitlementsAdmin = new DefaultSudoEntitlementsAdminClient(
  apiKey,
)
```

{% endtab %}
{% endtabs %}

For endpoint information and to obtain an API key [send us a request](https://sudoplatform.com/#lets-chat).


# Entitlement Definitions

Administrative APIs for working with entitlement definitions

Entitlement definitions describe particular entitlements. They specify whether an entitlement is Boolean or numeric and whether a numeric entitlement is expendable or not.

## Common Entitlements Definition Types

```graphql
type EntitlementDefinition
  name: String!
  description: String
  type: String!
  expendable: Boolean!
}
```

## Listing Entitlement Definitions

Call the `listEntitlementDefinitions` query to list all of the entitlement definitions in the system. This list will vary depending on the Sudo Platform service enabled in your environment.

The results list is paginated with a default page size of 10. To retrieve all results your application must implement the following algorithm.

```
1. Call listEntitlementDefinitions query with nextToken set to null
2. If nextToken in the returned EntitlementDefinitionsConnection is null, 
   there are no more results to retrieve
3. Call listEntitlementDefinitions query with nextToken set to the value
   returned from the previous call
4. Go to step 2
```

```graphql
# Pagination connection for use when listing entitlement definitions
type EntitlementDefinitionConnection {
  items: [EntitlementsDefinition!]!
  nextToken: String
}

# Input for the listEntitlementDefinitions query
input ListEntitlementDefinitionsInput {
  token: String!
}

type Query {
# Retrieves all entitlement definitions.
listEntitlementDefinitions(
  nextToken: String
): EntitlementDefinitionConnection!
}
```

### Possible Errors

* **ServiceError** will be returned for internal errors.

Listing entitlement definitions using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let listOutput = try await client.listEntitlementDefinitionsWithNextToken(
        limit: 50,
        nextToken: nil,
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val listOutput =
    client.listEntitlementDefinitions(
        50,
        null 
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const limit = 50
let nextToken: string|undefined
const listOutput = await client.listEntitlementDefinitions(
  limit,
  nextToken
)

```

{% endtab %}
{% endtabs %}


# Managing Entitlements Sets

Administrative APIs for managing entitlements sets

The standard create, read, update and delete management operations are performed by calling the addEntitlementsSet, getEntitlementsSet or listEntitlementsSets, setEntitlementsSet, and removeEntitlementsSet APIs respectively.

## Common Entitlements Sets Types

```graphql
# An entitlement
type Entitlement {
  # Name of the entitlement
  name: String!

  # Optional description of the entitlement
  description: String

  # Value of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer.
  value: Float!
}

# A set of entitlements
type EntitlementsSet {
  # Time of initial creation of an entitlements set in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  createdAtEpochMs: Float!

  # Time of most recent update of an entitlements set in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  updatedAtEpochMs: Float!

  # Version of the entitlements set. Incremented each time an update is made.
  version: Int!

  # Name of the entitlements set.
  name: String!

  # Optional description of the entitlements set.
  description: String

  # Entitlements conferred by this entitlements set.
  entitlements: [Entitlement!]!
}
```

## Add a New Entitlements Set

A new entitlements set can be added to the system by calling the `addEntitlementsSet` mutation.

```graphql
# Input for the addEntitlementsSet mutation
input AddEntitlementsSetInput {
  name: String!
  description: String
  entitlements: [EntitlementInput!]!
}

type Mutation {
  # Adds an entitlement set
  addEntitlementsSet(
    input: AddEntitlementsSetInput!
  ): EntitlementsSet!
}
```

### Possible Errors

* **InvalidEntitlementsError** will be returned if an entitlement name is not a recognized entitlement.
* **ServiceError** will be returned for internal errors.

Adding a new entitlements set using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSet = try await client.addEntitlementsSetWithName(
        name: "Premium user",
        description: "Entitlements set for premium users",
        entitlements: [Entitlement(name: "sudoplatform.sudo.max", description: "Max. Sudos", value: 3)]
    )
} catch {    
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSet =
    client.addEntitlementsSet(
        "Premium user", 
        "Entitlements set for premium users",
        listOf(
            Entitlement("sudoplatform.sudo.max", "Max. Sudos", 3)
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'Premium user',
  description: 'Entitlements for premium users',
  entitlements: [
    {
      name: 'sudoplatform.sudo.max',
      description: 'Max. Sudos',
      value: 3,
    }
  ],
}
const entitlementsSet = await client.addEntitlementsSet(input)

```

{% endtab %}
{% endtabs %}

## Get an Entitlements Set

Call the `getEntitlementsSet` query to retrieve an entitlements set by name.

```graphql
# Input for the getEntitlementsSet query
input GetEntitlementsSetInput {
  name: String!
}

type Query {
  # Gets an entitlement set.
  getEntitlementsSet(
    input: GetEntitlementsSetInput!
  ): EntitlementsSet
}
```

### Possible Errors

* **ServiceError** will be returned for internal errors.

Retrieving an entitlements set using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSet = try await client.getEntitlementsSetWithName(
        name: "Premium user",
    ) 
} catch let error {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSet =
    client.getEntitlementsSet(
        "Premium user"
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'Premium user',
}
const entitlementsSet = await client.getEntitlementsSet(input)

```

{% endtab %}
{% endtabs %}

## List all Entitlements Sets

Call the `listEntitlementsSet` query to list all of the entitlements sets in the system. The results list is paginated with a page size of 10. To retrieve all results your application must implement the following algorithm.

```
1. Call listEntitlementsSet query with nextToken set to null
2. If nextToken in the returned EntitlementsSetsConnection is null, 
   there are no more results to retrieve
3. Call listEntitlementsSet query with nextToken set to the value
   returned from the previous call
4. Go to step 2
```

```graphql
# Pagination connection for use when listing entitlements sets
type EntitlementsSetsConnection {
  items: [EntitlementsSet!]!
  nextToken: String
}

# Input for the listEntitlementsSets query
input ListEntitlementsSetsInput {
  token: String!
}

type Query {
# Retrieves all entitlements sets.
listEntitlementsSets(
  nextToken: String
): EntitlementsSetsConnection!
}
```

### Possible Errors

* **ServiceError** will be returned for internal errors.

Listing entitlements sets using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let listOutput = try await client.listEntitlementsSetsWithNextToken(
        nextToken: nil
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val listOutput =
    client.listEntitlementsSets(
        null 
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const listOutput = await client.listEntitlementsSets()

```

{% endtab %}
{% endtabs %}

## Update an Existing Entitlements Set

Call the `setEntitlementsSet` mutation to update the contents of an existing entitlements set. The updated entitlements set is returned.

```graphql
# Input representing an entitlement
input EntitlementInput {
  name: String!
  description: String
  value: Int!
}

# Input for the setEntitlementsSet mutation
input SetEntitlementsSetInput {
  name: String!
  description: String
  entitlements: [EntitlementInput!]!
}

type Mutation {
  # Change the entitlements conferred by an entitlements set.
  setEntitlementsSet(input: SetEntitlementsSetInput!): EntitlementsSet!
}
```

### Possible Errors

* **InvalidEntitlementsError** will be returned if an entitlement name is not a recognized entitlement.
* **ServiceError** will be returned for internal errors.

Updating an entitlements set using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSet = try await client.setEntitlementsSetWithName(
        name: "Premium user",
        description: "Entitlements set for premium users",
        entitlements: [Entitlement(name: "sudoplatform.sudo.max", description: "Max. Sudos", value: 5)]
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}    

```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSet =
    client.setEntitlementsSet(
        "Premium user", 
        "Entitlements set for premium users",
        listOf(
            Entitlement("sudoplatform.sudo.max", "Max. Sudos", 5)
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'Premium user',
  description: 'Entitlements for premium users',
  entitlements: [
    {
      name: 'sudoplatform.sudo.max',
      description: 'Max. Sudos',
      value: 3,
    }
  ],
}
const entitlementsSet = await client.setEntitlementsSet(input)

```

{% endtab %}
{% endtabs %}

## Remove an Entitlements Set

Call the `removeEntitlementsSet` mutation to remove an existing entitlements set by name. The removed entitlements set is returned on success. null is returned if the named entitlements set cannot be found.

```graphql
# Input for the removeEntitlementsSet mutation
input RemoveEntitlementsSetInput {
  name: String!
}

type Mutation {
  # Remove an entitlements set. Any users configured against this
  # entitlements set will become unentitled.
  removeEntitlementsSet(input: RemoveEntitlementsSetInput!): EntitlementsSet
}
```

### Possible Errors

* **EntitlementsSetInUseError** will be returned if any entitlements sequences exist that reference the entitlements set
* **ServiceError** will be returned for internal errors.

Removing an entitlements set using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSet = try await client.removeEntitlementsSetWithName(
        name: "Premium user"
    )
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
} 
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSet =
    client.removeEntitlementsSet(
        "Premium user"
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'Premium user',
}
const entitlementsSet = await client.removeEntitlementsSet(input)

```

{% endtab %}
{% endtabs %}


# Managing Entitlements Sequences

Administrative APIs for managing entitlements sequences

The standard create, read, update and delete management operations are performed by calling the addEntitlementsSeqence, getEntitlementsSequence or listEntitlementsSetquences, setEntitlementsSequence and removeEntitlementsSequence APIs respectively.

## Common Entitlements Sequence Types

```graphql
# A transition within an entitlements sequence
type EntitlementsSequenceTransition {
  # Name of entitlements set
  entitlementsSetName: String!

  # ISO8601 period string - if not specified then this transition
  # is the final state for all users on the sequence.
  duration: String
}

# A sequence of entitlements set transitions
type EntitlementsSequence {
  # Name of the entitlements sequence
  name: String!

  # Description of the entitlements sequence
  description: String

  # Time of initial creation of an entitlements sequence in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  createdAtEpochMs: Float!

  # Time of most recent update of an entitlements sequence in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  updatedAtEpochMs: Float!

  # Version of the entitlements sequence. Incremented each time an update is made.
  version: Int!

  # Sequence of transitions a user will go through in order. Must not be empty.
  transitions: [EntitlementsSequenceTransition!]!
}
```

## Add a New Entitlements Sequence

A new entitlements sequence can be added to the system by calling the `addEntitlementsSequence` mutation. All entitlements sets referenced by the transitions in the sequence must exist prior to adding the entitlements sequence.

```graphql
# Input of a single transition within an entitlements sequence
input EntitlementsSequenceTransitionInput {
  # Name of entitlements set
  entitlementsSetName: String!

  # ISO8601 period string - if not specified then this transition
  # is the final state for all users on the sequence. If the
  # final transition has a duration, then when that duration passed
  # the user becomes unentitled for all entitlements.
  duration: String
}

# Input for the addEntitlementsSequence mutation
input AddEntitlementsSequenceInput {
  # Name of the entitlements sequence
  name: String!

  # Description of the entitlements sequence
  description: String

  # Sequence of transitions a user will go through in order. Must not be empty.
  transitions: [EntitlementsSequenceTransitionInput!]!
}

type Mutation {
  # Adds an entitlement sequence
  addEntitlementsSequence(
    input: AddEntitlementsSequenceInput!
  ): EntitlementsSequence!
}
```

### Possible Errors

* **InvalidArgumentError** will be returned if the transitions array is empty or any transitions earlier in the list than the last transition do not specify a duration.
* **EntitlementsSetNotFoundError** will be returned if any of the referenced entitlements sets do not exist
* **ServiceError** will be returned for internal errors.

Adding a new entitlements sequence using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSetquence = try await client.addEntitlementsSequenceWithName(
        name: "premium_subscription",
        description: "Premium subscription plan",
        transitions: [
            EntitlementSequenceTransition(
                entitlementsSetName: "initial_entitlements_set",
                duration: "P3M"
            ),
            EntitlementSequenceTransition(
                entitlementsSetName: "second_entitlements_set",
                duration: "P1M"
            )
        ]
    )
} catch {    
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSequence =
    client.addEntitlementsSequence(
        "premium_subscription", 
        "Premium subscription plan",
        listOf(
            EntitlementSequenceTransition("initial_entitlements_set", "P3M"),
            EntitlementSequenceTransition("second_entitlements_set", "P1M")
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'premium_subscription',
  description: "Premium subscription plan',
  transitions: [
    {
      entitlementsSetName: 'initial_entitlements_set',
      duration: 'P3M',
    },
    {
      entitlementsSetName: 'second_entitlements_set',
      duration: 'P1M',
    },
  ],
}
const entitlementsSequence = await client.addEntitlementsSequence(input)

```

{% endtab %}
{% endtabs %}

## Get an Entitlements Sequence

Call the `getEntitlementsSequence` query to retrieve an entitlements set by name.

```graphql
# Input for the getEntitlementsSequence query
input GetEntitlementsSequenceInput {
  name: String!
}

type Query {
  # Gets an entitlement sequence.
  getEntitlementsSequenc(
    input: GetEntitlementsSequenceInput!
  ): EntitlementsSequence
}
```

### Possible Errors

* **ServiceError** will be returned for internal errors.

Retrieving an entitlements sequence using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSequence = try await client.getEntitlementsSequenceWithName(
        name: "premium_subscription_plan"
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSequence =
    client.getEntitlementsSequence(
        "premium_subscription_plan"
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'premium_subscription_plan',
}
const entitlementsSequence = await client.getEntitlementsSequence(input)

```

{% endtab %}
{% endtabs %}

## List all Entitlements Sequences

Call the `listEntitlementsSequences` query to list all of the entitlements setquences in the system. The results list is paginated with a page size of 10. To retrieve all results your application must implement the following algorithm.

```
1. Call listEntitlementsSequences query with nextToken set to null
2. If nextToken in the returned EntitlementsSequencessConnection is null, 
   there are no more results to retrieve
3. Call listEntitlementsSequences query with nextToken set to the value
   returned from the previous call
4. Go to step 2
```

```graphql
# Pagination connection for use when listing entitlements sequences
type EntitlementsSequencesConnection {
  items: [EntitlementsSequence!]!
  nextToken: String
}

# Input for the listEntitlementsSequences query
input ListEntitlementsSequencesInput {
  token: String!
}

type Query {
# Retrieves all entitlements sequences.
listEntitlementsSequences(
  nextToken: String
): EntitlementsSequencesConnection!
}
```

### Possible Errors

* **ServiceError** will be returned for internal errors.

Listing entitlements sequences using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let listOutput = try await client.listEntitlementsSequencesWithNextToken(
        nextToken: nil
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val listOutput =
    client.listEntitlementsSequences(
        null 
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const listOutput = await client.listEntitlementsSequences()

```

{% endtab %}
{% endtabs %}

## Update an Existing Entitlements Sequence

Call the `setEntitlementsSequence` mutation to update the contents of an existing entitlements sequence. The updated entitlements sequence is returned.

```graphql
# Input of a single transition within an entitlements sequence
input EntitlementsSequenceTransitionInput {
  # Name of entitlements set
  entitlementsSetName: String!

  # ISO8601 period string - if not specified then this transition
  # is the final state for all users on the sequence. If the
  # final transition has a duration, then when that duration passed
  # the user becomes unentitled for all entitlements.
  duration: String
}

# Input for the setEntitlementsSequence mutation
input SetEntitlementsSequenceInput {
  name: String!
  description: String
  transitions: [EntitlementSequenceTransitionInput!]!
}

type Mutation {
  # Change the entitlements set transitions represented by an entitlements sequence.
  setEntitlementsSequence(input: SetEntitlementsSequenceInput!): EntitlementsSequence!
}
```

### Possible Errors

* **InvalidArgumentError** will be returned if the transitions array is empty or any transitions earlier in the list than the last transition do not specify a duration.
* **EntitlementsSetNotFoundError** will be returned if any of the referenced entitlements sets do not exist
* **ServiceError** will be returned for internal errors.

Updating an entitlements sequence using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSequence = try await client.setEntitlementsSequenceWithName(
        name: "premium_subscription",
        description: "Preumium subscription plan",
        transitions: [
            EntitlementSequenceTransition(
                entitlementsSetName: "initial_entitlements_set",
                duration: "P3M"
            ),
            EntitlementSequenceTransition(
                entitlementsSetName: "second_entitlements_set",
                duration: "P1M"
            )
        ]

    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}    

```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSequence =
    client.setEntitlementsSequence(
        "premium_subscription", 
        "Preumium subscription plan",
        listOf(
            EntitlementSequenceTransition("initial_entitlements_set", "P3M"),
            EntitlementSequenceTransition("second_entitlements_set", "P1M")
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'premium_subscription',
  description: "Premium subscription plan',
  transitions: [
    {
      entitlementsSetName: 'initial_entitlements_set',
      duration: 'P3M',
    },
    {
      entitlementsSetName: 'second_entitlements_set',
      duration: 'P1M',
    },
  ],
}
const entitlementsSequence = await client.setEntitlementsSequence(input)

```

{% endtab %}
{% endtabs %}

## Remove an Entitlements Sequence

Call the `removeEntitlementsSequence` mutation to remove an existing entitlements sequence by name. The removed entitlements sequence is returned on success. null is returned if the named entitlements sequence cannot be found.

```graphql
# Input for the removeEntitlementsSequence mutation
input RemoveEntitlementsSequenceInput {
  name: String!
}

type Mutation {
  # Remove an entitlements sequence. Any users configured against this
  # entitlements sequence will become unentitled.
  removeEntitlementsSequence(input: RemoveEntitlementsSequenceInput!): EntitlementsSequence
}
```

### Possible Errors

* **ServiceError** will be returned for internal errors.

Removing an entitlements sequence using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let entitlementsSequence = try await client.removeEntitlementsSequenceWithName(
        name: "premium_subscription"
    )
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
} 
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val entitlementsSequence =
    client.removeEntitlementsSequence(
        "premium_subscription"
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  name: 'premium_subscription',
}
const entitlementsSequence = await client.removeEntitlementsSequence(input)

```

{% endtab %}
{% endtabs %}


# Managing User Entitlements

Administrative APIs for managing user entitlements

A user's entitlements can be modified either by applying an entitlements set to the user or by explicitly applying individual entitlements to the user.

## Common Entitlements Types

```graphql
# An entitlement
type Entitlement {
  # Name of the entitlement
  name: String!

  # Optional description of the entitlement
  description: String

  # Value of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer.
  value: Float!
}

# Sub-resource consumer of the entitlement
# For example some entitlements are per-Sudo
type EntitlementConsumer {
  # ID of the consuming resource
  id: ID!

  # Issuer of the consuming resource ID e.g. sudoplatform.sudoservice for Sudos
  issuer: String!
}

# Effective entitlements for an external user
type ExternalUserEntitlements  {
  # Time of initial creation of user entitlements mapping in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  createdAtEpochMs: Float!

  # Time of last updates of user entitlements mapping in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  updatedAtEpochMs: Float!

  # Version number of the user's entitlements. This is incremented every
  # time there is a change of entitlements set or explicit entitlements
  # for this user.
  #
  # For users entitled by entitlement set, the fractional part of this version
  # specifies the version of the entitlements set itself. Entitlements set version
  # is divided by 100000 then added to the user entitlements version
  #
  # This ensures that the version of user entitlements always increases mon
  version: Float!

  # External IDP identifier identifying the user
  externalId: String!

  # Sudo Platform owner. This value matches the subject in identity
  # tokens used to authenticate to Sudo Platform services.
  owner: String

  # Name of the entitlements set specified for this user. Will be undefined
  # if entitlements have been specified explicitly rather than by an
  # entitlements set name.
  entitlementsSetName: String

  # Effective entitlements for the user either obtained from the entitlements
  # set or as specified explicitly for this user.
  entitlements: [Entitlement!]!
}

# An error result returned for an operation in a bulk applyEntitlements*ToUsers
# mutation.
type ExternalUserEntitlementsError {
  # Error code of failed operation
  error: String!
}

# Union of success and error results returned for operations in a bulk
# applyEntitlements*ToUsers mutation.
union ExternalUserEntitlementsResult =
    ExternalUserEntitlements
  | ExternalUserEntitlementsError

```

## Apply Entitlements Set to a User

A previously defined entitlements set can be applied to a user by calling the `applyEntitlementsSetToUser` mutation.

```graphql
# Input for the applyEntitlementsSetToUser mutation
input ApplyEntitlementsSetToUserInput {
  externalId: String!
  entitlementsSetName: String!
}


# Apply an entitlement set with the specified name to a user.
applyEntitlementsSetToUser(
    input: ApplyEntitlementsSetToUserInput!
): ExternalUserEntitlements!
```

### Possible Errors

* **EntitlementsSetNotFoundError** will be returned if an entitlements set with the name of the supplied entitlementsSetName parameter is unknown to the entitlements service.
* **AlreadyUpdatedError** will be returned if an attempt to update a user's entitlements is made after the user's entitlements have already been updated to a later version.
* **ServiceError** will be returned for internal errors.

Applying an entitlements set to a user using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let userEntitlements = try await client.applyEntitlementsSetToUserWithExternalId(
        externalId: externalId,
        entitlementsSetName: "Premium user"
    )
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlements =
    client.applyEntitlementsSetToUser(
        externalId,
        "Premium user" 
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  externalId,
  name: 'Premium user',
}
const userEntitlements = await client.applyEntitlementsSetToUser(input)

```

{% endtab %}
{% endtabs %}

## Apply Entitlements Sets to Multiple Users

Previously defined entitlements set can be applied to multiple users by calling the `applyEntitlementsSetToUsers` mutation.

```graphql
# Input for the applyEntitlementsSetToUsers mutation
input ApplyEntitlementsSetToUsersInput {
  operations: [ApplyEntitlementsSetToUserInput!]!
}

# Apply entitlement sets to multiple users.
applyEntitlementsSetToUsers(
    input: ApplyEntitlementsSetToUsersInput!
): [ExternalUserEntitlementsResult!]!
```

### Possible Errors

Each individual operation may fail with any of the same errors that `applyEntitlementsSetToUser` may fail. These are returned in the corresponding element of the returned list of results.

In addition, the overall mutation can fail with the following errors:

* **LimitExceededError** will be returned if too many operations are specified. The maximum number of operations is 1500. This may be adjusted for specific environments.
* **BulkOperationDuplicateUsersError** will be returned if more than one individual operation specifies the same value for `externalId`.&#x20;
* **ServiceError** will be returned for internal errors.

Applying an entitlements set to multiple users using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
applyEntitlementsSetToUsers not currently available for Swift
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlementResults =
    client.applyEntitlementsSetToUsers(
        listOf(
            ApplyEntitlementsSetOperation("external-id-1", "premium_user"), 
            ApplyEntitlementsSetOperation("external-id-2", "trial_user")
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const userEntitlements = await client.applyEntitlementsSetToUsers([
  { externalId: 'external-id-1', entitlementsSetName: 'premium_user', },
  { externalId: 'external-id-2', entitlementsSetName: 'trial_user', },
])

```

{% endtab %}
{% endtabs %}

## Apply Entitlements Sequence to a User

A previously defined entitlements setquence can be applied to a user by calling the `applyEntitlementsSequenceToUser` mutation.

```graphql
# Input for the applyEntitlementsSequenceToUser mutation
input ApplyEntitlementsSequenceToUserInput {
  externalId: String!
  entitlementsSequenceName: String!

  # Milliseconds since epoch from when user's transitions should
  # be calculated. Defaults to current time.
  # Is a Float only for precision. Must be an integral value.
  transitionsRelativeToEpochMs: Float
}


# Apply an entitlement sequence with the specified name to a user.
applyEntitlementsSequenceToUser(
    input: ApplyEntitlementsSequenceToUserInput!
): ExternalUserEntitlements!
```

### Possible Errors

* **EntitlementsSequenceNotFoundError** will be returned if an entitlements sequence with the name of the supplied entitlementsSequenceName parameter is unknown to the entitlements service.
* **AlreadyUpdatedError** will be returned if an attempt to update a user's entitlements is made after the user's entitlements have already been updated to a later version.
* **ServiceError** will be returned for internal errors.

Applying an entitlements sequence to a user using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let userEntitlements = try await client.applyEntitlementsSequenceToUserWithExternalId(
        externalId: externalId,
        entitlementsSequenceName: "premium_subscription_plan"
    )
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlements =
    client.applyEntitlementsSequenceToUser(
        externalId,
        "premium_subscription_plan" 
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  externalId,
  name: 'premium_subscription_plan',
}
const userEntitlements = await client.applyEntitlementsSequenceToUser(input)

```

{% endtab %}
{% endtabs %}

## Apply Entitlements Sequences to Multiple Users

Previously defined entitlements sequences can be applied to multiple users by calling the `applyEntitlementsSequenceToUsers` mutation.

```graphql
# Input for the applyEntitlementsSequenceToUsers mutation
input ApplyEntitlementsSequenceToUsersInput {
  operations: [ApplyEntitlementsSequenceToUserInput!]!
}

# Apply entitlement sequences to multiple users.
applyEntitlementsSequenceToUsers(
    input: ApplyEntitlementsSequenceToUsersInput!
): [ExternalUserEntitlementsResult!]!
```

### Possible Errors

Each individual operation may fail with any of the same errors that `applyEntitlementsSequenceToUser` may fail. These are returned in the corresponding element of the returned list of results.

In addition, the overall mutation can fail with the following errors:

* **LimitExceededError** will be returned if too many operations are specified. The maximum number of operations is 1500. This may be adjusted for specific environments.
* **BulkOperationDuplicateUsersError** will be returned if more than one individual operation specifies the same value for `externalId`.&#x20;
* **ServiceError** will be returned for internal errors.

Applying an entitlements set to multiple users using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
applyEntitlementsSequenceToUsers not currently available for Swift
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlementResults =
    client.applyEntitlementsSequenceToUsers(
        listOf(
            ApplyEntitlementsSequenceOperation("external-id-1", "premium_user"), 
            ApplyEntitlementsSequenceOperation("external-id-2", "trial_user")
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const userEntitlements = await client.applyEntitlementsSequenceToUsers([
  { externalId: 'external-id-1', entitlementsSequenceName: 'premium_user', },
  { externalId: 'external-id-2', entitlementsSequenceName: 'trial_user', },
])

```

{% endtab %}
{% endtabs %}

## Apply Entitlements to a User

A list of individual entitlements can be applied to a user by calling the `applyEntitlementsToUser` mutation.

```graphql
# Input representing an entitlement
input EntitlementInput {
  # Name of the entitlement
  name: String!

  # Description, if any, of the entitlement as specified by the entitlements administrator.
  description: String

  # Value of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer. Maximum value when setting entitlements is 2^52 - 1
  value: Float!
}

# Input for the applyEntitlementsToUser mutation
input ApplyEntitlementsToUserInput {
  externalId: String!
  entitlements: [EntitlementInput!]!
}

# Apply entitlements to a user without using a named entitlements set.
applyEntitlementsToUser(
  input: ApplyEntitlementsToUserInput!
): ExternalUserEntitlements!
```

### Possible Errors

* **InvalidEntitlementsError** will be returned if an entitlement name is not a recognized entitlement.
* **AlreadyUpdatedError** will be returned if an attempt to update a user's entitlements is made after the user's entitlements have already been updated to a later version.
* **ServiceError** will be returned for internal errors.

Applying entitlements to a user using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let userEntitlements = try await client.applyEntitlementsToUserWithExternalId(
        externalId: externalId,
        entitlements: [Entitlement(name: "sudoplatform.sudo.max", description: "Max. Sudos", value: 3)]
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlements =
    client.applyEntitlementsToUser(
        externalId, 
        listOf(
            Entitlement("sudoplatform.sudo.max", "Max. Sudos", 3)
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  externalId,
  entitlements: [
    {
      name: 'sudoplatform.sudo.max',
      description: 'Max. Sudos',
      value: 3,
    }
  ],
}
const userEntitlements = await client.applyEntitlementsToUser(input)

```

{% endtab %}
{% endtabs %}

## Apply Entitlements to Multiple Users

Entitlements can be applied to multiple users by calling the `applyEntitlementsToUsers` mutation.

```graphql
# Input for the applyEntitlementsToUsers mutation
input ApplyEntitlementsToUsersInput {
  operations: [ApplyEntitlementsToUserInput!]!
}

# Apply entitlement to multiple users.
applyEntitlementsToUsers(
    input: ApplyEntitlementsToUsersInput!
): [ExternalUserEntitlementsResult!]!
```

### Possible Errors

Each individual operation may fail with any of the same errors that `applyEntitlementsToUser` may fail. These are returned in the corresponding element of the returned list of results.

In addition, the overall mutation can fail with the following errors:

* **LimitExceededError** will be returned if too many operations are specified. The maximum number of operations is 1500. This may be adjusted for specific environments.
* **BulkOperationDuplicateUsersError** will be returned if more than one individual operation specifies the same value for `externalId`.&#x20;
* **ServiceError** will be returned for internal errors.

Applying an entitlements set to multiple users using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
applyEntitlementsToUsers not currently available for Swift
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlementResults =
    client.applyEntitlementsToUsers(
      listOf(
        ApplyEntitlementsOperation("external-id-1", listOf(
            Entitlement("sudoplatform.sudo.max", "Max Sudos", 3)
        )),
        ApplyEntitlementsOperation("external-id-2", listOf(
            Entitlement("sudoplatform.sudo.max", "Max Sudos", 2)
        ))
      )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const userEntitlements = await client.applyEntitlementsToUsers([
  { externalId: 'external-id-1', [{name: 'sudoplatform.sudo.max', value: 3}] },
  { externalId: 'external-id-2', [{name: 'sudoplatform.sudo.max', value: 2}] },
])

```

{% endtab %}
{% endtabs %}

## Apply Expendable Entitlements to a User

A list of individual expendable entitlements can be applied to a user by calling the `applyExpendableEntitlementsToUser` mutation. The user's expendable entitlements will be incremented (or decremented if entitlement value is negaetive) by the amount specified for each entitlement. Since this is an increment operation, a request ID is required and is used to provide idempotence of the operation. Replayed requests will have no effect on the user's expendable entitlements the current user entitlements will be returned.

```graphql
# Input representing an entitlement
input EntitlementInput {
  # Name of the entitlement
  name: String!

  # Description, if any, of the entitlement as specified by the entitlements administrator.
  description: String

  # Value of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer. Maximum value when setting entitlements is 2^52 - 1
  value: Float!
}

# Input for the applyEntitlementsToUser mutation
input ApplyExpendableEntitlementsToUserInput {
  externalId: String!
  expendableEntitlements: [EntitlementInput!]!
  requestId: String!
}

# Apply expendable entitlements to a user.
applyExpendableEntitlementsToUser(
  input: ApplyExpendableEntitlementsToUserInput!
): ExternalUserEntitlements!
```

### Possible Errors

* **InvalidEntitlementsError** will be returned if an entitlement name is not a recognized expendable entitlement.
* **DuplicateEntitlementError** will be returned if the same expendable entitlement is specified more than once in the `expendableEntitlements` property of the input.
* **NegativeEntitlementError** will be returned if the change in an expendable entitlement would result in negative entitlement.
* **AlreadyUpdatedError** will be returned if an attempt to update a user's entitlements is made after the user's entitlements have already been updated to a later version.
* **ServiceError** will be returned for internal errors.

Applying entitlements to a user using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let userEntitlements = try await client.applyEntitlementsToUserWithExternalId(
        externalId: externalId,
        entitlements: [Entitlement(name: "sudoplatform.sudo.max", description: "Max. Sudos", value: 3)]
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlements =
    client.applyEntitlementsToUser(
        externalId, 
        listOf(
            Entitlement("sudoplatform.sudo.max", "Max. Sudos", 3)
        )
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  externalId,
  entitlements: [
    {
      name: 'sudoplatform.sudo.max',
      description: 'Max. Sudos',
      value: 3,
    }
  ],
}
const userEntitlements = await client.applyEntitlementsToUser(input)

```

{% endtab %}
{% endtabs %}

## Get Entitlements for a User

The effective entitlements assigned to a user and their current consumption of those entitlements is retrieved by calling the `getEntitlementsForUser` query.

Entitlement values are returned with three pieces of information:

`value` is the total entitled amount

`consumed` is the amount of the entitlement consumed

`available` is the amount remaining.

It is always true that `value = consumed + available`

It's possible that available can be negative. This can happen transiently when a user's entitlement is reduced and the system has not yet had time to reclaim the resources to bring the user back in to compliance with their entitlement.

```graphql
type EntitlementConsumption {
  # Consumer of entitlement. If not present, entitlement is consumed
  # at the user level
  consumer: EntitlementConsumer

  # Name of the entitlement
  name: String!

  # Value of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer. Maximum value when retrieving entitlements
  # is 2^53 - 1
  value: Float!

  # Consumed amount of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer. Maximum value when retrieving entitlements
  # is 2^53 - 1
  consumed: Float!

  # Available amount of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer. Maximum value when retrieving entitlements
  # is 2^53 - 1
  available: Float!

  # Time this entitlement was first consumed
  firstConsumedAtEpochMs: Float

  # Time of most recent consumption of this entitlement
  lastConsumedAtEpochMs: Float
}

# An aggregated list of entitlement consumption information for an external user.
type ExternalEntitlementsConsumption  {
  # User's active entitlements
  entitlements: ExternalUserEntitlements!
  # User's entitlement consumption
  consumption: [EntitlementConsumption!]!
}

# Input type for getEntitlementsForUser query
input GetEntitlementsForUserInput {
  externalId: String!
}

type Query {
  # Retrieve effective entitlements for a given external user.
  getEntitlementsForUser(
    input: GetEntitlementsForUserInput!
  ): ExternalEntitlementsConsumption!
}
```

The `getEntitlementsForUser` query returns an aggregation of the specified user’s entitlement consumption information.

### Possible Errors

* **NoEntitlementsError** is returned if no entitlements have been defined for the user
* **ServiceError** will be returned for internal errors.

Retrieving a user's entitlements and consumption using SDK.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let userEntitlementsConsumption = try await client.getEntitlementsForUserWithExternalId(
        externalId: externalId
    ) 
} catch {
    // Handle error. An error may be thrown if the backend is unable perform
    // the operation due to invalid input, availability or security issues.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val userEntitlementsConsumption =
    client.getEntitlementsForUser(
        externalId, 
    )
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
const input = {
  externalId,
}
const userEntitlementsConsumption = await client.getEntitlementsForUser(input)

```

{% endtab %}
{% endtabs %}

To remove all record of a user from the entitlements system call the `removeEntitledUser` mutation.

```graphql
# Input for the removeEntitledUser mutation
input RemoveEntitledUserInput {
  externalId: String!
}

```


# API Schema

The GraphQL API schema for the Sudo Platform Entitlements administrative API

```graphql
# An entitlement
type Entitlement {
  # Name of the entitlement
  name: String!

  # Optional description of the entitlement
  description: String

  # Value of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer.
  value: Float!
}

# Entitlement Type in an entitlement definition: "numeric" or "boolean"
scalar EntitlementType

# A set of entitlements
type EntitlementsSet {
  # Time of initial creation of an entitlements set in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  createdAtEpochMs: Float!

  # Time of most recent update of an entitlements set in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  updatedAtEpochMs: Float!

  # Version of the entitlements set. Incremented each time an update is made.
  version: Int!

  # Name of the entitlements set.
  name: String!

  # Optional description of the entitlements set.
  description: String

  # Entitlements conferred by this entitlements set.
  entitlements: [Entitlement!]!
}

# Pagination connection for use when listing entitlements sets
type EntitlementsSetsConnection {
  items: [EntitlementsSet!]!
  nextToken: String
}

# Definition of an Entitlement
type EntitlementDefinition {
  name: String!
  description: String
  type: EntitlementType!
  expendable: Boolean!
}

# Pagination connection for use when listing entitlement definitions
type EntitlementDefinitionConnection {
  items: [EntitlementDefinition!]!
  nextToken: String
}

# Definition of a single transition within an entitlements sequence
type EntitlementsSequenceTransition {
  # Name of entitlements set
  entitlementsSetName: String!

  # ISO8601 period string - if not specified then this transition
  # is the final state for all users on the sequence.
  duration: String
}

# Definition of a sequence of entitlements sets through which a user will transition
type EntitlementsSequence {
  # Name of the entitlements sequence
  name: String!

  # Description of the entitlements sequence
  description: String

  # Time of initial creation of an entitlements sequence in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  createdAtEpochMs: Float!

  # Time of most recent update of an entitlements sequence in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  updatedAtEpochMs: Float!

  # Version of the entitlements sequence. Incremented each time an update is made.
  version: Int!

  # Sequence of transitions a user will go through in order. Must not be empty.
  transitions: [EntitlementsSequenceTransition!]!
}

# Pagination connection for use when listing entitlements sequences
type EntitlementsSequencesConnection {
  items: [EntitlementsSequence!]!
  nextToken: String
}

# Effective entitlements for an external user
type ExternalUserEntitlements {
  # Time of initial creation of user entitlements mapping in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  createdAtEpochMs: Float!

  # Time of last updates of user entitlements mapping in milliseconds
  # since epoch. Number is integral, float type provides sufficient
  # precision.
  updatedAtEpochMs: Float!

  # Version number of the user's entitlements. This is incremented every
  # time there is a change of entitlements set or explicit entitlements
  # for this user.
  #
  # For users entitled by entitlement set, the fractional part of this version
  # specifies the version of the entitlements set itself. Entitlements set version
  # is divided by 100000 then added to the user entitlements version
  #
  # This ensures that the version of user entitlements always increases mon
  version: Float!

  # External IDP identifier identifying the user
  externalId: String!

  # Sudo Platform owner. This value matches the subject in identity
  # tokens used to authenticate to Sudo Platform services.
  owner: String

  # Name of the entitlements set specified for this user. Will be undefined
  # if entitlements have been specified explicitly rather than by an
  # entitlements set name.
  entitlementsSetName: String

  # Name of the entitlements sequence specified for this user. Will be undefined
  # if entitlements have been specified explicitly or by entitlements set
  # rather than by an entitlements sequence name.
  entitlementsSequenceName: String

  # Effective entitlements for the user either obtained from the entitlements
  # set or as specified explicitly for this user.
  entitlements: [Entitlement!]!

  # Expendable entitlements for the user.
  expendableEntitlements: [Entitlement!]!

  # Milliseconds since epoch from when user's transitions should
  # be calculated. Defaults to current time.
  # Is a Float only for precision. Must be an integral value.
  transitionsRelativeToEpochMs: Float
}

# An error result returned for an operation in a bulk applyEntitlements*ToUsers
# mutation.
type ExternalUserEntitlementsError {
  # Error code of failed operation
  error: String!
}

# Union of success and error results returned for operations in a bulk
# applyEntitlements*ToUsers mutation.
union ExternalUserEntitlementsResult =
    ExternalUserEntitlements
  | ExternalUserEntitlementsError

# An aggregated list of entitlement consumption information for an external user.
type ExternalEntitlementsConsumption {
  # User's active entitlements
  entitlements: ExternalUserEntitlements!
  # User's entitlement consumption
  consumption: [EntitlementConsumption!]!
}

# Entitled user.
type EntitledUser {
  externalId: String!
}

# Input representing an entitlement
input EntitlementInput {
  # Name of the entitlement
  name: String!

  # Description, if any, of the entitlement as specified by the entitlements
  #  administrator.
  description: String

  # Value of the entitlement. Type Float to allow for values
  # values larger than possible with Int. Value is a
  # positive integer. Maximum value on input is 2^52-1
  value: Float!
}

# Input for the setEntitlementsSet mutation
input SetEntitlementsSetInput {
  name: String!
  description: String
  entitlements: [EntitlementInput!]!
}

# Input for the addEntitlementsSet mutation
input AddEntitlementsSetInput {
  name: String!
  description: String
  entitlements: [EntitlementInput!]!
}

# Input for the getEntitlementsSet query
input GetEntitlementsSetInput {
  name: String!
}

# Input for the removeEntitlementsSet mutation
input RemoveEntitlementsSetInput {
  name: String!
}

# Input for the applyEntitlementsSetToUser mutation
input ApplyEntitlementsSetToUserInput {
  externalId: String!
  entitlementsSetName: String!
}

# Input for the applyEntitlementsSetToUsers mutation
input ApplyEntitlementsSetToUsersInput {
  operations: [ApplyEntitlementsSetToUserInput!]!
}

# Input for the applyEntitlementsSequenceToUser mutation
input ApplyEntitlementsSequenceToUserInput {
  externalId: String!
  entitlementsSequenceName: String!

  # Milliseconds since epoch from when user's transitions should
  # be calculated. Defaults to current time.
  # Is a Float only for precision. Must be an integral value.
  transitionsRelativeToEpochMs: Float
}

# Input for the applyEntitlementsSequenceToUsers mutation
input ApplyEntitlementsSequenceToUsersInput {
  operations: [ApplyEntitlementsSequenceToUserInput!]!
}

# Input for the applyEntitlementsToUser mutation
input ApplyEntitlementsToUserInput {
  externalId: String!
  entitlements: [EntitlementInput!]!
}

# Input for the applyEntitlementsToUsers mutation
input ApplyEntitlementsToUsersInput {
  operations: [ApplyEntitlementsToUserInput!]!
}

# Input for the applyExpendableEntitlementsToUser mutation
input ApplyExpendableEntitlementsToUserInput {
  externalId: String!
  expendableEntitlements: [EntitlementInput!]!
  requestId: ID!
}

# Input for the getEntitlementsForUser query
input GetEntitlementsForUserInput {
  externalId: String!
}

# Input for the getEntitlementDefinition query
input GetEntitlementDefinitionInput {
  name: String!
}

# Input of a single transition within an entitlements sequence
input EntitlementsSequenceTransitionInput {
  # Name of entitlements set
  entitlementsSetName: String!

  # ISO8601 period string - if not specified then this transition
  # is the final state for all users on the sequence.
  duration: String
}

# Input for the getEntitlementsSequence query
input GetEntitlementsSequenceInput {
  name: String!
}

# Input for the addEntitlementsSequence mutation
input AddEntitlementsSequenceInput {
  # Name of the entitlements sequence
  name: String!

  # Description of the entitlements sequence
  description: String

  # Sequence of transitions a user will go through in order. Must not be empty.
  transitions: [EntitlementsSequenceTransitionInput!]!
}

# Input for the setEntitlementsSequence mutation
input SetEntitlementsSequenceInput {
  # Name of the entitlements sequence
  name: String!

  # Description of the entitlements sequence
  description: String

  # Sequence of transitions a user will go through in order. Must not be empty.
  transitions: [EntitlementsSequenceTransitionInput!]!
}

# Input for the removeEntitlementsSequence mutation
input RemoveEntitlementsSequenceInput {
  name: String!
}

# Input for the removeEntitledUser mutation
input RemoveEntitledUserInput {
  externalId: String!
}

type Query {
  # Gets an entitlement set.
  getEntitlementsSet(input: GetEntitlementsSetInput!): EntitlementsSet

  # Retrieves all entitlements sets.
  listEntitlementsSets(nextToken: String): EntitlementsSetsConnection!

  # Gets an entitlement sequence.
  getEntitlementsSequence(
    input: GetEntitlementsSequenceInput!
  ): EntitlementsSequence

  # Retrieves all entitlements sequences.
  listEntitlementsSequences(
    nextToken: String
  ): EntitlementsSequencesConnection!

  # Gets an entitlement definition
  getEntitlementDefinition(
    input: GetEntitlementDefinitionInput!
  ): EntitlementDefinition

  # Retrieves all entitlements definitions
  listEntitlementDefinitions(
    limit: Int
    nextToken: String
  ): EntitlementDefinitionConnection!

  # Retrieve effective entitlements for a given external user.
  getEntitlementsForUser(
    input: GetEntitlementsForUserInput!
  ): ExternalEntitlementsConsumption!
}

type Mutation {
  # Adds an entitlement set
  addEntitlementsSet(input: AddEntitlementsSetInput!): EntitlementsSet!

  # Change the entitlements conferred by an entitlements set.
  setEntitlementsSet(input: SetEntitlementsSetInput!): EntitlementsSet!

  # Remove an entitlements set. Any users configured against this entitlements
  # set will become unentitled.
  removeEntitlementsSet(input: RemoveEntitlementsSetInput!): EntitlementsSet

  # Adds an entitlement sequence
  addEntitlementsSequence(
    input: AddEntitlementsSequenceInput!
  ): EntitlementsSequence!

  # Replace the definition of an entitlements sequence
  setEntitlementsSequence(
    input: SetEntitlementsSequenceInput!
  ): EntitlementsSequence!

  # Remove an entitlements sequence. Any users configured against this entitlements
  # sequence will become unentitled.
  removeEntitlementsSequence(
    input: RemoveEntitlementsSequenceInput!
  ): EntitlementsSequence

  # Apply an entitlement set with the specified name to a user.
  applyEntitlementsSetToUser(
    input: ApplyEntitlementsSetToUserInput!
  ): ExternalUserEntitlements!

  # Apply an entitlement set with the specified names to users in bulk.
  # Equivalent to calling applyEntitlementsSetToUser
  # for each operation.
  applyEntitlementsSetToUsers(
    input: ApplyEntitlementsSetToUsersInput!
  ): [ExternalUserEntitlementsResult!]!

  # Apply an entitlement sequence with the specified name to a user.
  applyEntitlementsSequenceToUser(
    input: ApplyEntitlementsSequenceToUserInput!
  ): ExternalUserEntitlements!

  # Apply an entitlement sequence with the specified names to users in bulk.
  # Equivalent to calling applyEntitlementsSequenceToUser
  # for each operation.
  applyEntitlementsSequenceToUsers(
    input: ApplyEntitlementsSequenceToUsersInput!
  ): [ExternalUserEntitlementsResult!]!

  # Apply entitlements to a user without using a named entitlements set.
  applyEntitlementsToUser(
    input: ApplyEntitlementsToUserInput!
  ): ExternalUserEntitlements!

  # Apply entitlements to users without using a named entitlements set.
  # Equivalent to calling applyEntitlementsToUser
  # for each operation.
  applyEntitlementsToUsers(
    input: ApplyEntitlementsToUsersInput!
  ): [ExternalUserEntitlementsResult!]!

  # Apply expendable entitlements to a user
  applyExpendableEntitlementsToUser(
    input: ApplyExpendableEntitlementsToUserInput!
  ): ExternalUserEntitlements!

  # Remove an entitled user. Entitlements and consumption records related
  # to the specified user will be removed.
  removeEntitledUser(input: RemoveEntitledUserInput!): EntitledUser
}
```

## Errors

In general, errors specific to each API defined in the schema will be returned in the HTTP body as GraphQL error (please see [GraphQL specification](https://github.com/graphql/graphql-spec/blob/main/spec/Section%207%20--%20Response.md#:~:text=Error%20result%20format\&text=If%20an%20error%20can%20be%20associated%20to%20a%20particular%20field,caused%20by%20a%20runtime%20error.) for more detail). However, some system wide errors maybe returned via HTTP status:

* 401: This indicates that you are using the wrong API key to access the Graph API.
* 403: This indicates that the access was denied due to a security policy.
* 423: This indicates the system is currently unavailable due to maintenance. &#x20;
* 500: This indicates that there's an internal service error. This maybe transient but it unlikely to be resolved in a short amount of time.
* 503: This indicates the service is temporarily unavailable but it is likely to be restored shortly.


# End-user API

Performing entitlements operations from clients

Sudo Platform end-user clients establish initial entitlements for a user by calling the [`redeemEntitlements`](/guides/entitlements/end-user-api/redeeming-entitlements) API.

Current entitlement information for a user is retrieved by calling the [`getEntitlementsConsumption`](/guides/entitlements/end-user-api/retrieving-entitlements) API.


# Integrate the Entitlements SDK

Integrate the Entitlements SDK into your application

## Prerequisites

1. [Complete the Getting Started Guide](/guides/getting-started)
2. [Integrate the User SDK](/guides/users/integrate-the-user-sdk)

## Get Started

* [Integrate the iOS SDK](#integrate-the-ios-sdk)
* [Integrate the Android SDK](#integrate-the-android-sdk)
* [Integrate the JavaScript SDK](#integrate-the-javascript-sdk)

## Integrate the iOS SDK

To use Entitlements SDK in an iOS app, you need to install the SudoEntitlements package via Swift Package Manager

Open your project settings in XCode, and go to the `Package Dependencies` tab. Click on the `+` sign to add a dependency.&#x20;

Enter the repository URL [`https://github.com/sudoplatform/sudo-entitlements-ios`](https://github.com/sudoplatform/sudo-entitlements-ios) in the top right search box and select the `sudo-entitlements-ios` repository.&#x20;

Select the required version and `Add Package`.&#x20;

Sudo Platform SDKs conform to semantic versioning so in most cases you will leave the Dependency Rule as `Up to Next Major Version` to receive regular updates without introducing any breaking changes.

This will resolve the local package dependency and install the latest version of the Entitlements SDK.

{% hint style="info" %}
In order to instantiate a Entitlements client, make sure you have followed instructions for [Getting Started](/guides/getting-started) and [User SDK](/guides/users/integrate-the-user-sdk) (see [Prerequisites](#prerequisites) above)
{% endhint %}

### Client Initialization

To instantiate a client in your application, add the following

{% tabs %}
{% tab title="Swift" %}

```swift
import SudoEntitlements

let userClient = // ... see "Users" docs

do {
    let entitlementsClient = DefaultSudoEntitlementsClient(
        userClient: userClient
    )
} catch {
    // Handle initialization error. An error might be thrown due to invalid
    // or missing configuration file.
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action**. Be sure to take a look at the [open source iOS sample app](https://github.com/sudoplatform/samples-ios) on Github that our team published as a reference for integrating the Entitlements SDK into your app as quickly and seamlessly as possible.
{% endhint %}

## Integrate the Android SDK

{% tabs %}
{% tab title="Gradle" %}
Add this line to the dependencies section of the app module `build.gradle` and synchronize your project with Android Studio.

```
dependencies {
    implementation 'com.sudoplatform:sudoentitlements:$latest'
}
```

{% hint style="info" %}
In order to instantiate a Entitlements client, make sure you have followed instructions for [Getting Started](https://github.com/sudoplatform/developer-documentation/tree/7df19c270e90d50d373eb3b71cecb2cd68b5b32a/guides/entitlements/getting-started.md) and [User SDK](https://github.com/sudoplatform/developer-documentation/tree/7df19c270e90d50d373eb3b71cecb2cd68b5b32a/guides/entitlements/users/integrate-the-user-sdk.md) (see Prerequisites above)
{% endhint %}
{% endtab %}
{% endtabs %}

### Client Initialization

To instantiate a client in your application, add the following

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val userClient = // ... see "Users" docs

val entitlementsClient = SudoEntitlementsClient.builder()
    .setContext(appContext)
    .setSudoUserClient(userClient)
    .build()
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open source Android sample app](https://github.com/sudoplatform/samples-android) on Github that our team published as a reference for integrating the Entitlements SDK into your app as quickly and seamlessly as possible.
{% endhint %}

## Integrate the JavaScript SDK

The JavaScript SDK is open source and compatible with CommonJS module loading.

{% tabs %}
{% tab title="NPM" %}
Add this line to the `dependencies` section of your application's `package.json` file.

```
dependencies {
  '@sudoplatform/sudo-entitlements':'$latest'
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
In order to instantiate a Entitlements client, make sure you have followed instructions for [Getting Started](https://github.com/sudoplatform/developer-documentation/tree/7df19c270e90d50d373eb3b71cecb2cd68b5b32a/guides/entitlements/getting-started.md) and [User SDK](https://github.com/sudoplatform/developer-documentation/tree/7df19c270e90d50d373eb3b71cecb2cd68b5b32a/guides/entitlements/users/integrate-the-user-sdk.md) (see Prerequisites above)
{% endhint %}

### Client Initialization

To instantiate a client in your application, add the following

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const DefaultSudoEntitlementsClient
  = require('@sudoplatform/sudo-entitlements')
    .DefaultSudoEntitlementsClient
const userClient = // see "Users" docs

const entitlementsClient =
  new DefaultSudoEntitlementsClient(userClient)
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open source JavaScript sample app](https://github.com/sudoplatform/samples-javascript) on Github that our team published as a reference for integrating the Entitlements SDK into your web application as quickly and seamlessly as possible.
{% endhint %}


# Redeeming Entitlements

Entitlements API for redeeming user entitlements

A federated user must be registered with the Sudo Platform entitlements service before they can access other entitled services in their environment. This process is called *redeeming entitlements.*

Entitlement redemption must occur the first time a federated user signs in to the Sudo Platform. However, it is an idempotent operation and subsequent calls will have no effect.

The redemption operation uses information in the user's identity token and takes no parameters.

{% tabs %}
{% tab title="Swift" %}

```swift
  do {
    let redeemed = try await entitlementsClient.redeemEntitlements()
    // Handle success
  } catch {
    // Handle error
  }
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
try {
  val entitlements = entitlementsClient.redeemEntitlements()
  // Handle redeemed entitlements
}
catch (e: SudoEntitlements.EntitlementsException) {
  // Handle error
}
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
try {
  const entitlements = await entitlementsClient.redeemEntitlements()
  // Handle redeemed entitlements
}
catch (err) {
  // Handle error
}
```

{% endtab %}
{% endtabs %}

The `redeemEntitlements` method returns the user's entitlements. See the [API Reference](/guides/entitlements/end-user-api/api-reference) for details of the returned structure.

## Errors

The `redeemEntitlements` API will throw errors in the the following circumstances:

{% tabs %}
{% tab title="Swift" %}

|                          |                                                                                                                                                     |                                                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Condition**            | Action                                                                                                                                              |                                                                                                                                                 |
| `.notSignedIn`           | User was not signed in when `redeemEntitlements` was invoked.                                                                                       | Ensure user is signed in prior to invoking `redeemEntitlements`                                                                                 |
| `.ambiguousEntitlements` | User has been configured to match more than one entitlements set.                                                                                   | Ensure your identity provider has been set up to include claims that in the user's token that will map to exactly one entitlements set.         |
| `.invalidToken`          | The user's token does not contain a claim that can be mapped to an entitlements set, or has the user been pre-configured with initial entitlements. | Ensure your identity provider has been set up to include an expected claim when federating sign-on or pre-configure the user with entitlements. |
| `.serviceError`          | A transient error occurred in the service while redeeming entitlements.                                                                             | Retry the request. If the problem persists, [ask us for help](https://sudoplatform.com/#lets-chat)                                              |
| {% endtab %}             |                                                                                                                                                     |                                                                                                                                                 |

{% tab title="Kotlin" %}
Errors in Kotlin are thrown as exceptions in the `SudoEntitlementsClient.EntitlementsException` class name space.

|                                  |                                                                                                                                                     |                                                                                                                                                 |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Condition**                    | Action                                                                                                                                              |                                                                                                                                                 |
| `NotSignedInException`           | User was not signed in when `redeemEntitlements` was invoked.                                                                                       | Ensure user is signed in prior to invoking `redeemEntitlements`                                                                                 |
| `AmbiguousEmtitlementsException` | User has been configured to match more than one entitlements set.                                                                                   | Ensure your identity provider has been set up to include claims that in the user's token that will map to exactly one entitlements set.         |
| `InvalidTokenException`          | The user's token does not contain a claim that can be mapped to an entitlements set, or has the user been pre-configured with initial entitlements. | Ensure your identity provider has been set up to include an expected claim when federating sign-on or pre-configure the user with entitlements. |
| `UnknownException`               | A transient error occurred in the service while redeeming entitlements.                                                                             | Retry the request. If the problem persists, [ask us for help](https://sudoplatform.com/#lets-chat)                                              |
| {% endtab %}                     |                                                                                                                                                     |                                                                                                                                                 |

{% tab title="JavaScript" %}

|                              |                                                                                                                                                     |                                                                                                                                                 |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Condition**                | Action                                                                                                                                              |                                                                                                                                                 |
| `NotSignedInError`           | User was not signed in when `redeemEntitlements` was invoked.                                                                                       | Ensure user is signed in prior to invoking `redeemEntitlements`                                                                                 |
| `AmbiguousEntitlementsError` | User has been configured to match more than one entitlements set.                                                                                   | Ensure your identity provider has been set up to include claims that in the user's token that will map to exactly one entitlements set.         |
| `InvalidTokenError`          | The user's token does not contain a claim that can be mapped to an entitlements set, or has the user been pre-configured with initial entitlements. | Ensure your identity provider has been set up to include an expected claim when federating sign-on or pre-configure the user with entitlements. |
| `ServiceError`               | A transient error occurred in the service while redeeming entitlements.                                                                             | Retry the request. If the problem persists, [ask us for help](https://sudoplatform.com/#lets-chat)                                              |
| {% endtab %}                 |                                                                                                                                                     |                                                                                                                                                 |
| {% endtabs %}                |                                                                                                                                                     |                                                                                                                                                 |


# Retrieving Entitlements

Entitlements APIs for retrieving user entitlements information

### getEntitlementsConsumption

The user's current entitlement status can be retrieved by calling the `getEntitlementsConsumption` method. This retrieves information about the user's current level of entitlement as well as information about any entitlements consumed.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
  let entitlements = try await entitlementsClient.getEntitlementsConsumption()
  // Handle entitlements
} catch {
  // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
try {
  val consumption = entitlementsClient.getEntitlementsConsumption()
  // Handle consumption
}
catch (e: SudoEntitlements.EntitlementsException) {
  // Handle error
}
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
try {
  const consumption = await entitlementsClient.getEntitlementsConsumption()
  // Handle consumption
}
catch (err) {
  // Handle error
}
```

{% endtab %}
{% endtabs %}

The `getEntitlementsConsumption` method returns an `EntitlementsConsumption` object that specifies both the user's entitlements and any consumption of those entitlements.

Entitlements may be consumed by the user themselves or may be consumed by sub-resources of the user.

For example the `sudoplatform.sudo.max` entitlement controls the maximum number of Sudo Profiles a user may have at any one time. This entitlement is consumed at the user level. Conversely the `sudoplatform.virtual-cards.virtual-cardMaxPerSudo` specifies how many Sudo Virtual Cards may be active for any single Sudo Profile. Each Sudo Profile's consumption of this entitlement is listed separately in the `EntitlementsConsumption` object. Consumers are identified by an `id` (in the case of a Sudo Profile this is the Sudo Profile's ID) and the `issuer` of the ID (`sudoplatform.sudoservice` for Sudo Profile IDs).

## Errors

The `getEntitlementsConsumption` API will throw errors in the the following circumstances:

{% tabs %}
{% tab title="Swift" %}

|                        |                                                                                                                                                         |                                                                                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Exception**          | Condition                                                                                                                                               | Action                                                                                                                                          |
| `.notSignedIn`         | User was not signed in when `getEntitlements` was invoked.                                                                                              | Ensure user is signed in prior to invoking `getEntitlements`                                                                                    |
| `.invalidToken`        | The user's token does not contain a claim that can be mapped to an entitlements set, or the user has not been pre-configured with initial entitlements. | Ensure your identity provider has been set up to include an expected claim when federating sign-on or pre-configure the user with entitlements. |
| `.noEntitlementsError` | The user has not called `redeemEntitlements` yet and so their identity is not bound to any entitlements                                                 | Ensure `redeemEntitlements` has been called prior to attempting to retrieve entitlements consumption information                                |
| `.serviceError`        | A transient error occurred in the service while retrieving entitlements.                                                                                | Retry the request. If the problem persists, [ask us for help](https://sudoplatform.com/#lets-chat)                                              |
| {% endtab %}           |                                                                                                                                                         |                                                                                                                                                 |

{% tab title="Kotlin" %}
Errors in Kotlin are thrown as exceptions in the `SudoEntitlementsClient.EntitlementsException` class name space.

|                           |                                                                                                                                                     |                                                                                                                                                 |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Exception**             | Condition                                                                                                                                           | Action                                                                                                                                          |
| `NotSignedInException`    | User was not signed in when `getEntitlementsConsumption` was invoked.                                                                               | Ensure user is signed in prior to invoking `getEntitlementConsumption`                                                                          |
| `InvalidTokenException`   | The user's token does not contain a claim that can be mapped to an entitlements set, or has the user been pre-configured with initial entitlements. | Ensure your identity provider has been set up to include an expected claim when federating sign-on or pre-configure the user with entitlements. |
| `NoEntitlementsException` | The user has not called `redeemEntitlements` yet and so their identity is not bound to any entitlements                                             | Ensure `redeemEntitlements` has been called prior to attempting to retrieve entitlements consumption information                                |
| `UnknownException`        | A transient error occurred in the service while retrieving entitlements.                                                                            | Retry the request. If the problem persists, [ask us for help](https://sudoplatform.com/#lets-chat)                                              |
| {% endtab %}              |                                                                                                                                                     |                                                                                                                                                 |

{% tab title="JavaScript" %}

|                       |                                                                                                                                                     |                                                                                                                                                 |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Error**             | Condition                                                                                                                                           | Action                                                                                                                                          |
| `NotSignedInError`    | User was not signed in when `getEntitlements` was invoked.                                                                                          | Ensure user is signed in prior to invoking `getEntitlements`                                                                                    |
| `InvalidTokenError`   | The user's token does not contain a claim that can be mapped to an entitlements set, or has the user been pre-configured with initial entitlements. | Ensure your identity provider has been set up to include an expected claim when federating sign-on or pre-configure the user with entitlements. |
| `NoEntitlementsError` | The user has not called `redeemEntitlements` yet and so their identity is not bound to any entitlements                                             | Ensure `redeemEntitlements` has been called prior to attempting to retrieve entitlements consumption information                                |
| `ServiceError`        | A transient error occurred in the service while retrieving entitlements.                                                                            | Retry the request. If the problem persists, [ask us for help](https://sudoplatform.com/#lets-chat)                                              |
| {% endtab %}          |                                                                                                                                                     |                                                                                                                                                 |
| {% endtabs %}         |                                                                                                                                                     |                                                                                                                                                 |


# SDK Releases

Entitlements SDK releases

Use the links below to learn about significant new features and fixes added in each release of the Entitlements SDKs and to access the source code for the SDKs.

## Entitlements SDK for iOS

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-entitlements-ios/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-entitlements-ios/)
* [API Reference](https://sudoplatform.github.io/sudo-entitlements-ios/index.html)

## Entitlements SDK for Android

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-entitlements-android/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-entitlements-android/)
* [API Reference](https://sudoplatform.github.io/sudo-entitlements-android/sudoentitlements/index.html)

## Entitlements SDK for JavaScript

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-entitlements-js/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-entitlements-js/)
* [API Reference](https://sudoplatform.github.io/sudo-entitlements-js/index.html)


# API Reference

Entitlements SDK API reference

The links below provides a detailed API reference for the Entitlements SDK.

* [iOS API Reference](https://sudoplatform.github.io/sudo-entitlements-ios/index.html)
* [Android API Reference](https://sudoplatform.github.io/sudo-entitlements-android/sudoentitlements/index.html)
* [JavaScript API Reference](https://sudoplatform.github.io/sudo-entitlements-js/index.html)


# Sudos

The digital identity for the modern age

A Sudo (pronounced sue-doh) is a digital identity that helps your users protect themselves against the constant risks of privacy threats and security breaches. Sudo digital identities let your users interact with other people and services while protecting against the misuse of their personal data and legal identity information.

We live in a world of data-brokering, targeted ads, and digital profiling. Malicious use of personal information has created distrust and reduced feelings of cyber safety. Sudos give control back to your users, helping protect them so they feel safe online again.

## Key Features

|                      |                                                                                                                                                                                                                                                                          |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Compartmentalization | Your users can create a different Sudo digital identity for each purpose, such as shopping, social media, or dating. This minimizes how much of their personal information is shared with other people or services and makes data aggregation and identity fraud harder. |
| Extensible           | Sudos allow your users to alter any part of their identity, such as phone numbers, email addresses and virtual credit cards. The Sudo Platform makes it incredibly easy to integrate our privacy-preserving capabilities into all your products.                         |
| Disposable           | Your users might have many reasons to want to change their digital identity, including data breaches, unscrupulous data trading, and aggressive marketing. Sudos are designed to be easy to delete or update if needed.                                                  |

## Next Steps

* Integrate the **Sudo SDK** into your [JavaScript](/guides/sudos/integrate-the-sudo-sdk#integrate-the-js-sdk), [iOS](/guides/sudos/integrate-the-sudo-sdk#integrate-the-ios-sdk) and [Android](/guides/sudos/integrate-the-sudo-sdk#integrate-the-android-sdk) applications
* Learn more about [Sudo digital identities](/concepts/sudo-digital-identities) and other core concepts


# Integrate the Sudo Profiles SDK

Integrate the Sudo Profiles SDK into your application

## Prerequisites

1. [Complete the Getting Started Guide](/guides/getting-started)
2. [Integrate the User SDK](/guides/users/integrate-the-user-sdk)

## Get Started

* [Integrate the iOS SDK](#integrate-the-ios-sdk)
* [Integrate the Android SDK](#integrate-the-android-sdk)
* [Integrate the JS SDK](https://docs.sudoplatform.com/guides/sudos/integrate-the-sudo-sdk#integrate-the-js-sdk)

## Integrate the iOS SDK

To use Sudo Profiles SDK in an iOS app, you need to install the SudoProfiles package via Swift Package Manager

Open your project settings in XCode, and go to the `Package Dependencies` tab. Click on the `+` sign to add a dependency.&#x20;

Enter the repository URL [`https://github.com/sudoplatform/sudo-profiles-ios`](https://github.com/sudoplatform/sudo-profiles-ios) in the top right search box and select the `sudo-profiles-ios` repository.&#x20;

Select the required version and `Add Package`.&#x20;

Sudo Platform SDKs conform to semantic versioning so in most cases you will leave the Dependency Rule as `Up to Next Major Version` to receive regular updates without introducing any breaking changes.

This will resolve the local package dependency and install the latest version of the Sudo Profiles SDK.

> Instantiating and initializing a client:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let client: SudoProfilesClient = try DefaultSudoProfilesClient(sudoUserClient: sudoUserClient)
} catch {
    // Handle initialization error. An error might be thrown due to invalid
    // or missing confiugration file.
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

## Integrate the Android SDK

To use Sudo Profiles SDK in an Android app you need to add `SudoProfiles` library as a dependency to your project.

> Edit the app module Gradle script (build.gradle) and add `SudoProfiles` library as a dependency.

{% tabs %}
{% tab title="gradle" %}

```java
dependencies {
    api 'com.sudoplatform:sudoprofiles:$latest_version'
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The latest version of the SDK can be found at [SDK Releases](/guides/sudos/sdk-releases).
{% endhint %}

> Edit AndroidManifest.xml to add the following app permissions:

{% tabs %}
{% tab title="Diff" %}

```diff
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+   xmlns:tools="http://schemas.android.com/tools"
    package="com.sudoplatform.sampleapp">

+   <uses-permission android:name="android.permission.INTERNET" />
+   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+   <uses-permission android:name="android.permission.WAKE_LOCK" />
+   <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
+       tools:replace="android:allowBackup"
+       android:allowBackup="false"
```

{% endtab %}
{% endtabs %}

> Instantiating and initializing a client:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val client = SudoProfilesClient
            .builder(appContext, sudoUserClient, blobContainerUri)
            .build()
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

## Integrate the JS SDK

To use the **Sudo Profiles SDK** in a JS project you must add `@sudoplatform/sudo-profiles` as a dependency to your project

```bash
yarn add '@sudoplatform/sudo-profiles'
# or
npm install --save '@sudoplatform/sudo-profiles'
```

> Instantiating and initializing a Sudo Profiles client:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
import {
  DefaultSudoProfilesClient,
} from '@sudoplatform/sudo-profiles'

// Instantiate and initialize the client
const sudoProfilesClient = new DefaultSudoProfilesClient({
  sudoUserClient: sudoUserClient,
})
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

### Symmetric Key Management for JS SDK

Symmetric keys are used in Sudo Profiles JS SDK to encrypt and decrypt Sudo claims. These keys are not stored within our system and need to be loaded into the Sudo Profile Client to be able to encrypt and decrypt your Sudo claims data. These symmetric keys can also be rotated, therefore a potential list of keys will need to be set. The storage and management of these keys will also be the responsibility of the consumer of the Sudo Profiles JS SDK.

> Adding Symmetric keys to Sudo Profiles Client:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
sudoProfilesClient.pushSymmetricKey('1234', '14A9B3C3540142A11E70ACBB1BD8969F')
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Symmetric keys are loaded into the Sudo Profiles Client after the client has been instantiated and initialized. Any old keys that have been used in the past to encrypt Sudo claims will also need to be set so that those Sudo claims can be decrypted. The last key loaded will be set as the current key for encrypting and decrypting new Sudo claims.

It is a requirement that the Sudo Profiles Client be loaded with at least one symmetric key.
{% endhint %}


# Sudo Entitlements

The Sudo Platform entitlements system is used to control consumption of Sudo Platform resources and the Sudo service integrates with this system.

{% hint style="info" %}
Read more about the Sudo Platform entitlements system and how to integrate with it in the  [Entitlements](/guides/entitlements) section of the Sudo Platform documentation.
{% endhint %}

The following table describes the entitlements used to control access to the Sudo service:

<table><thead><tr><th>Entitlement</th><th width="186.33333333333331">Type</th><th>Description</th></tr></thead><tbody><tr><td>sudoplatform.sudo.max</td><td>Numeric</td><td>Maximum number of Sudos a user can have.</td></tr></tbody></table>


# Manage Sudos

## Create Sudos

> To create a new Sudo:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let sudoInput = SudoCreateInput(
        title: "Mr.", 
        firstName: "John", 
        lastName: "Smith",
        label: "Shopping", 
        notes: "Used for shopping.", 
        avatar: .fileUrl(avatarUrl)
    )
    let sudo = try await client.createSudo(input: sudoInput)
    // Create successful
} catch {
    // Handle error. An error may be thrown if the backend is unable
    // to create the Sudo due to availability, security or entitlement issues or
    // due to unrecoverable circumstances arising from programmatic error or 
    // configuration error. For example, if the keychain access entitlement 
    // is not set up correctly, the client is not signed in,
    // or basic system resources are unavailable.   
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val sudo = Sudo()
sudo.title = "Mr."
sudo.firstName = "John"
sudo.lastName = "Smith"
sudo.label = "Shopping"
sudo.notes = "Used for shopping."
sudo.avatar = avatarUri

CoroutineScope(Dispatchers.IO).launch {
    try {
        client.createSudo(sudo)
        // Sudo created successfully.
    } catch (e: SudoProfileException) {
        // Handle error.
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { Sudo } from '@sudoplatform/sudo-profiles'

const sudo = new Sudo()
sudo.title = "Mr."
sudo.firstName = "John"
sudo.lastName = "Smith"
sudo.label = "Shopping"
sudo.notes = "Used for shopping."
sudo.setAvatar(avatarFile)

try {
  const createdSudo = await client.createSudo(sudo)
  // Sudo created successfully.
} catch(err) {
  // Handle error.  
}
```

{% endtab %}
{% endtabs %}

## List Sudos

> To list Sudos:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let sudos = try await client.listSudos(cachePolicy: .remoteOnly)
    // Download the avatar images.
    for sudo in sudos {
        if let avatarClaim = sudo.avatarClaim {
            let data = try await client.getBlob(forClaim: avatarClaim, cachePolicy: .remoteOnly)
            let image = UIImage(data: data)
        }
    }    
} catch {
    // Handle error. An error may be thrown if the backend is unable
    // to list Sudos due to availability or for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly, the client is not signed in,
    // or basic system resources are unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        val sudos = client.listSudos(ListOption.REMOTE_ONLY)
        for (sudo in sudos) {
            // Load the avatar image.
            val avatar = sudo.avatar
            if (avatar != null) {
                val file = File(avatar)
                val data = file.readBytes()
            }
        }

    } catch (e: SudoProfileException) {
        // Handle error.
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  const sudos = await client.listSudos()
} catch(err) {
  // Handle error.
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The `cachePolicy` parameter specifies whether to return the cached entries or fetch the data from the backend. Initially the cache will be empty but is populated the first time the data is fetched from the backend using `remoteOnly` option. If the app is offline or if it requires a separate action to fetch new data from the backend then use `cacheOnly` option to show the user with the last known state then update the view when the app is able to fetch new data from the backend. create, update and delete APIs can also update the cache but the cache needs to be hydrated first by invoke list API with `remoteOnly.`
{% endhint %}

## Modify Sudos

> To update an existing Sudo:

{% tabs %}
{% tab title="Swift" %}

<pre class="language-swift"><code class="lang-swift"><strong>let updateInput = SudoUpdateInput(
</strong><strong>    sudoId: sudoId, 
</strong><strong>    version: version, 
</strong><strong>    firstName: .newValue("David")
</strong>)
do {
    let updatedSudo = try await client.updateSudo(input: updateInput)
    // Update successful.
} catch {
    // Handle error. An error may be thrown if the backend is unable to
    // update Sudo due to availability or security issues or for unrecoverable 
    // circumstances arising from programmatic error or configuration error. 
    // For example, if the keychain access entitlement is not set up correctly, 
    // the client is not signed in, or basic system resources are unavailable.
}
</code></pre>

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
sudo.firstName = "David"
CoroutineScope(Dispatchers.IO).launch {
    try {
        client.updateSudo(sudo)
        // Sudo updated successfully.
    } catch (e: SudoProfileException) {
        // Handle error.
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
sudo.firstName = "David"
try {
  const updatedSudo = await client.updateSudo(sudo)
  // Sudo updated successfully.
} catch(err) {
  // Handle error.
}
```

{% endtab %}
{% endtabs %}

## Subscribe to Changes

> To subscribe to a specific Sudo change notification:

{% tabs %}
{% tab title="Swift" %}

```swift
class MySubscriber: SudoSubscriber {

    func sudoChanged(changeType: SudoChangeType, sudo: Sudo) {
        // Process new, updated or deleted Sudo.
    }

    func connectionStatusChanged(changeType: SudoChangeType, state: SubscriptionConnectionState) {
        if state == .connected {
            // The subscription for the provided change type is now connected hence the subscribers
            // will start receiving Sudo changes.
        } else if state == .disconnected {
            // The subscription is disconnected and all subscribers for the provided
            // change type were automatically unsubscribed. Subscribe again to establish a 
            // new connection or report the error.
        }
    }

}

let subscriber = MySubscriber()
do {
    try await client.subscribe(id: "subscriber_id", changeTypes: [.create], subscriber: subscriber)
} catch {
    // Handle error. An error might be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly, the client is not signed in,
    // or basic system resources are unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
class MySubscriber: SudoSubscriber {

    override fun connectionStatusChanged(state: SudoSubscriber.ConnectionState) {
        if (state == SudoSubscriber.ConnectionState.CONNECTED) {
            // The subscription is now connected hence the subscribers
            // will start receiving Sudo changes.
        } else {
            // The subscription is disconnected and all subscribers were
            // automatically unsubscribed. Subscribe again to establish a 
            // new connection or report the error.        
        }
    }

    override fun sudoChanged(changeType: SudoSubscriber.ChangeType, sudo: Sudo) {
        // Process new, updated or deleted Sudo.
    }
}

val subscriber = MySubscriber()
CoroutineScope(Dispatchers.IO).launch {
    client.subscribeAsync("<subscriber_id>", SudoSubscriber.ChangeType.CREATE, subscriber)
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { 
  SudoSubscriber, 
  ConnectionState,
  ChangeType, 
  } from '@sudoplatform/sudo-profiles'

class MySubscriber implements SudoSubscriber {
  public sudoChanged(changeType, sudo) {
    // Process new, updated or deleted Sudo.
  }

  public connectionStatusChanged(state) {
    if (state === ConnectionState.Connected) {
        // The subscription is now connected hence the subscribers
        // will start receiving Sudo changes.
    } else {
        // The subscription is disconnected and all subscribers were
        // automatically unsubscribed. Subscribe again to establish a 
        // new connection or report the error.        
    }
  }    
}

try {
  const subscriber = new MySubscriber()
  client.subscribe('subscriber_id', ChangeType.Create, subscriber)
} catch(err) {
  // Handle error.
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For creating a new Sudo, the subscribers will be notified twice: ".create" notification when the Sudo is created and ".update" notification when the Sudo is updated with claims that requires reference to Sudo ID. The request to create a Sudo is considered complete when both of notifications have been received.
{% endhint %}

> To subscribe to Sudo change notification of all types:

{% tabs %}
{% tab title="Swift" %}

```swift
class MySubscriber: SudoSubscriber {

    func sudoChanged(changeType: SudoChangeType, sudo: Sudo) {
        // Process new, updated or deleted Sudo.
    }

    func connectionStatusChanged(changeType: SudoChangeType, state: SubscriptionConnectionState) {
        if state == .connected {
            // The subscription is now connected hence the subscribers
            // will start receiving Sudo changes.
        } else if state == .disconnected {
            // The subscription is disconnected and all subscribers were
            // automatically unsubscribed. Subscribe again to establish a 
            // new connection or report the error.
        }
    }

}

let subscriber = MySubscriber()
do {
    try await client.subscribe(id: "subscriber_id", subscriber: subscriber)
} catch {
    // Handle error. An error may be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, if the keychain
    // access entitlement is not set up correctly, the client is not signed in,
    // or basic system resources are unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
class MySubscriber: SudoSubscriber {

    override fun connectionStatusChanged(state: SudoSubscriber.ConnectionState) {
        if (state == SudoSubscriber.ConnectionState.CONNECTED) {
            // The subscription is now connected hence the subscribers
            // will start receiving Sudo changes.
        } else {
            // The subscription is disconnected and all subscribers were
            // automatically unsubscribed. Subscribe again to establish a 
            // new connection or report the error.        
        }
    }

    override fun sudoChanged(changeType: SudoSubscriber.ChangeType, sudo: Sudo) {
        // Process new, updated or deleted Sudo.
    }
}

val subscriber = MySubscriber()
CoroutineScope(Dispatchers.IO).launch {
    client.subscribeAsync("<subscriber_id>", subscriber)
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { 
  SudoSubscriber, 
  ConnectionState,
  ChangeType, 
  } from '@sudoplatform/sudo-profiles'

class MySubscriber implements SudoSubscriber {
  public sudoChanged(changeType, sudo) {
    // Process new, updated or deleted Sudo.
  }

  public connectionStatusChanged(state) {
    if (state === ConnectionState.Connected) {
        // The subscription is now connected hence the subscribers
        // will start receiving Sudo changes.
    } else {
        // The subscription is disconnected and all subscribers were
        // automatically unsubscribed. Subscribe again to establish a 
        // new connection or report the error.        
    }
  }    
}

try {
  const subscriber = new MySubscriber()
  client.subscribeAll('subscriber_id', subscriber)
} catch(err) {
  // Handle error.
}
```

{% endtab %}
{% endtabs %}

## Delete Sudos

> To delete an existing Sudo:

{% tabs %}
{% tab title="Swift" %}

```swift
let input = SudoDeleteInput(sudoId: sudoId, version: version)
do {
    try await client.deleteSudo(input: input) 
    // Delete successful.
} catch {
    // Handle error. An error might be thrown if the backend is unable 
    // to delete the Sudo due to availability or security issues or for 
    // unrecoverable circumstances arising from programmatic error or 
    // configuration error. For example, if the keychain access entitlement is 
    // not set up correctly, the client is not signed in, or basic system 
    // resources are unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        client.deleteSudo(sudo)
        // Sudo deleted successfully.
    } catch (e: SudoProfileException) {
        // Handle error.
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  await client.deleteSudo(sudo)
  // Sudo deleted successfully.
} catch(err) {
  // Handle error.
}
```

{% endtab %}
{% endtabs %}

## Get Ownership Proof

Many Sudo Platform services require the user to present a proof that they own a particular Sudo so that a provisioned resource can be associated with a Sudo. Sudo Profiles SDK provides an API to generate a cryptographic proof of Sudo ownership so it can be used in provisioning APIs of other SDKs such as Email SDK.

> To obtain a Sudo ownership proof for provisioning a resource tied to a Sudo.

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let jwt = try await client.getOwnershipProof(sudo: sudo, audience: "<service_specific_audience>")
    // Ownership proof obtained successfully.
} catch {
    // Handle error. An error might be thrown if the backend is unable
    // to delete the Sudo due to availability or security issues or for 
    // unrecoverable circumstances arising from programmatic error or 
    // configuration error. For example, if the keychain access entitlement is 
    // not set up correctly, the client is not signed in, or basic system 
    // resources are unavailable.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
CoroutineScope(Dispatchers.IO).launch {
    try {
        client.getOwnershipProof(sudo, "<service_specific_audience>")
        // Ownership proof obtained successfully.
    } catch (e: SudoProfileException) {
        // Handle error.
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  await client.getOwnershipProof(sudoId, "<service_specific_audience>")
  // Ownership proof obtained successfully.
} catch(err) {
  // Handle error.
}
```

{% endtab %}
{% endtabs %}

## Resetting Client State

You can reset the internal state information maintained by `SudoProfilesClient` by calling `reset` API.

{% hint style="danger" %}
Resetting the client state will cause all persistent data to be lost including any cached Sudos and encryption keys. Unless you are intending to import the encryption keys from an external source, e.g. key backup, you should call `generateEncryptionKey` API to create a new encryption key before using `SudoProfileClient` again.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    try await client.reset()
} catch {
    // Handle error. An error may be thrown for unrecoverable circumstances arising
    // from programmatic error or configuration error. For example, basic system resources
    // are not accessible.
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
client.reset()
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
try {
  await client.reset()
} catch(err) {
  // Handle error. An error might be thrown for unrecoverable circumstances arising
  // from programmatic error or configuration error. For example, basic system resources
  // are not accessible.
}
```

{% endtab %}
{% endtabs %}


# SDK Releases

Additional resources for the Sudo Profiles SDKs

Use the links below to learn about significant new features and fixes added in each release of the Sudo Profiles SDKs and to access the source code for the SDKs and sample apps.

## Sudo Profiles SDK for iOS

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-profiles-ios/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-profiles-ios)
* [Sample app source code](https://github.com/sudoplatform/samples-ios/tree/master/telephony)

## Sudo Profile SDK for Android

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-profiles-android/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-profiles-android)
* [Sample app source code](https://github.com/sudoplatform/samples-android)

## Sudo Profiles SDK for JS

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-profiles-js/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-profiles-js)
* Sample app source code (*coming soon*)


# API Reference

API references for the Sudo Profiles SDKs

The links below provides a detailed API reference for the Sudo Profiles SDKs.

* [iOS API Reference](https://sudoplatform.github.io/sudo-profiles-ios/index.html)
* [Android API Reference](https://sudoplatform.github.io/sudo-profiles-android/)
* [JS API Reference](https://sudoplatform.github.io/sudo-profiles-js/index.html)


# Telephony

Unlock the power of phone numbers within your products.

The **Telephony SDK** allows you to offer your users all the features associated with a mobile phone number while preserving their privacy. The Sudo Platform makes it effortless to integrate phone numbers, messaging, and calling within your existing or new products. This enables your users to use different phone numbers for different situations, contexts or roles. In turn, this compartmentalization of their personal information helps keep them safe online.

Mobile phones have become so ubiquitous that a phone number is now synonymous with a user's identity. More and more services and applications require a user to share their phone number alongside their other personal data. This coupling of a phone number with personal identity carries significant privacy and security implications. If these are left unchecked, they expose users to significant threats such as identity theft, social engineering, SIM swapping, account takeovers, doxing, and more.

## Key Features

|                                                               |                                                                                                                                                                         |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Phone Numbers](/guides/telephony/phone-numbers)              | Give your users their own phone numbers that work just like mobile number but with the privacy and security benefits of a Sudo.                                         |
| [SMS Messages](/guides/telephony/text-messaging#sms-messages) | Quickly and easily add secure SMS messaging to any app, giving your users the ability to send messages to or receive messages from any phone number.                    |
| [MMS Messages](/guides/telephony/text-messaging#mms-messages) | Allow your users to send multimedia messages such as photos, GIFs, videos, contact cards and more.                                                                      |
| [Voice Calling](/guides/telephony/calling)                    | Provide your users with the ability to make and receive voice calls to mobile, toll-free, landline, and VOIP phone numbers without revealing their device's SIM number. |

## Next Steps

* Integrate the **Telephony SDK** into your [iOS](/guides/telephony/integrate-the-telephony-sdk#integrate-the-ios-sdk) and [Android](/guides/telephony/integrate-the-telephony-sdk#integrate-the-android-sdk) applications
* Learn more about [Sudo digital identities](/concepts/sudo-digital-identities) and other core concepts


# Integrate the Telephony SDK

Integrate the Telephony SDK into your application

## Prerequisites

1. [Complete the Getting Started Guide](/guides/getting-started)
2. [Integrate the User SDK](/guides/users/integrate-the-user-sdk)
3. [Integrate the Sudo SDK](/guides/sudos/integrate-the-sudo-sdk)

## Get Started

* [Integrate the iOS SDK](#integrate-the-ios-sdk)
* [Integrate the Android SDK](#integrate-the-android-sdk)
* Integrate the Web SDK (Coming soon!)

## Integrate the iOS SDK

Add this line to your [Podfile](/guides/getting-started#ios-only-setup-cocoapods-in-a-project) near the line `pod 'SudoUser'`

```
pod 'SudoTelephony'
```

Install pod dependencies by running the following command in your project directory:

```
pod install --repo-update
```

This will update the local CocoaPods repository and install the latest version of the Telephony SDK.

{% hint style="info" %}
In order to instantiate a Telephony client, make sure you have followed instructions for [Getting Started](/guides/getting-started), [User SDK](/guides/users/integrate-the-user-sdk) and [Sudo SDK](/guides/sudos/integrate-the-sudo-sdk) (see [Prerequisites](#prerequisites) above)
{% endhint %}

To instantiate a client in your application, add the following

{% tabs %}
{% tab title="Swift" %}

```swift
import SudoTelephony

let userClient = // ... see "Users" docs
let profilesClient = // ... see "Sudos" docs
let callingProviderConfig = // .. Optional. see "Voice Call" docs
let telephonyClient = try! DefaultSudoTelephonyClient(
    sudoUserClient: userClient, 
    sudoProfilesClient: profilesClient,
    callProviderConfiguration: callingProviderConfig
)
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
If you are integrating [voice calling](/guides/telephony/calling) into your application, you must pass in a CallProviderConfiguration. Failure to do so will result in errors being returned when attempting to make a voice call.
{% endhint %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open source iOS sample app](https://github.com/sudoplatform/samples-ios) on GitHub that our team published as a reference for integrating the Telephony SDK into your app as quickly and seamlessly as possible.
{% endhint %}

## Integrate the Android SDK

Add this line to the dependencies section of the app module `build.gradle` and run gradle sync

```
dependencies {
    api 'com.sudoplatform:sudotelephony:$latest_version'
}
```

{% hint style="info" %}
The latest version of the SDK can be found at [SDK Releases](/guides/telephony/sdk-releases).
{% endhint %}

{% hint style="info" %}
In order to instantiate a Telephony client, make sure you have followed instructions for [Getting Started](/guides/getting-started), [User SDK](/guides/users/integrate-the-user-sdk) and [Sudo SDK](/guides/sudos/integrate-the-sudo-sdk) (see [Prerequisites](#prerequisites) above).
{% endhint %}

To instantiate a client in your application, add the following

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val userClient = // ... see "Users" docs
val profilesClient = // ... see "Sudos" docs

val telephonyClient = DefaultSudoTelephonyClient(
    appContext, 
    userClient, 
    profilesClient
)
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open source Android sample app](https://github.com/sudoplatform/samples-android/tree/master/telephony) on GitHub that our team published as a reference for integrating the Telephony SDK into your app as quickly and seamlessly as possible.
{% endhint %}


# Manage Phone Numbers

Phone numbers are a central part of the **Telephony SDK**. Phone numbers from different countries around the world can be provisioned and used to to communicate with other phone numbers.

Provisioning a phone number is a two-step process. First, a search must be performed for an available phone number using a specific country code. After the desired phone number is selected, the phone number can be provisioned to a Sudo.

## Get Supported Countries

To get the list of supported countries, use the `getSupportedCountries()` method. The country code format currently supported is the two-letter format described in [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).

In sandbox mode, the "ZZ" country code is included to allow provisioning simulated phone numbers. Refer to the [Telephony Simulator](/guides/telephony/simulator-and-the-zz-country-code) page for details.

{% tabs %}
{% tab title="Swift" %}

```swift
try! telephonyClient.getSupportedCountries { result in
    switch result {
    case .success(let countries):
        // countries: list of strings containing the country ISO codes
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
telephonyClient.getSupportedCountries { result ->
    when (result) {
        is Result.Success -> {
            // result.value.countries: list of strings containing 
            // the country ISO codes
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Each project in your Sudo Platform account can be configured to support a specific set of country codes. Your solutions engineer can assist you in selecting the desired country codes.
{% endhint %}

## Search Available Phone Numbers

A search for a phone number can be performed using either a country code, a country code plus phone number prefix (e.g., in the United States the prefix is called an "area code") or a latitude and longitude. A limit can be provided to ensure a maximum number of results are returned.

### By Country Only

To search for a phone number by country code, use the `searchAvailablePhoneNumbers()` method and provide the two-character ISO country code.

{% tabs %}
{% tab title="Swift" %}

```swift
// By country code
try! telephonyClient.searchAvailablePhoneNumbers(countryCode: "US", 
                                                 limit: 5) { (result) in
    switch result {
    case .success(let searchResult):
        // searchResult: AvailablePhoneNumberResult (see below for details) 
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// By country code 

// val countryCode = "US"
telephonyClient.searchAvailablePhoneNumbers(countryCode) { result ->
    when (result) {
        is Result.Success -> {
            // result.value.numbers: PhoneNumberSearchResult
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

### By Country and Prefix

To search for a phone number by country code and prefix, use the `searchAvailablePhoneNumbers()` method and provide the two-character country code and prefix digits.

{% tabs %}
{% tab title="Swift" %}

```swift
//By area code
try! telephonyClient.searchAvailablePhoneNumbers(countryCode: "US", 
                                                 prefix: "385",
                                                 limit: nil) { (result) in
    switch result {
    case .success(let searchResult):
        // searchResult: AvailablePhoneNumberResult (see below for details) 
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// By area code 

// val countryCode = "US"
// val areaCode = "385"
telephonyClient.searchAvailablePhoneNumbers(countryCode, areaCode) { result ->
    when (result) {
        is Result.Success -> {
            // result.value.numbers: PhoneNumberSearchResult (see below for details) 
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

### By Latitude and Longitude

To search for a phone number by country code and latitude/longitude, use the `searchAvailablePhoneNumbers()` method and provide the two-character country code and latitude/longitude. This function finds geographically close numbers within miles. Applies to only phone numbers in the US and Canada.

{% tabs %}
{% tab title="Swift" %}

```swift
// By lat/lng

// let country: String = "US"
// let lat: Double = 37.234332396
// let lng: Double = -115.80666344
try! client.searchAvailablePhoneNumbers(countryCode: country,
                                        latitude: lat,
                                        longitude: lng,
                                        limit: 5) { result in
    switch result {
    case .success(let searchResult):
        // searchResult: AvailablePhoneNumberResult (see below for details) 
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// By lat/lng

// val countryCode = "US"
// val latitude = "37.234332396"
// val longitude = "-115.80666344"
telephony.searchAvailablePhoneNumbers(countryCode, 
                                      latitude, 
                                      longitude) { result ->
    when (result) {
        is Result.Success -> {
            // result.value.numbers: PhoneNumberSearchResult (see below for details) 
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }

    this.latch.countDown()
}
```

{% endtab %}
{% endtabs %}

### Search Result

Both calls to `searchAvailablePhoneNumbers()` return a `AvailablePhoneNumberResult` (iOS) / `PhoneNumberSearchResult` (Android) on success. The result contains a list of available numbers and the country code / prefix used in the search.

The Telephony SDK uses the [E.164 phone number format](https://en.wikipedia.org/wiki/E.164) for all string representations of phone numbers.

{% tabs %}
{% tab title="Swift" %}

```swift
struct AvailablePhoneNumberResult {
    let numbers: [String] // List of numbers available in E.164 format
    let countryCode: String // Country code used for search
    let prefix: String? // Prefix used for search
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
data class PhoneNumberSearchResult (val id: String,
                                    val numbers: List<String>,
                                    val countryCode: String,
                                    val gps: AvailablePhoneNumberResult.Gps?,
                                    val prefix: String?,
                                    val state: PhoneNumberSearchState)
```

{% endtab %}
{% endtabs %}

## Provision Phone Number

After an available phone number is found, it can be provisioned to a Sudo using the `provisionPhoneNumber()` method. Provisioning a phone number to a Sudo makes it available to communicate with other phone numbers, such as sending and receiving SMS and MMS messages.

The Telephony SDK uses the [E.164 phone number format](https://en.wikipedia.org/wiki/E.164) for all string representations of phone numbers and this format is expected when provisioning a number.

{% tabs %}
{% tab title="Swift" %}

```swift
try! telephonyClient.provisionPhoneNumber(countryCode: "US", 
                                          phoneNumber: "+442071838750", 
                                          sudoId: "abc123") { result in
    switch result {
    case .success(let number):
        // number: PhoneNumber (see below for details)
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val countryCode = "US"
// val phoneNumber = "+442071838750"
// val sudoID = "abc123"
telephonyClient.provisionPhoneNumber(countryCode, phoneNumber, sudoID) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: PhoneNumber (see below for details)
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

### Phone Number

The `PhoneNumber` type is returned when a phone number is provisioned. This type is expected in other places where a phone number is used, such as sending an SMS/MMS message.

{% tabs %}
{% tab title="Swift" %}

```swift
public struct PhoneNumber {
    public enum State: Equatable {
        case complete
        case provisioning
        case deprovisioning
        case failed
        case unknown
    }

    public let id: String
    public let phoneNumber: String
    public let state: State
    public let version: Int
    public let created: Date
    public let updated: Date
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
data class PhoneNumber(
    val id: String,
    val phoneNumber: String,
    val state: PhoneNumberState,
    val version: Int,
    val created: Date,
    val updated: Date
)

public enum PhoneNumberState {
  PROVISIONING,
  FAILED,
  COMPLETE,
  DEPROVISIONING
}
```

{% endtab %}
{% endtabs %}

### Entitlements

Each user is allocated a limited number of phone numbers per Sudo. The current limit is 1 number per Sudo. In the future this value will be configurable from the Admin Console. If an attempt is made to provision a number in excess of the entitlements, and error will be returned from the SDK.

## De-Provision Phone Number

A previously provisioned phone number can be de-provisioned by using the `deletePhoneNumber()` method. This method expects an [E.164 phone number format](https://en.wikipedia.org/wiki/E.164) formatted phone number string. The phone number must have already been provisioned for this to succeed.

{% tabs %}
{% tab title="Swift" %}

```swift
try! telephonyClient.deletePhoneNumber(phoneNumber: "+442071838750") { result in
    switch result {
    case .success(let phoneNumber):
        // phoneNumber: E.164 formatted number
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val phoneNumber: PhoneNumber
telephonyClient.deletePhoneNumber(phoneNumber.number) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: PhoneNumber
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

## Retrieving Existing Phone Numbers

Previously provisioned numbers can be retrieved using a phone number identifier or by retrieving all provisioned numbers.

### Using Phone Number ID

You can retrieve a previously provisioned phone numbers by using the `getPhoneNumber()` method.

{% tabs %}
{% tab title="Swift" %}

```swift
// let phoneNumber: PhoneNumber
try! telephonyClient.getPhoneNumber(id: phoneNumber.id) { (result) in
    switch result {
    case .success(let number):
        // number: PhoneNumber
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val phoneNumber: PhoneNumber
telephonyClient.getPhoneNumber(phoneNumber.id) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: PhoneNumber
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

### All Provisioned Phone Numbers

To retrieve all provisioned phone numbers, use the `listPhoneNumbers()` method. This method supports paging using the `limit` and `nextToken` parameters, and optionally filters by sudo id.

{% tabs %}
{% tab title="Swift" %}

```swift
let sudoID: String
try! telephonyClient.listPhoneNumbers(sudoId: sudoId, limit: nil, nextToken: nil) { (result) in
    switch result {
    case .success(let listToken):
        // listToken: TelephonyListToken (see below for details)
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val sudoID = "abc123"
telephonyClient.listPhoneNumbers(sudoId, null, null) { result ->
    when(result) {
        is Result.Success -> {
            // result.value: TelephonyListToken<PhoneNumber>
        }
        is Result.Error -> {
            // result.throwable: telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

The result of the `listPhoneNumbers()` call is a TelephonyListToken as described below.

### Retrieve Multiple Phone Numbers Result

When multiple phone numbers are retrieved, for example through the `getPhoneNumbers()` method, a list token object is returned which includes a list of phone numbers and a paging token used to retrieve the next set of results.

{% tabs %}
{% tab title="Swift" %}

```swift
struct TelephonyListToken {
    let items: [PhoneNumber] // Phone numbers
    let nextToken: String? // Reference for next page

}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
data class TelephonyListToken<PhoneNumber>(
    val items: List<PhoneNumber>,
    val nextToken: String?
)
```

{% endtab %}
{% endtabs %}


# Text Messaging

## Sending Messages

The Telephony SDK can handle sending of SMS and MMS messages. Sending messages requires a provisioned phone number, discussed in the phone numbers section of the documentation.

Refer to the [Telephony Simulator](/guides/telephony/simulator-and-the-zz-country-code) page for details on sending messages from simulated phone numbers.

### SMS Messages

SMS messages can be sent using the `sendSMSMessage()` method. This method takes a local number, which must be a `PhoneNumber`, an E.164 formatted remote number and a message body.

{% tabs %}
{% tab title="Swift" %}

```swift
let localNumber: PhoneNumber
try! telephonyClient.sendSMSMessage(localNumber: localNumber, 
    remoteNumber: "+442071838750" , body: "Hi there") { result in
    switch result {
    case .success(let message):
        // message: PhoneMessage (see details below)
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val phoneNumber: PhoneNumber
// val destinationPhoneNumber = "+442071838750"
// val message = "Hi there"
telephonyClient.sendSMSMessage(phoneNumber, 
                               destinationPhoneNumber, 
                               message) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: PhoneMessage
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

The result of successfully sending a message is a `PhoneMessage` as described below.

### MMS Messages

MMS messages can be sent using the `sendMMSMessage()` method. This is very similar to the method used for SMS but with an additional parameter `localUrl: URL` for the path to the MMS media file to be sent with the message.

{% tabs %}
{% tab title="Swift" %}

```swift
let localNumber: PhoneNumber
let media = URL(string: "path to media")!
try! telephonyClient.sendMMSMessage(localNumber: localNumber, 
                                    remoteNumber: "+442071838750", 
                                    body: "Check out my new car!", 
                                    localUrl: media) { result in
    switch result {
    case .success(let message):
        // message: PhoneMessage (see details below)
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val phoneNumber: PhoneNumber
// val destinationPhoneNumber = "+442071838750"
// val message = "Hi there"
// val localUrl: URL
telephonyClient.sendMMSMessage(phoneNumber, 
                               destinationPhoneNumber, 
                               message, 
                               localUrl) { result -> 
    when (result) {
        is Result.Success -> {
            // result.value: PhoneMessage (see details below)
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

The result of successfully sending a message is a `PhoneMessage`, described below.

## Phone Message

SMS and MMS messages are represented in the Telephony SDK by a `PhoneMessage` as described below:

{% tabs %}
{% tab title="Swift" %}

```swift
struct PhoneMessage {

    /// The direction of the message
    enum Direction {
        case inbound
        case outbound
        case unknown
    }

    /// The state of the message
    enum State {
        case queued
        case sent
        case delivered
        case undelivered
        case failed
        case received
        case unknown
    }

    let id: String // Unique ID of message
    let owner: String // Unique ID of message owner
    let conversation: String // Unique ID of the conversation this message is part of
    let updated: Date // Date/time message was last updated
    let created: Date // Date/time message was created
    let localPhoneNumber: String // E.164 formatted local phone number
    let remotePhoneNumber: String // E.164 formatted remote phone number
    let body: String // body of message
    let direction: Direction // Direction of message (see enum above)
    let state: State // State of message (see enum above)
    let media: [MediaObject] // Media attachements for MMS messages
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
enum MessageDirection {
  INBOUND
  OUTBOUND
}

enum MessageState {
  QUEUED
  SENT
  DELIVERED
  UNDELIVERED
  FAILED
  RECEIVED
}

data class PhoneMessage(
    val id: String,
    val owner: String,
    val conversation: String,
    val updated: Instant,
    val created: Instant,
    val local: String,
    val remote: String,
    val body: String,
    val direction: MessageDirection,
    val state: MessageState,
    val media: List<MediaObject> = emptyList()
)
```

{% endtab %}
{% endtabs %}

## Getting Messages

To retrieve SMS and MMS messages, including both sent and received messages, use the `getMessage()` and `getMessages()` methods. Messages can be retrieved using either a message ID or the local and remote phone numbers.

### Single Message using ID

To retrieve a single message, use the `getMessage()` method with the message ID. This returns a `PhoneMessage` object as described above.

{% tabs %}
{% tab title="Swift" %}

```swift
try! telephonyClient.getMessage(id: "abc123") { (result) in
    switch result {
    case .success(let message):
        // message: PhoneMessage
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val message: PhoneMessage
telephonyClient.getMessage(message.id) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: PhoneMessage (see details below)
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

### Multiple Messages using a Local and Remote Number

To retrieve multiple messages for a specific local and remote number, use the `getMessages()` method. A limit can be specified for paging, as well as a paging token.

{% tabs %}
{% tab title="Swift" %}

```swift
let localNumber: PhoneNumber
try! telephonyClient.getMessages(localNumber: localNumber, 
    remoteNumber: "+442071838750", limit: 20, nextToken: nil) { (result) in
    switch result {
    case .success(let listToken):
        // listToken: TelephonyListToken<PhoneMessage> (see details below)
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val phoneNumber: PhoneNumber
// val destinationPhoneNumber = "+442071838750"
// val limit: Int = 20
// val nextToken: String? = null
telephonyClient.getMessages(phoneNumber,
                           destinationPhoneNumber,
                           limit,
                           nextToken) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: TelephonyListToken<PhoneMessage>
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

The result of the `getMessages()` call is a `TelephonyListToken` as described below.

### Get Messages Result

When multiple messages are retrieved, for example through the `getMessages()` method, a list token object is returned which includes a list of results and a paging token that can be used to retrieve the next set of results.

{% tabs %}
{% tab title="Swift" %}

```swift
struct TelephonyListToken<PhoneMessage> {
    let items: [PhoneMessage] // Phone messages
    let nextToken: String? // Reference for next page
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
data class TelephonyListToken<PhoneMessage>(
    val items: List<PhoneMessage>,
    val nextToken: String?
)
```

{% endtab %}
{% endtabs %}

## Conversations

A *conversation* is a convenient way to retrieve all messages between a specific local number and a specific remote number, sometimes referred to in outside contexts as a message thread or chat history. There are three methods for fetching conversations using different parameters.

**Conversation by ID**

To retrieve a conversation by ID, use the `getConversation(conversationId: String)` method.

{% tabs %}
{% tab title="Swift" %}

```swift
try! telephonyClient.getConversation(conversationId: "1") { (result) in
    switch result {
    case .success(let conversation):
        // conversation: PhoneMessageConverastion
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val id = "abc"
telephonyClient.getConversation(id) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: PhoneMessageConversation
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

**Conversation by local and remote number**

To retrieve a conversation using the local and remote number, use the `getConversation(localNumber: PhoneNumber, remoteNumber: String)` method.

{% tabs %}
{% tab title="Swift" %}

```swift
let localNumber: PhoneNumber
try! self.getConversation(localNumber: localNumber, remoteNumber: "+1") { (result) in
    switch result {
    case .success(let conversation):
        // conversation: PhoneMessageConverastion
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val localNumber: PhoneNumber
// val remoteNumber = "+442071838750"
telephonyClient.getConversation(localNumber, remoteNumber) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: PhoneMessageConversation
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

### Conversations by local number

To retrieve the list of conversations for a given local number, use the `getConversations` method. A limit can be specified for paging, as well as a paging token.

{% tabs %}
{% tab title="Swift" %}

```swift
let localNumber: PhoneNumber
try! telephonyClient.getConversations(localNumber: localNumber, 
                                      limit: 20, 
                                      nextToken: nil) { (result) in
    switch result {
    case .success(let listToken):
        // listToken: TelephonyListToken<PhoneMessageConversation>
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val localNumber: PhoneNumber
// val limit = 20
// val nextToken = null
telephonyClient.getConversations(localNumber, limit, nextToken) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: TelephonyListToken<PhoneMessageConversation>
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

The `PhoneMessageConversation` type contains a `latestPhoneMessage` property which can be used to retrieve the local and remote phone numbers involved in the conversation, as well as to display a preview of the conversation contents.

{% tabs %}
{% tab title="Swift" %}

```swift
struct PhoneMessageConversation {
    let id: String // Unique id of conversation
    let latestPhoneMessage: PhoneMessage? // Latest sent/received message in conversation
    public let type: MessageConversationType // `MessageConversationType` of the conversation
    public let latestMessageId: String // ID of the latest message in the conversation
    public var latestPhoneMessage: PhoneMessage? // Latest phone message
    public let created: Date // Creation date of the conversation
    public let updated: Date // Date of the last modification to the conversation
}

struct TelephonyListToken<PhoneMessageConversation> {
    let items: [PhoneMessageConversation] // Conversations
    let nextToken: String? // Reference for next page
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
data class PhoneMessageConversation (
    val id: String, // Unique ID of the conversation
    val owner: String, // ID of the owner of the message
    val type: MessageConversationType, // `MessageConversationType` of the conversation
    val latestMessageId: String, // ID of the latest message in the conversation
    var latestPhoneMessage: PhoneMessage?, // Latest phone message
    val created: Date, // Creation date of the conversation
    val updated: Date // Date of the last modification to the conversation
)

data class TelephonyListToken<PhoneMessageConversation>(
    val items: List<PhoneMessageConversation>, // Conversations
    val nextToken: String? // Reference for next page
)
```

{% endtab %}
{% endtabs %}

### Downloading MMS Message Media

When MMS messages are retrieved, the media data is not automatically downloaded. This gives you control over performance when retrieving MMS messages, particularly those with large media attachments.

To download MMS message media, use the `downloadData()` method, passing it a `MediaObject` which can be found in the `media` property of an MMS message. This method returns the raw media data. Images (jpeg, png, gif) are currently the only supported message media type.

{% tabs %}
{% tab title="Swift" %}

```swift
let message: PhoneMessage
let mediaItem = message.media[0]
try! telephonyClient.downloadData(for: mediaItem) { (result) in
    switch result {
    case .success(let data):
        // Convert data to image for display
        let image = UIImage(data: data)
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val mediaItem: MediaObject
telephonyClient.downloadData(mediaItem) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: ByteArray
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

## Deleting Messages

To delete a message, the `deleteMessage()` method is used and requires the message ID.

{% tabs %}
{% tab title="Swift" %}

```swift
try! telephonyClient.deleteMessage(id: "1234") { result in
    switch result {
    case .success(let id):
        // id: Message ID string
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val message: PhoneMessage
telephonyClient.deleteMessage(message.id) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: Message ID string
        }
        is Result.Error -> {
            // result.throwable: com.anonyome.telephonysdk.Result.Error
        }
    }
}
```

{% endtab %}
{% endtabs %}

## Subscribing to Message Events

To subscribe to new messages and message status change events, use the `subscribeToMessages` method. When new messages or message updates occur, the result handler or subscriber you provide will be called with the latest version of the message. On iOS this method returns a `SubscriptionToken` that cancels the subscription when it's released. On Android a unique ID is passed in to identify the subscriber and can be passed in to the `unsubscribeFromPhoneMessages(id: String?)` method on Android.

{% tabs %}
{% tab title="Swift" %}

```swift
let token = try! telephonyClient.subscribeToMessages { (result) in
    switch result {
    case .success(let message):
        // message: PhoneMessage
    case .failure(let error):
        // error: SudoTelephonyClientError, if an error occurred processing subscription data
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// val subscriber: PhoneMessageSubscriber
// val subscriberId: String
telephonyClient.subscribeToMessages(subscriber, subscriberId)

// PhoneMessageSubscriber definition
interface PhoneMessageSubscriber : TelephonySubscriber {
    /**
     * Notifies the subscriber of a new 'PhoneMessage'.
     * @param phoneMessage new `PhoneMessage`.
     */
    fun phoneMessageReceived(phoneMessage: PhoneMessage)
}
```

{% endtab %}
{% endtabs %}


# Voice Calling

## Voice Calling Setup

{% tabs %}
{% tab title="iOS" %}
**Configure your Xcode project for outgoing calls.**

* In your app's `info.plist`, add the `NSMicrophoneUsageDescription` key. This is required to request microphone permission.
* In your app's capabilities, add the background mode capabilities `Audio, Airplay, and Picture in Picture` ,`Voice over IP`, and `Remote Notifications`
* Add the `Push Notifications` capability.
  {% endtab %}

{% tab title="Android" %}
**Configure your Android application for outgoing calls.**

* Add the following to your `Android Manifest` file:

  ```
  <uses-permission android:name="android.permission.RECORD_AUDIO"/>
  ```
* Request microphone permissions from within your application code:

  ```
  ActivityCompat.requestPermissions(
      this, 
      arrayOf(Manifest.permission.RECORD_AUDIO), 
      MIC_PERMISSION_REQUEST_CODE // a code you set for monitoring whether or not the permission was granted
  )
  ```

**Configure your Android application for incoming calls.**

* Incoming calls take advantage of Firebase Cloud Messaging. To get started with Cloud Messaging follow the Firebase set up instructions [here](https://firebase.google.com/docs/cloud-messaging/android/client). *\*\**
  {% endtab %}
  {% endtabs %}

## Making an Outgoing Call

Outgoing calls are made using the `createVoiceCall` method. The active call is returned to the provided delegate or listener once it connects. During this time the call can be considered to be in progress.

{% tabs %}
{% tab title="Swift" %}

```swift
let localNumber: PhoneNumber!
let remoteNumber = "2024561414"
let delegate: ActiveCallDelegate
try! telephonyClient.createVoiceCall(localNumber: localNumber, remoteNumber: remoteNumber, delegate: delegate)
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val localNumber: PhoneNumber
val remoteNumber = "2024561414"
val listener: ActiveCallListener
telephonyClient.createVoiceCall(localNumber, remoteNumber, listener)
```

{% endtab %}
{% endtabs %}

## Interacting With an Active Call

The active call delegate (iOS) / listener (Android) must be provided when a call is created and is used to communicate active call events such as when the call connects, disconnects, or fails.

If a call connects successfully, the delegate/listener will be provided with an`ActiveVoiceCall` object via the `activeVoiceCallDidConnect` method. This object provides functions for manipulating the call such as muting the microphone, ending the call, or switching the audio to speakerphone.

{% tabs %}
{% tab title="Swift" %}

```swift
public protocol ActiveCallDelegate {

    /// Notifies the delegate that the call has connected
    /// - Parameters:
    ///     - call: The `ActiveVoiceCall`
    func activeVoiceCallDidConnect(_ call: ActiveVoiceCall)

    /// Notifies the delegate that the call failed to connect
    /// - Parameters:
    ///     - error: `CallingError` that occurred.
    func activeVoiceCallDidFailToConnect(withError error: CallingError)

    /// Notifies the delegate that the call has been disconnected
    /// - Parameters:
    ///     - call: The `ActiveVoiceCall`
    ///     - error: Error that caused the call to disconnect if one occurred.
    func activeVoiceCall(_ call: ActiveVoiceCall, didDisconnectWithError error: Error?)

    /// Notifies the delegate that the call has been muted or un-muted
    /// - Parameters:
    ///     - call: The `ActiveVoiceCall`
    ///     - isMuted: Whether outgoing call audio is muted
    func activeVoiceCall(_ call: ActiveVoiceCall, didChangeMuteState isMuted: Bool)

    /// Called when the system audio route has changed.
    /// Use `call.isOnSpeaker` to determine if the call is on speaker.
    ///
    /// - Parameter call: The `ActiveVoiceCall`
    func activeVoiceCallAudioRouteDidChange(_ call: ActiveVoiceCall)
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
interface ActiveCallListener : TelephonySubscriber {
    /**
     * Notifies the listener that the call has connected
     * @param call The `ActiveVoiceCall`
     */
    fun activeVoiceCallDidConnect(call: ActiveVoiceCall)

    /**
     * Notifies the listener that the call failed to connect
     * @param exception An Exception that occurred
     */
    fun activeVoiceCallDidFailToConnect(exception: Exception)

    /**
     * Notifies the listener that the call has been disconnected
     * @param call The `ActiveVoiceCall`
     * @param exception An optional Exception that is the cause of the disconnect if not null
     */
    fun activeVoiceCallDidDisconnect(call: ActiveVoiceCall, exception: Exception?)

    /**
     * Notifies the listener that the call has been muted or un-muted
     * @param call The `ActiveVoiceCall`
     * @param isMuted Whether outgoing call audio is muted
     */
    fun activeVoiceCallDidChangeMuteState(call: ActiveVoiceCall, isMuted: Boolean)

    /**
     * Notifies the listener that the call audio routing has changed
     * @param call The `ActiveVoiceCall`
     * @param audioDevice The `AudioDevice` that the audio has been routed to
     */
    fun activeVoiceCallDidChangeAudioDevice(call: ActiveVoiceCall, audioDevice: VoiceCallAudioDevice)
}
```

{% endtab %}
{% endtabs %}

## Receiving an Incoming Call

{% hint style="info" %}
**VOIP Notifications for Incoming Calls.** Receiving incoming voice calls on iOS and Android devices requires APNS and FCM push credentials, respectively. If you would like to enable this functionality, contact your solutions engineer to assist you in configuring your environments with push credentials for your applications.
{% endhint %}

{% tabs %}
{% tab title="iOS" %}
**Subscribe to Incoming Calls**

In AppDelegate.swift import PushKit and add a `PKPushRegistry` variable:

{% code title="AppDelegate.swift" %}

```swift
import PushKit
class AppDelegate: UIResponder, UIApplicationDelegate {
    // ...
    var pushRegistry: PKPushRegistry!
}
```

{% endcode %}

In the `applicationDidFinishLaunchingWithOptions` function set up the push registry:

```swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // ...
    self.pushRegistry = PKPushRegistry(queue: nil)
    pushRegistry.delegate = self
    pushRegistry.desiredPushTypes = [.voIP]
    return true
}
```

Implement the `PKPushRegistryDelegate` methods to register, deregister and handle incoming call push notifications. When a push notification comes in, pass the payload from the voip push on to the `SudoTelephonyClient` along with an `IncomingCallNotificationDelegate`:

```swift
extension AppDelegate: PKPushRegistryDelegate {

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        try! self.telephonyClient.registerForIncomingCalls(with: pushCredentials.token, useSandbox: true, completion: { (error) in
            if let error = error {
                // Error updating push credentials
            }
            else {
                // Push credentials updated with telephony SDK
            }
        })
    }

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        let _ = try! self.telephonyClient.handleIncomingPushNotificationPayload(payload.dictionaryPayload, notificationDelegate: self)
        completion()
    }

    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        try! self.telephonyClient.deregisterForIncomingCalls(completion: { (error) in
            if let error = error {
                // Error de-registering for push notifications
            } else {
                // Successfully de-registered
            }
        })
    }
}
```

**IncomingCallNotificationDelegate:**

```swift
public protocol IncomingCallNotificationDelegate: class {
    /// Called when an incoming call is recieved.  This is called before reporting the call to callkit so the correct
    /// display name can be provided.
    ///
    /// In your implementation of this function you should set the `displayName` property of the incoming call that should be displayed by callKit.
    ///
    /// You should also set the `delegate` of the incoming call.  When the call is answered, `incomingCallAnswered(_ call: ActiveVoiceCall)` will be called
    /// and future call related events will be delivered to the delegate.
    func incomingCallReceived(_ call: IncomingCall) -> Void

    /// Indicates that the incoming call was canceled.
    /// - Parameter call: The incoming call
    /// - Parameter error: The error that occured, if any.
    func incomingCall(_ call: IncomingCall, cancelledWithError error: Error?)


    /// An incoming call was answered through CallKit and is in the process of connecting.
    /// No more updates on the incoming call will be provided to this delegate.
    /// Future call updates will be provided to the delegate set on the incoming call delegate in `incomingCallReceived(_ call: IncomingCall) -> Void`.
    func incomingCallAnswered(_ call: ActiveVoiceCall)
}
```

When the `incomingCallReceived()` function is called, you can take the `IncomingCall` object and either accept the call or decline the call. The accept method takes an `ActiveCallDelegate` instance which will then handle all of the call events.

**IncomingCall:**

```swift
public protocol IncomingCall: class {

    /// When an incoming call is recieved the delegate should be set in order to get updates on call related events.
    /// For example if the call is answered through CallKit the delegate will recieve the call connected event with
    /// a handle to the call.
    var delegate: ActiveCallDelegate? { get set }

    /// The phone number receiving the call
    var localNumber: String { get }

    /// The phone number the call is coming from
    var remoteNumber: String { get }

    /// The UUID for the call
    var uuid: UUID { get }

    /// Accepts the call with a delegate to provide call lifecycle updates.
    func accept(with delegate: ActiveCallDelegate)

    /// Decline the call.
    func decline()

    /// The display name used by callKit when reporting a call
    var displayName: CallKitDisplayName? { get set }
}
```

{% endtab %}

{% tab title="Android" %}
**Subscribe to Incoming Calls**

In order to receive calls you must first set up Firebase Cloud Messaging in your app. Instructions can be found [here](https://firebase.google.com/docs/cloud-messaging/android/client).

\_\*\*\_Subscribe to incoming calls using the `fcmToken` provided by Firebase. The following code will fail if Google Play Services are not enabled on the device.

```kotlin
FirebaseInstanceId.getInstance().instanceId
    .addOnCompleteListener(OnCompleteListener { task ->
        if (!task.isSuccessful) {
            // failed to get instance id
            return@OnCompleteListener
        }

        val fcmToken = task.result!!.token
        sudoTelephonyClient.calling.registerForIncomingCalls(fcmToken) { result ->
            when (result) {
                is Result.Success -> {
                    // successfully registered
                }
                is Result.Error -> {
                    // failed to register
                }
            }
        }
    })
```

**Handle Incoming Firebase Messages**

When a message comes in from Firebase, pass it on to the `SudoTelephonyClient` along with an `IncomingCallNotificationListener`:

```kotlin
override fun onMessageReceived(message: RemoteMessage) {
    super.onMessageReceived(message)
    // listener: IncomingCallNotificationListener
    sudoTelephonyClient.calling.handleIncomingPushNotification(message.data, listener)
}
```

**IncomingCallNotificationListener:**

```kotlin
/**
 * Listener for receiving notifications about new `IncomingCall` events.
 */
interface IncomingCallNotificationListener: TelephonySubscriber {
    /**
     * Notifies the listener that the call was received
     * @param call The `IncomingCall`
     */
    fun incomingCallReceived(call: IncomingCall)

    /**
     * Notifies the listener that the call was canceled
     * @param call The `IncomingCall`
     */
    fun incomingCallCanceled(call: IncomingCall, error: Throwable?)
}
```

When the `incomingCallReceived()` function is called, you can take the `IncomingCall` object and either accept the call or decline the call. The accept method takes an `ActiveCallListener` instance which will then handle all of the call events.

**IncomingCall:**

```kotlin
class IncomingCall() {
    /**
     * The phone number receiving the call
     */
    var localNumber: String

    /**
     * The phone number the call is coming from
     */
    var remoteNumber: String

    /**
     * The unique string identifying the call invite
     */
    var callSid: String

    /**
     * Accepts the call
     * @param listener A listener to receive call lifecycle updates
     */
    fun acceptWithListener(listener: ActiveCallListener)

    /**
     * Declines the call
     */
    fun decline()
}
```

{% endtab %}
{% endtabs %}

## Manage Call Records

A call record is generated for each call made and received. The SDK provides three methods for accessing that data and one for deleting call records.

### Get Call Records

Retrieve a list of call records with a provided `PhoneNumber` and an optional limit and token for pagination:

{% tabs %}
{% tab title="Swift" %}

```swift
// localNumber: PhoneNumber
// nextToken: String
try telephonyClient.getCallRecords(localNumber: localNumber, limit: nil, nextToken: nextToken) { (result) in
    switch result {
    case .success(let listToken):
        // listToken: TelephonyListToken<CallRecord>
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// localNumber: PhoneNumber
// nextToken: String
sudoTelephonyClient.calling.getCallRecords(localNumber, null, nextToken) { result ->
    when (result) {
        is Result.Success -> {
            // result.value.items: List<CallRecord>
        }
        is Result.Absent -> {
            // no call records
        }
        is Result.Error -> {
            // result.throwable: Throwable
        }
    }
}
```

{% endtab %}
{% endtabs %}

### **Get Call Record**

Get a `CallRecord` providing its ID.

{% tabs %}
{% tab title="Swift" %}

```swift
// callRecordId: String
try telephonyClient.getCallRecord(callRecordId: callRecordId) { (result) in
    switch result {
    case .success(let callRecord):
        // callRecord: CallRecord
    case .failure(let error):
        // error: SudoTelephonyClientError
    }  
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// callRecordId: String
sudoTelephonyClient.calling.getCallRecord(callRecordId) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: CallRecord
        }
        is Result.Error -> {
            // result.throwable: Throwable
        }
    }
}
```

{% endtab %}
{% endtabs %}

### **Subscribe To Call Records**

Subscribe to `CallRecord` events and state changes. On iOS the `subscribeToCallRecords()` function returns a `SubscriptionToken` which can be used to cancel the subscription. On Android the function takes a `CallRecordSubscriber` instance which will receive `CallRecord` updates.

{% tabs %}
{% tab title="Swift" %}

```swift
let subscriptionToken = try telephonyClient.subscribeToCallRecords(resultHandler: { (result) in
    switch result {
    case .success(let callRecord):
        // callRecord: CallRecord
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
})
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
sudoTelephonyClient.calling.subscribeToCallRecords(this, "CallRecordSubscriberId")



/**
 * Subscriber for receiving notifications about new `CallRecord` objects.
 */
interface CallRecordSubscriber : TelephonySubscriber {

    /**
     * Notifies the subscriber of a new 'CallRecord'.
     */
    fun callRecordReceived(callRecord: CallRecord)
}
```

{% endtab %}
{% endtabs %}

### **Delete Call Record**

Delete a `CallRecord` providing its ID.

{% tabs %}
{% tab title="Swift" %}

```swift
// callRecordId: String
try telephonyClient.deleteCallRecord(id: callRecordId) { (result) in
    switch result {
    case .success(let deletedId):
        // deletedId: String
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// callRecordId: String
sudoTelephonyClient.calling.deleteCallRecord(callRecordId) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: String (callRecordId)
        }
        is Result.Error -> {
            // result.throwable: Throwable
        }
    }
}
```

{% endtab %}
{% endtabs %}

### Call Record

A `CallRecord` contains all the information for a voice call made or received.

{% tabs %}
{% tab title="Swift" %}

```swift
public struct CallRecord {
    /// Enum that represents the direction of the call.
    public enum Direction {
        case inbound
        case outbound
        case unknown

        init(internalDirection: SudoTelephony.Direction) {
            switch internalDirection {
            case .inbound:
                self = .inbound
            case .outbound:
                self = .outbound
            case .unknown:
                self = .unknown
            }
        }
    }

    // Enum that represents the state of the call record.
    public enum State {
        case authorized
        case queued
        case ringing
        case answered
        case completed
        case unanswered
        case unknown

        init(internalState: CallState) {
            switch internalState {
            case .authorized:
                self = .authorized
            case .queued:
                self = .queued
            case .ringing:
                self = .ringing
            case .answered:
                self = .answered
            case .completed:
                self = .completed
            case .unanswered:
                self = .unanswered
            case .unknown:
                self = .unknown
            }
        }
    }

    /// Voicemail data belonging to a call record
    public struct VoicemailData {
        /// Unique ID of the voicemail record
        public let id: String
        /// The duration of the voicemail recording in seconds
        public let durationSeconds: UInt
        /// The media object that can be used to download the voicemail recording
        public let media: MediaObject
    }

    /// Unique ID of the call record
    public let id: String
    /// The user ID of the owner of the call record (also referred to as the subject)
    public let owner: String
    /// The ID of the sudo that made or received the call
    public let sudoOwner: String
    /// The ID of the `PhoneNumber` associated with the call record
    public let phoneNumberId: String
    /// The direction of the call. Either `outbound` or `inbound`
    public let direction: Direction
    /// The state of the call record
    public let state: State
    /// Timestamp that represents the last time the call was updated
    public let updated: Date
    /// Timestamp that represents the time the call was created
    public let created: Date
    /// The E164Number of the local phone number
    public let localPhoneNumber: String
    /// The E164Number of the remote phone number
    public let remotePhoneNumber: String
    /// The duration of the call in seconds
    public let durationSeconds: UInt32
    /// The voicemail data of this call record if it exists
    public let voicemail: VoicemailData?
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
data class CallRecord(
     /**
      * Unique ID of the call record
      */
     val id: String,
     /**
      * The user ID of the owner of the call record (also referred to as the subject)
      */
     val owner: String,
     /**
      * The ID of the sudo that made or received the call
      */
     val sudoOwner: String,
     /**
      * The ID of the `PhoneNumber` associated with the call record
      */
     val phoneNumberId: String,
     /**
      * The direction of the call. Either `outbound` or `inbound`
      */
     val direction: Direction,
     /**
      * The state of the call record
      */
     val state: CallRecordState,
     /**
      * Timestamp that represents the last time the call was updated
      */
     val updated: Instant,
     /**
      * Timestamp that represents the time the call was created
      */
     val created: Instant,
     /**
      * The E164Number of the local phone number
      */
     val localPhoneNumber: String,
     /**
      * The E164Number of the remote phone number
      */
     val remotePhoneNumber: String,
     /**
      * The duration of the call in seconds
      */
     val durationSeconds: Int,
     /**
      * The voicemail data of this call record if it exists
      */
     val voicemail: VoicemailData?

) : Parcelable

/**
 * Enum that represents the state of the call record.
 */
enum class CallRecordState() {
     AUTHORIZED,
     QUEUED,
     RINGING,
     ANSWERED,
     COMPLETED,
     UNANSWERED,
     UNKNOWN;
}

/**
 * Voicemail data belonging to a call record
 */
data class VoicemailData (
     /**
      * Unique ID of the voicemail record
      */
     val id: String,
     /**
      * The duration of the voicemail recording in seconds
      */
     val durationSeconds: Int,
     /**
      * The media object that can be used to download the voicemail recording
      */
     val media: MediaObject
) : Parcelable
```

{% endtab %}
{% endtabs %}

## Voicemail

When an incoming call is rejected or times out, the caller is sent to voicemail. After hearing a recorded greeting, the caller can record up to 3 minutes of voicemail. The SDK provides several methods to manage these stored voicemail recordings.

### Get Voicemails

Retrieve a list of voicemail records with a provided `PhoneNumber` and an optional limit and token for pagination:

{% tabs %}
{% tab title="Swift" %}

```swift
// localNumber: PhoneNumber
// nextToken: String
telephonyClient.getVoicemails(localNumber: localNumber, limit: nil, nextToken: nextToken) { (result) in
    switch result {
    case .success(let listToken):
        // listToken: TelephonyListToken<Voicemail>
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// localNumber: PhoneNumber
// nextToken: String
sudoTelephonyClient.calling.getVoicemails(localNumber, null, nextToken) { result ->
    when (result) {
        is Result.Success -> {
            // result.value.items: List<Voicemail>
        }
        is Result.Absent -> {
            // no voicemails
        }
        is Result.Error -> {
            // result.throwable: Throwable
        }
    }
}
```

{% endtab %}
{% endtabs %}

### **Get Voicemail**

Get a `Voicemail` by its ID.

{% tabs %}
{% tab title="Swift" %}

```swift
// voicemailId: String
telephonyClient.getVoicemail(id: voicemailId) { (result) in
    switch result {
    case .success(let voicemail):
        // voicemail: Voicemail
    case .failure(let error):
        // error: SudoTelephonyClientError
    }  
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// voicemailId: String
sudoTelephonyClient.calling.getVoicemail(voicemailId) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: Voicemail
        }
        is Result.Error -> {
            // result.throwable: Throwable
        }
    }
}
```

{% endtab %}
{% endtabs %}

### **Subscribe To Voicemails**

Subscribe to `Voicemail` create events, update events, and delete events. On iOS the `subscribeToVoicemails()` function returns a `SubscriptionToken` which can be used to cancel the subscription. On Android the function takes a `VoicemailSubscriber` instance which will receive Voicemail events.

{% tabs %}
{% tab title="Swift" %}

```swift
let subscriptionToken = try telephonyClient.subscribeToVoicemails(resultHandler: { (result) in
    switch result {
    case .success(let voicemail):
        // voicemail: Voicemail
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
})
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
sudoTelephonyClient.calling.subscribeToVoicemails(this, "VoicemailSubscriberId")



/**
 * Subscriber for receiving notifications about new `Voicemail` updates.
 */
interface VoicemailSubscriber : TelephonySubscriber {

    /**
     * Notifies the subscriber of a new 'Voicemail' update.
     */
    fun voicemailUpdated(voicemail: Voicemail)
}
```

{% endtab %}
{% endtabs %}

**Delete Voicemail**

Delete a `Voicemail` by its ID.

{% tabs %}
{% tab title="Swift" %}

```swift
// voicemailId: String
try telephonyClient.deleteVoicemail(id: voicemailId) { (result) in
    switch result {
    case .success:
        // successfully deleted
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// voicemailId: String
sudoTelephonyClient.calling.deleteVoicemail(voicemailId) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: String (voicemailId)
        }
        is Result.Error -> {
            // result.throwable: Throwable
        }
    }
}
```

{% endtab %}
{% endtabs %}

### Voicemail Record

A `Voicemail` contains all the information for a voicemail recording.

{% tabs %}
{% tab title="Swift" %}

```swift
public struct Voicemail {
    /// Unique ID of the voicemail record
    public let id: String
    /// The ID of the Sudo Platform user that owns the voicemail.
    public let owner: String
    /// The ID of the Sudo that received the voicemail.
    public let sudoOwner: String
    /// The ID of the `PhoneNumber` associated with the voicemail.
    public let phoneNumberId: String
    /// The ID of the `CallRecord` associated with the voicemail.
    public let callRecordId: String?
    /// Timestamp that represents the time the voicemail was created.
    public let created: Date
    /// Timestamp that represents the last time the voicemail was updated.
    public let updated: Date
    /// The E.164 formatted phone number that received the voicemail.
    public let localPhoneNumber: String
    /// The E.164 formatted phone number of the caller.
    public let remotePhoneNumber: String
    /// The duration of the voicemail recording in seconds.
    public let durationSeconds: UInt
    /// The media object that can be used to download the voicemail recording.
    public let media: MediaObject
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
data class Voicemail(
    /**
     * Unique ID of the voicemail record
     */
    val id: String,
    /**
     * The ID of the Sudo Platform user that owns the voicemail.
     */
    val owner: String,
    /**
     * The ID of the Sudo that received the voicemail.
     */
    val sudoOwner: String,
    /**
     * The ID of the `PhoneNumber` associated with the voicemail.
     */
    var phoneNumberId: String,
    /**
     * The ID of the `CallRecord` associated with the voicemail.
     */
    var callRecordId: String?,
    /**
     * Timestamp that represents the time the voicemail was created.
     */
    val created: Instant,
    /**
     * Timestamp that represents the last time the voicemail was updated.
     */
    val updated: Instant,
    /**
     * The E.164 formatted phone number that received the voicemail.
     */
    val localPhoneNumber: String,
    /**
     * The E.164 formatted phone number of the caller.
     */
    val remotePhoneNumber: String,
    /**
     * The duration of the voicemail recording in seconds.
     */
    val durationSeconds: Int,
    /**
     * The media object that can be used to download the voicemail recording.
     */
    val media: MediaObject
) : Parcelable
```

{% endtab %}
{% endtabs %}

### Downloading Voicemail Audio Data

When voicemail records are retrieved, the audio data is not automatically downloaded. This gives you control over performance when retrieving voicemail records, particularly those with long recordings.

To download voicemail audio data, use the `downloadData()` method, passing it a `MediaObject` which can be retrieved from the `media` property of a `Voicemail`. This method returns the raw audio data.

{% tabs %}
{% tab title="Swift" %}

```swift
import AVFoundation

let voicemail: Voicemail
telephonyClient.downloadData(for: voicemail.media) { (result) in
    switch result {
    case .success(let data):
        let audioPlayer = try! AVAudioPlayer(data: data)
        audioPlayer.play()
    case .failure(let error):
        // error: SudoTelephonyClientError
    }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// voicemail: Voicemail
sudoTelephonyClient.downloadData(voicemail.media) { result ->
    when (result) {
        is Result.Success -> {
            // result.value: ByteArray                   
        }
        is Result.Error -> {
            // result.throwable: Throwable
        }
    }
}
```

{% endtab %}
{% endtabs %}

### Associating Voicemail Records with Call Records

Voicemail records and call records can be managed independently. However, a call record that has a corresponding voicemail has an additional `voicemail` property containing a copy of the voicemail data. When said voicemail is deleted this copy is also deleted.

{% tabs %}
{% tab title="Swift" %}

```swift
let callRecord: CallRecord
if let voicemailData: CallRecord.VoicemailData = callRecord.voicemail {
    // The call record has associated voicemail data.
    // voicemailData.id: String
    // voicemailData.durationSeconds: UInt
    // voicemailData.media: MediaObject
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val callRecord: CallRecord
callRecord.voicemail?.let { voicemailData ->
    // The call record has associated voicemail data.
    // voicemailData.id: String
    // voicemailData.durationSeconds: Int
    // voicemailData.media: MediaObject
}
```

{% endtab %}
{% endtabs %}

The `Voicemail`record also has an optional `callRecordId` property which relates the voicemail to its call record if the call record has not been deleted.


# Telephony Simulator

This page describes the Telephony simulator and "ZZ" country code.

## Telephony Simulator

The Telephony Simulator is a tool that enables you to test **Telephony SDK** integration without the dependency of provisioning real phone numbers and sending text messages across the wire.

## The "ZZ" Country Code

The "ZZ" country code is included in the [list of supported countries](/guides/telephony/phone-numbers#get-supported-countries) for phone provisioning in projects that are in sandbox mode. Provisioning phone numbers in this region does not incur any number provisioning costs, as the resulting numbers are "simulated" and cannot communicate with other phone numbers.

Outgoing SMS and MMS messages from simulated numbers will by default be marked as successfully delivered. However, this behavior can be changed by sending a message to a test number as described below.

## Test Numbers

There are a few phone numbers that have special behavior in the simulated telephony region. These test numbers can be used to exercise specific code paths that may be difficult to test otherwise.

**Note:** The special behavior for these test numbers only occurs when using a phone number provisioned in the simulated telephony region.

| Test Number  | Behavior                                                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| +11111111111 | Attempting to provision this phone number with the simulated telephony region will always fail.                                            |
| +12222222222 | Messages sent to this test number will end up in the ["Undelivered" state](/guides/telephony/text-messaging#phone-message).                |
| +13333333333 | Messages sent to this test number will end up in the ["Failed" state](/guides/telephony/text-messaging#phone-message).                     |
| +15555555555 | When an SMS or MMS message is sent to this test number, a SMS reply will be automatically generated in response.                           |
| +17777777777 | When an SMS or MMS message is sent to this test number, an MMS reply with an image attachment will be automatically generated in response. |


# SDK Releases

Additional resources for the Telephony SDKs

Use the links below to learn about significant new features and fixes added in each release of the Telephony SDKs and to access the source code for the SDKs and sample apps.

## Telephony SDK for iOS

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-telephony-ios/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-telephony-ios/)
* [Sample app source code](https://github.com/sudoplatform/samples-ios/tree/master/telephony)

## Telephony SDK for Android

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-telephony-android/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-telephony-android)
* [Sample app source code](https://github.com/sudoplatform/samples-android/tree/master/telephony)


# API Reference

API references for the Telephony SDKs

The links below provides a detailed API reference for the Telephony SDKs.

* [iOS API Reference](https://sudoplatform.github.io/sudo-telephony-ios/index.html)
* [Android API Reference](https://sudoplatform.github.io/sudo-telephony-android/sudotelephony/index.html)


# Email

Unlock the power of secure and private email within your products.

The Sudo **Email SDK** allows you to offer multiple email based communication capabilities to your users while at the same time preserving and enhancing their email security and privacy posture.&#x20;

Email has become so ubiquitous that email addresses are now synonymous with a users' identities. More and more services and applications require users to share their email address alongside their other personal data. This tight coupling of an email address with personal identity carries significant privacy and security implications in our data economy. If these are left unchecked, they expose users to significant threats such as identity theft, social engineering, account takeovers, doxing, and more.

To Sudo Platform offers the following capabilities for enabling users to operate more security, privately and in control of their email.&#x20;

## Key Features

<table><thead><tr><th width="160"></th><th></th></tr></thead><tbody><tr><td><a href="/pages/7KULCPUOIjqoX6CGY852">Email Addresses</a></td><td><p>Give your users one or more of fully featured email accounts for your use case.  <strong>Email Addresses</strong> include all the send, receive, folder, spam capabilities and more that you expect from an email service.</p><p><br>This is the ultimate in private email solutions, your users' inbox is encrypted, only accessible to them and works in both E2EE or public email use cases.</p></td></tr><tr><td><a href="/pages/uQyQJ8UPnyk1H3RW1bcX">Email Masks</a></td><td>For your users who prefer to manage email messages in their existing email provider of choice (e.g. Mysudo, Gmail, Outlook, etc) the Sudo Platform offers <strong>Email Masks</strong>.  Email Masks still offer a level of obfuscation and privacy for your personal email address when interacting with sites, apps or interactions.</td></tr></tbody></table>

## Next Steps

* Integrate the **Email SDK** into your [web](/guides/email/integrate-the-email-sdk#integrate-the-js-sdk), [iOS](/guides/email/integrate-the-email-sdk#integrate-the-ios-sdk) and [Android](/guides/email/integrate-the-email-sdk#integrate-the-android-sdk) applications
* Learn more about [Sudo digital identities](/concepts/sudo-digital-identities) and other core concepts


# Integrate the Email SDK

Integrate the Email SDK into your application

## Prerequisites <a href="#prerequisites" id="prerequisites"></a>

1. [​Complete the Getting Started Guide​](/guides/getting-started)
2. ​[Integrate the User SDK​](/guides/users/integrate-the-user-sdk)
3. [​Integrate the Sudo Profiles SDK​](/guides/sudos/integrate-the-sudo-sdk)

## Get Started <a href="#get-started" id="get-started"></a>

* [Integrate the JS SDK](#integrate-the-js-sdk)
* [​Integrate the iOS SDK​](#integrate-the-ios-sdk)
* [​Integrate the Android SDK​](#integrate-the-android-sdk)

## Integrate the JS SDK

To use the **Email SDK** in a Web or Node.js project, you must add `@sudoplatform/sudo-email` as a dependency to your project.

```javascript
yarn add '@sudoplatform/sudo-email'
# or
npm install --save '@sudoplatform/sudo-email'
```

{% hint style="info" %}
In order to instantiate an Email client, make sure you have followed instructions for [Getting Started](/guides/getting-started), [User SDK](/guides/users/integrate-the-user-sdk) and [Sudo Profiles SDK](/guides/sudos/integrate-the-sudo-sdk) (see [Prerequisites](#prerequisites) above).
{% endhint %}

To instantiate and initialize a client in your application, add the following:

{% tabs %}
{% tab title="TypeScript" %}

```javascript
import { DefaultApiClientManager } from '@sudoplatform/sudo-api-client'
import { DefaultConfigurationManager } from '@sudoplatform/sudo-common'
import { DefaultSudoEmailClient } from '@sudoplatform/sudo-email'
import { DefaultSudoProfilesClient } from '@sudoplatform/sudo-profiles'
import { DefaultSudoUserClient } from '@sudoplatform/sudo-user'

const sdkConfigJSON = /* ... refer to Users documentation ... */
DefaultConfigurationManager.getInstance().setConfig(sdkConfigJSON)

const userClient = new DefaultSudoUserClient(/* refer to Users documentation */)
const profilesClient = new DefaultSudoProfilesClient(/* refer to Sudos documentation */)
const apiClientManager = DefaultApiClientManager.getInstance().setAuthClient(userClient)

const emailClient = new SudoEmailClient(
    apiClientManager
    userClient,
    profilesClient,
)
```

{% endtab %}
{% endtabs %}

## Integrate the iOS SDK <a href="#integrate-the-ios-sdk" id="integrate-the-ios-sdk"></a>

To use the Email SDK in an iOS app, you need to install the SudoEmail package via Swift Package Manager

Open your project settings in XCode, and go to the `Package Dependencies` tab. Click on the `+` sign to add a dependency.&#x20;

Enter the repository URL [`https://github.com/sudoplatform/sudo-email-ios`](https://github.com/sudoplatform/sudo-email-ios) in the top right search box and select the `sudo-email-ios` repository.&#x20;

Select the required version and `Add Package`.&#x20;

Sudo Platform SDKs conform to semantic versioning so in most cases you will leave the Dependency Rule as `Up to Next Major Version` to receive regular updates without introducing any breaking changes.

This will resolve the local package dependency and install the latest version of the Email SDK.

{% hint style="info" %}
In order to instantiate an Email client, make sure you have followed instructions for [Getting Started](/guides/getting-started), [User SDK](/guides/users/integrate-the-user-sdk) and [Sudo Profiles SDK](/guides/sudos/integrate-the-sudo-sdk) (see [Prerequisites](#prerequisites) above)
{% endhint %}

To instantiate a client in your application, add the following:

{% tabs %}
{% tab title="Swift" %}

```swift
import SudoEmail
import SudoUser

let userClient = // ... see "Users" docs

let emailClient = try DefaultSudoEmailClient(
    keyNameSpace: "MyNameSpace",
    sudoUserClient: userClient
)
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open source iOS sample app](https://github.com/sudoplatform/samples-ios) on GitHub that our team published as a reference for integrating the **Email SDK** into your app as quickly and seamlessly as possible.
{% endhint %}

## Integrate the Android SDK <a href="#integrate-the-android-sdk" id="integrate-the-android-sdk"></a>

The Android SDK is open source and compatible with Android 7 (API level 24) and above.

{% tabs %}
{% tab title="Gradle" %}
Add this line to the dependencies section of the app module `build.gradle` and synchronize your project with Android Studio.

```groovy
dependencies {
    implementation 'com.sudoplatform:sudoemail:$latest_version'
}
```

{% hint style="info" %}
The latest version of the SDK can be found at [SDK Releases](/guides/email/sdk-releases).
{% endhint %}

{% hint style="info" %}
In order to instantiate an Email client, make sure you have followed instructions for [Getting Started](/guides/getting-started) and [User SDK](/guides/users/integrate-the-user-sdk) and [Sudo Profiles SDK ](/guides/sudos/integrate-the-sudo-sdk)(see Prerequisities above)
{% endhint %}
{% endtab %}
{% endtabs %}

To instantiate a client in your application, add the following:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val userClient = // ... see "Users" docs

val emailClient = SudoEmailClient.builder()
    .setContext(appContext) 
    .setSudoUserClient(userClient)
    .build()
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one client instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open source Android sample app](https://github.com/sudoplatform/samples-android) on Github that our team published as a reference for integrating the **Email SDK** into your app as quickly and seamlessly as possible.
{% endhint %}

### Authentication

In order to successfully invoke any methods on the Email Client, the User Client must be authenticated (signed in). See [Authentication](/guides/users/authentication) for more details on how to implement the sign in process.

Authentication can be done explicitly as part of application bootstrap, or you may provide a callback which implements the signIn process. If such a callback is provided, it will be invoked whenever an email SDK method is called and the userClient is not already signed in. &#x20;

Set the sign in callback to empty to remove this behaviour.

{% tabs %}
{% tab title="Typescript" %}

```typescript
// Define the callback, it will likely invoke userClient.signIn or
// similar depending on your required processing
  const callback: SudoPlatformSignInCallback = {
    signIn: async () => {
      // Perform signIn processing here.
    },
  }

// Call setSignInCallback
emailClient.setSignInCallback(callback)
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Define the delegate, it will likely invoke userClient.signIn or
// similar depending on your required processing
class EmailSignInDelegate: SudoPlatformSignInDelegate {
    func signIn() async throws {
         // Perform signIn processing here.
    }
}

// Call setSignInDelegate
let delegate = EmailAddressSignInDelegate()
await emailClient.setSignInDelegate(delegate)
```

{% endtab %}

{% tab title="Kotlin" %}

```kt
// Define the callback, it will likely invoke userClient.signIn or
// similar depending on your required processing
class EmailSignInCallback : SudoPlatformSignInCallback {
   override suspend fun signIn() {
      // Perform signIn processing here.
   }
}

// Call setSignInCallback
val callback = new EmailSignInCallback()
emailClient.setSignInCallback(callback)
```

{% endtab %}
{% endtabs %}


# Email Service Configuration

Allow your consuming clients to obtain email service configuration details to enable client side enforcement and richer UX feedback.

## Get Configuration Data

A call to `getConfigurationData` returns the various limits applied to email sending and receiving, as well as the limits applied to email management.  This data is useful when building client side UX that prevents users from exceeding service limits.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Interface representing the configuration data returned
export interface ConfigurationData {
  deleteEmailMessagesLimit: number
  updateEmailMessagesLimit: number
  emailMessageMaxInboundMessageSize: number
  emailMessageMaxOutboundMessageSize: number
  emailMessageRecipientsLimit: number
  encryptedEmailMessageRecipientsLimit: number
}

try {
    const configurationData = await emailClient.getConfigurationData()
    // `configurationData` contains the various limits configured for emails.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
do {
    let configurationData = try await emailClient.getConfigurationData()
    // `configurationData` contains the following properties:
    /// The number of email messages that can be deleted at a time.
    /// public var deleteEmailMessagesLimit: Int

    /// The number of email messages that can be updated at a time.
    /// public var updateEmailMessagesLimit: Int

    /// The maximum allowed size of an inbound email message.
    /// public var emailMessageMaxInboundMessageSize: Int

    /// The maximum allowed size of an outbound email message.
    /// public var emailMessageMaxOutboundMessageSize: Int
    
    /// The maximum number of recipients for an out-of-network email message.
    /// public var emailMessageRecipientsLimit: Int
    
    /// The maximum number of recipients for an in-network encrypted email message.
    /// public var encryptedEmailMessageRecipientsLimit: Int
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val configurationData = withContext(Dispatchers.IO) {
            emailClient.getConfigurationData()
        }
        // The [configurationData] is returned which contains the following properties:
        //     * deleteEmailMessagesLimit: The number of email messages that can be deleted at a time.
        //     * updateEmailMessagesLimit: The number of email messages that can be updated at a time.
        //     * emailMessageMaxInboundMessageSize: The maximum allowed size of an inbound email message.
        //     * emailMessageMaxOutboundMessageSize: The maximum allowed size of an outbound email message.
        //     * emailMessageRecipientsLimit: The maximum number of recipients for an out-of-network email message.
        //     * encryptedEmailMessageRecipientsLimit: The maximum number of recipients for an in-network encrypted email message.
    } catch (e: EmailConfigurationException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}


# Email SDK Pagination

Provides the ability to separate and traverse through pages of data

Email is data heavy and as such various APIs provide the ability to query results in a paginated form. These APIs will typically have an optional `nextToken` input property to facilitate pagination. By default, if a limit is not provided, or set as `undefined/nil/null`, the default limit will be 10.

{% hint style="info" %}
When specifying a large number of records in a query, an upper limit of 1MB is enforced on the data retrieved. If the 1MB limit is hit, the data will be returned as a page and require using the next token of the result to query the next page of results.
{% endhint %}

The following types support pagination:

* Email Addresses
* Email Messages
* Email Folders

If intending to use pagination, the first call to an API should not include a `nextToken`, otherwise the result will return an error. When a paginated API call is successful, the output response will contain a `nextToken` property.

When using a query that supports pagination, it is important to always check the `nextToken`. Due to the nature of the data access from the service, it is possible to retrieve a result with no data but potentially have another page of information to retrieve which may contain results.

To access the next page of a query API, use the previously returned `nextToken` in the subsequent API call. If a successful response does not return a `nextToken`, or it is set to `undefined/nil/null`, the pagination results have been exhausted and there are no more pages to retrieve.

{% hint style="warning" %}
When using subsequent calls for pagination, ensure that the input information (e.g. `limit`) is the same for each call, otherwise unexpected behavior will occur. For example, if the first call uses an input property of `limit = 8`, then each subsequent call must use a `limit` of `8`.
{% endhint %}

## Pagination Example

{% tabs %}
{% tab title="TypeScript" %}
Below is an example usage of `getEmailAddresses` for a user with four email addresses and paginating through the results:

```typescript
const output: EmailAddress[] = []
let nextToken: string | undefined = undefined
try {
  do {
    const listOutput = await emailClient.listEmailAddresses({
      cachePolicy: CachePolicy.RemoteOnly,
      limit = 2,
      nextToken, // `undefined` on first invocation
    })
    output.push(...listOutput.items)
    nextToken = listOutput.nextToken
  } while (nextToken)
} catch {
  // Handle/notify user of errors
}
```

The first iteration of the `do...while` loop will have the `listOutput` return an array of `items` containing two `EmailAddress` objects, as well as a populated `nextToken` which is used in the subsequent call in the next iteration. The second iteration will have the `listOutput` return an array of `items` containing two `EmailAddress` objects, however the `nextToken` will be `undefined` as there are no more objects for the query to fetch.
{% endtab %}

{% tab title="Swift" %}
Below is an example usage of `listEmailAddresses` for a user with 4 email addresses:

```swift
var output: ListOutput<EmailAddress>?
var input = ListEmailAddressesInput(limit: 2, nextToken: nil)
do {
  output = try await emailClient.listEmailAddresses(withInput: input)
  /*
   * This output will contain an `items` array of 2 Email Address objects, as
   * well as a non-`nil` `nextToken` which is used in the subsequent call.
   */
  input = ListEmailAddressesInput(limit: 2, nextToken: output.nextToken)
  output = try await emailClient.listEmailAddresses(withInput: input)
  /*
   * This output will also contain 2 Email Address objects in the `items` array, however the
   * `nextToken` will be `nil`, as there are no more objects for the user to
   * fetch.
   */
} catch {
  // Handle/notify user of errors
}
```

{% endtab %}
{% endtabs %}


# Email SDK Caching

Perform queries through network or local lookups

In order to provide eventual consistency to users' data, the **Email SDK** caches the results of each method call result from the Email Service. This allows the ability to provide your users with real-time synchronization of data updates with the service.

For optimal behavior, cached results are best used to serve offline/fast data to the user while performing a background sync/update to deliver new information to the user.

## Sudo CachePolicy

Some access APIs offer the ability to provide a `CachePolicy` property to determine the approach that is used to access the data. The two policies offered are `RemoteOnly` and `CacheOnly` .

### RemoteOnly

This policy will access the appropriate records directly from the Email Service. When used as the selected cache policy, this will add and refresh new and existing records to the cache, as well as return the latest data set.

### CacheOnly

This policy will not call out to the Email Service, but will instead use any existing cached result of previous API calls used with the `RemoteOnly` policy. This policy comes with a caveat; queried results are cached 'as called'. This means that a cached query will only match a direct query result to the service (using `RemoteOnly`). For instance, if a call is made to get all results using `RemoteOnly`, and then a filter call is applied using `CacheOnly`, all the same results from the previous call are returned.


# Email Addresses

Email Addresses are a fully functional email product to power your use cases with "privacy by design" built in.

**Email Addresses** are the Sudo Platform email offering that gives your users a fully featured privacy-by-design email account including all the send, receive, folder, spam capabilities you expect from an email service.

Email Addresses can be used in multiple use cases including:

* Offering a privacy first email capability into your existing communication products.
* Integrating an in-app inbox experience for use cases where E2EE may be advantageous or regulated such as:
  * Interactions between your users and your Custom L1 support.
  * Interactions between your users where medical or financial data is handled, etc.

## Key Features

<table><thead><tr><th width="245.5"></th><th></th></tr></thead><tbody><tr><td><a href="/pages/-M8mjRF37aHo-Hp1TFec">Email Addresses</a></td><td>The starting point into the email address capability, provision email addresses for your domain backed by the privacy and security benefits of a Sudo.</td></tr><tr><td><a href="/pages/-MlD3Asp5xcm1ofCsmOj">Email Drafts and Folders</a></td><td>Allow your users the flexibility to manage their inbox with email folders and save draft emails securely.</td></tr><tr><td><a href="/pages/-M8mjWYRGqVgBfn_jeFY">Send and Receive Emails</a></td><td>Send and receive email messages in both E2EE use cases or to external email providers.  In either case, your email messages are stored and protected by encryption that is only able to be decrypted by your users.</td></tr></tbody></table>


# Email Address Entitlements

Managing consumption of Email capabilities

The Sudo Platform entitlements system is used to control consumption of Sudo Platform resources, Email Addresses are a feature who's usage is controlled by entitlements.  This allows you to control what and how many of each capability your users can consume within your own 'user plans' structures.

{% hint style="info" %}
Read more about the Sudo Platform entitlements system and how to integrate with it in the [Entitlements](/guides/entitlements) section of the Sudo Platform documentation.
{% endhint %}

The following table describes the entitlements used to control access to the Email Accounts:

<table><thead><tr><th width="330.64496873223425">Entitlement</th><th width="103.33333333333331">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>sudoplatform.email.emailAddressUserEntitled</code></td><td>Boolean</td><td>Whether or not the user is entitled to provision new email addresses.</td></tr><tr><td><code>sudoplatform.email.emailMessageSendUserEntitled</code></td><td>Boolean</td><td>Whether or not the user is entitled to send emails</td></tr><tr><td><code>sudoplatform.email.emailMessageReceiveUserEntitled</code></td><td>Boolean</td><td>Whether or not the user is entitled to receive emails</td></tr><tr><td><code>sudoplatform.email.emailAddressMaxPerSudo</code></td><td>Numeric</td><td>Maximum number of email addresses per Sudo profile.</td></tr><tr><td><code>sudoplatform.email.emailStorageMaxPerEmailAddress</code></td><td>Numeric</td><td>Maximum amount of non-draft email data stored per email address in bytes.</td></tr><tr><td><code>sudoplatform.email.emailStorageMaxPerUser</code></td><td>Numeric</td><td>Maximum amount of non-draft email data stored per user in bytes.</td></tr></tbody></table>


# Manage Email Addresses

Provides the essentials to give your users their own email address with the privacy and security benefits of a Sudo

Email addresses are a core component of the **Email SDK**. Addresses for different configured domains can be provisioned and used to communicate with other recipients.

Provisioning an email address is a two-step process. First, a check should be performed to determine if the email address is valid and available. Once results are returned and the desired address is selected, the address can be provisioned to a Sudo.

## Get Supported Email Domains

{% hint style="info" %}
**Deprecated -** see List Email Domains
{% endhint %}

When provisioning a new email address for your user, a domain is needed to ensure the email address will be valid. The Sudo Platform email service can be configured to support issuing email addresses for multiple domains.

{% hint style="info" %}
Each project in your Sudo Platform account can be configured to support a set of email domains. Your solutions engineer can assist you in configuring DNS entries for domains you own or you can have the Sudo Platform do that on your behalf.
{% endhint %}

A call to the `getSupportedEmailDomains` method allows your user to perform a domain search in order to retrieve a list of supported domains for provisioning an email address.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const domains = await emailClient.getSupportedEmailDomains()
    // `domains` contains a list of supported domains to be used for email address provisioning.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
do {
    let domains = try await emailClient.getSupportedEmailDomains()
    /// `domains` contains a list of supported domains to be used for email address provisioning.
} catch {
    /// Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val domains = withContext(Dispatchers.IO) {
            emailClient.getSupportedEmailDomains()
        }
        // [domains] contains a list of supported domains to be used for email address provisioning.
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
 }
```

{% endtab %}
{% endtabs %}

## Get Configured Email Domains

The Email SDK also provides a method to retrieve the configured email domains in your Sudo Platform account. This is used as part of email send operations to allows appropriate encryption decisions to be made when attempting to send end-to-end encrypted email messages.

A call to the `getConfiguredEmailDomains` method returns a list of these configured email domains.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const configuredDomains = await emailClient.getConfiguredEmailDomains(
        CachePolicy.RemoteOnly
    )
    // `configuredDomains` contains a list of configured domains.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
do {
    let configuredDomains = try await emailClient.getConfiguredEmailDomains()
    /// `configuredDomains` contains a list of configured domains.
} catch {
    /// Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val domains = withContext(Dispatchers.IO) {
            emailClient.getConfiguredEmailDomains()
        }
        // [configuredDomains] contains a list of configured domains.
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
 }
```

{% endtab %}
{% endtabs %}

## List Email Domains

The `listEmailDomains` method retrieves all email domains configured, including metadata about each domain such as whether it supports email masks. This method is the recommended replacement for the deprecated `getSupportedEmailDomains` and `getEmailMaskDomains` methods, providing a unified interface for domain discovery.

Use this method when provisioning new email addresses or email masks to determine which domains are available and what capabilities they support.

{% hint style="info" %}
**Migration Note:** If you are currently using `getSupportedEmailDomains` or `getEmailMaskDomains`, migrate to `listEmailDomains` which combines both into a single call with richer metadata.
{% endhint %}

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
  const domains = await emailClient.listEmailDomains()
  // `domains` contains a list of email domain objects with metadata.
  // Each domain includes whether it supports masks.
  for (const domain of domains) {
    console.log(`Domain: ${domain.domain}, Supports Masks: ${domain.isMaskDomain}`)
  }
} catch {
  // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
do {
    let domains = try await emailClient.listEmailDomains()
    /// `domains` contains a list of email domain objects with metadata.
    for domain in domains {
        print("Domain: \(domain.domain), Supports Masks: \(domain.isMaskDomain)")
    }
} catch {
    /// Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val domains = withContext(Dispatchers.IO) {
            emailClient.listEmailDomains()
        }
        // [domains] contains a list of email domain objects with metadata.
        for (domain in domains) {
            println("Domain: ${domain.domain}, Supports Masks: ${domain.isMaskDomain}")
        }
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

#### Migrating from Deprecated Methods

| Deprecated Method          | Replacement                                                               |
| -------------------------- | ------------------------------------------------------------------------- |
| `getSupportedEmailDomains` | `listEmailDomains()` — filter results where you need provisioning domains |
| `getEmailMaskDomains`      | `listEmailDomains()` — filter by `isMaskDomain == true`                   |

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Before (deprecated):
const supportedDomains = await emailClient.getSupportedEmailDomains()
const maskDomains = await emailClient.getEmailMaskDomains()

// After (recommended):
const allDomains = await emailClient.listEmailDomains()
const maskDomains = allDomains.filter(d => d.isMaskDomain)
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Before (deprecated):
let supportedDomains = try await emailClient.getSupportedEmailDomains()
let maskDomains = try await emailClient.getEmailMaskDomains()

// After (recommended):
let allDomains = try await emailClient.listEmailDomains()
let maskDomains = allDomains.filter { $0.isMaskDomain }
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Before (deprecated):
val supportedDomains = emailClient.getSupportedEmailDomains()
val maskDomains = emailClient.getEmailMaskDomains()

// After (recommended):
val allDomains = emailClient.listEmailDomains()
val maskDomains = allDomains.filter { it.isMaskDomain }
```

{% endtab %}
{% endtabs %}

## Check Email Address Availability

When choosing a unique email address, it is important to give your user feedback as to whether the email address they are attempting to provision is valid or not. This is achieved by using the `checkEmailAddressAvailability` method.

The first input property, `localParts`, takes in a set of [local parts](https://en.wikipedia.org/wiki/Email_address#Local-part) to validate and check if they are available. If any of the local parts are invalid or the input contains more than five local parts to check, an invalid argument error will be returned.

The second input property, `domains`, should be a set of email domains exclusively supported by the email service. It is recommended to use the `getSupportedEmailDomains` method to retrieve this set. If an unsupported domain is supplied, an invalid email domain error will be returned.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const localParts = Set(['johndoe', 'jane.citizen'])
    const domains = await emailClient.getSupportedEmailDomains(
        CachePolicy.RemoteOnly
    )
    const emailAddresses = await emailClient.checkEmailAddressAvailability({
        localParts,
        domains,
    })
    // For example, if the domains list is:
    // [
    //    "example.com",
    //    "sudoplatform.com"
    // ]
    // And all emails are available for both domains, this would return a result 
    // that looks like this:
    // [
    //    "johndoe@example.com",
    //    "johndoe@sudoplatform.com",
    //    "jane.citizen@example.com",
    //    "jane.citizen@sudoplatform.com"
    // ]
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
let localParts = ["johndoe", "jane.citizen"]
/// In reality, this would be a subset of the result of `getSupportedDomains`.
let supportedDomains = ["example.com", "sudoplatform.com"]
do {
    let checkAddressInput = CheckEmailAddressAvailabilityInput(
        localParts: [localPart],
        domains: [domain]
    )
    let validAddresses = try await emailClient.checkEmailAddressAvailability(
        withInput: checkAddressInput
    )
    /// If all emails were available for both domains, this would return a result that looks like this:
    /// [
    ///    "johndoe@example.com",
    ///    "johndoe@sudoplatform.com",
    ///    "jane.citizen@example.com",
    ///    "jane.citizen@sudoplatform.com"
    /// ]
} catch {
    /// Handle/notify user of error
}

```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val localParts = listOf("johndoe", "jane.citizen")
        val supportedDomains = emailClient.getSupportedEmailDomains()
        val input = CheckEmailAddressAvailabilityInput(
            localParts = localParts,
            domains = supportedDomains
        )
        val emailAddresses = emailClient.checkEmailAddressAvailability(input)
        // If all emails are available for both domains, 
        // this would return a result that looks like this:
        // [
        //    "johndoe@example.com",
        //    "johndoe@sudoplatform.com",
        //    "jane.citizen@example.com",
        //    "jane.citizen@sudoplatform.com"
        // ]
     } catch (e: EmailAddressException) {
        // Handle/notify user of error
     }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
All email addresses will be lower-cased in the return result. For example a local part input of `JoHnDoE` would be returned as `johndoe...`.
{% endhint %}

## Provision Email Address

Once an available email address has been found, it can be used to provision a valid email address for a Sudo. An email address can be provisioned by calling the `provisionEmailAddress` method and will return the newly provisioned email address object.

{% hint style="warning" %}
The input email address must be valid and available, so it is highly recommended that you use the [Check Email Address Availability](#check-email-address-availability) API to validate the email address first.
{% endhint %}

An `ownershipProofToken` is required as part of the input. This ties together the Sudo and email address such that the Sudo becomes the owner of the email address. Use the `getOwnershipProof` method on the `SudoProfilesClient` in the **Sudo** **Profiles SDK** in order to obtain an `ownershipProofToken`. See the [Sudo](/guides/sudos) section for more information.

An optional `alias` can also be provided as part of the input to provision an email address. The alias can be updated after initially provisioning the email address as discussed in the [Updating Metadata of an Email Address](#updating-metadata-of-an-email-address) section.

When an email address is provisioned, a set of standard email folders is also created and associated with the newly provisioned email address. Find out more about managing email folders [here](/guides/email/email-addresses/manage-email-folders).

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const emailAddressInput: string = // Found via the check email address availability API
const ownershipProofToken: string = // Found via the get ownership proof API (See "Sudos" section)
try {
    const emailAddress = await emailClient.provisionEmailAddress({
        emailAddressInput,
        ownershipProofToken,
        alias: 'John Smith' // Optionally supply an alias
    })
} catch {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift

let emailAddress = self.input.emailAddress  /// String of the form "\(localPart)@\(domain)"
let ownershipProofToken = self.input.ownershipProofToken   /// Found via the get ownership proof API (See "Sudos" section)
do {
    let provisionAddressInput = ProvisionEmailAddressInput(
        emailAddress: emailAddress,
        ownershipProofToken: ownershipProofToken,
        alias: "Fred Smith"   /// optional alias for the provisioned email address
    )
    let provisionedEmailAddress = try await emailClient.provisionEmailAddress(
        withInput: provisionAddressInput
    )
    /// `provisionedEmailAddress` contains the newly provisioned email address.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val emailAddress: String = // Found via the check email address availability API
val ownershipProofToken: String = // Found via the getOwnershipProof API (See "Sudos" section)
launch {
    try {
        val input = ProvisionEmailAddressInput(
            emailAddress = emailAddress,
            ownershipProofToken = ownershipProofToken,
            alias = 'John Smith' // Optionally supply an alias
        )
        val emailAddress = withContext(Dispatchers.IO) {
            emailClient.provisionEmailAddress(input)
        }
        // The returned [emailAddress] has been provisioned
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
 }
```

{% endtab %}
{% endtabs %}

### Entitlements

In order to be able to provision an email address, the user must be entitled. To provision an email address the following entitlement checks are performed:

1. The user must not have more Sudo Profiles than permitted by their `sudoplatform.sudo.max` entitlement.
2. The user is entitled to provision email addresses by the `sudoplatform.email.emailAddressUserEntitled` entitlement.
3. The Sudo Profile against which the new email address is to be provisioned has at least 1 remaining `sudoplatform.email.emailAddressMaxPerSudo` entitlement to consume.

If these entitlements' checks do not succeed, the `provisionEmailAddress` API fails with an insufficient entitlements error.

{% hint style="info" %}
See [Email Entitlements](/guides/email/email-addresses/email-entitlements) for an overview of how the Email service integrates with the Sudo Platform Entitlements system.
{% endhint %}

## Deprovision Email Address

An email address can be deprovisioned using the `deprovisionEmailAddress` method by passing in the `id` of an existing email address object. The email address object that was deprovisioned will be returned and will no longer be usable for provisioning another email address.

{% hint style="warning" %}
By deprovisioning the email address, the user's address and all associated user data including messages and folders will be deleted; the email address will no longer be usable. The deprovisioned address will also be held indefinitely and be blocked for re-use from a subsequent provision call.
{% endhint %}

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input email address id however makes sense for your implementation.
const emailAddressId = emailAddress.id
try {
    const emailAddress = await emailClient.deprovisionEmailAddress(
        emailAddressId
    )
    // The returned `emailAddress` has now been deprovisioned.
} catch {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input email address id however makes sense for your implementation.
let provisionedEmailAddressId = self.input.emailAddressId
do {
    let deprovisionedEmailAddress = try await emailClient.deprovisionEmailAddress(
        provisionedEmailAddressId
    )
    /// `deprovisionedEmailAddress` is the record that was just deprovisioned.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input email address id however makes sense for your implementation.
val emailAddressId = emailAddress.id
launch {
    try {
        val emailAddress = withContext(Dispatchers.IO) {
            emailClient.deprovisionEmailAddress(emailAddressId)
        }
        // The returned [emailAddress] has now been deprovisioned.
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Updating Metadata of an Email Address

The metadata of an email address object can be updated by calling the `updateEmailAddressMetadata` method. Currently, the only attribute that can be updated on an email address object is the `alias`. The `alias` is an encrypted optional attribute on the email address object which can be modified by the user.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input email address id however makes sense for your implementation.
try {
    const id = await emailClient.updateEmailAddressMetadata({
        id,
        values: { alias: 'John Smith' },
    })
    // `id` contains the identifier of the newly updated email address object.
} catch {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input email address id however makes sense for your implementation.
let emailAddressId = input.emailAddress.id
let updateEmailAddressMetadataInput = UpdateEmailAddressMetadataInput(
    id: emailAddress.id,
    values: UpdateEmailAddressMetadataValues(alias: "newAlias")
)
do {
    let updatedEmailAddressId = try await client.updateEmailAddressMetadata(
        withInput: updateEmailAddressMetadataInput
    )
    /// `updatedEmailAddressId` contains the identifier of the newly updated email address object.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

<pre class="language-kotlin"><code class="lang-kotlin">/// Collect the input email address id however makes sense for your implementation.
<strong>val emailAddressId = emailAddress.id
</strong>launch {
    try {
        val input = UpdateEmailAddressMetadataInput(
            id = emailAddress.id,
            alias = "newAlias"
        )
        val updatedEmailAddressId = withContext(Dispatchers.IO) {
            emailClient.updateEmailAddressMetadata(input)
        }
        // The returned [updatedEmailAddressId] contains the identifier of the newly updated email address object.
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
</code></pre>

{% endtab %}
{% endtabs %}

## Retrieving Existing Email Addresses

Previously provisioned email addresses can be accessed in two ways: via its identifier ([Single Email Address by Id](#using-email-address-id)), or via a broad multi access method ([Multiple Provisioned Email Addresses](#all-provisioned-email-addresses)).

{% hint style="warning" %}
If an email address has been provisioned and then deprovisioned, it will no longer be available via the methods discussed in this section.
{% endhint %}

A fetch of single or multiple email addresses can be performed remotely or locally by specifying the appropriate [CachePolicy](/guides/email/integrate-the-email-sdk/caching-1) as part of the input.

### Single Email Address by Id

To retrieve a single email address given its unique `id`, use the `getEmailAddress` method. This method will return the record if it exists.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const emailAddress = await emailClient.getEmailAddress({
        id,
        cachePolicy: CachePolicy.RemoteOnly,
    })
    // `emailAddress` contains the email address object, else `undefined` if not found.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input address of the email address object.
let emailAddressId = input.address.id
let getEmailAddressInput = GetEmailAddressInput(id: emailAddressId)
do {
    let emailAddress = try await emailClient.getEmailAddress(
        withInput: getEmailAddressInput
    )
    /// `emailAddress` contains the email address object, else `nil` if not found.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val emailAddressId = emailAddress.id
launch {
    try {
        val input = GetEmailAddressInput(
            id = emailAddressId
        )
        val emailAddress = withContext(Dispatchers.IO) {
            emailClient.getEmailAddress(input)
        }
        // [emailAddress] contains the email address object, else [null] if not found.
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Multiple Provisioned Email Addresses

The ability to retrieve multiple or all email addresses available to the user is supported. These results can be [paginated](/guides/email/integrate-the-email-sdk/pagination).

A call to a list API will return a `ListEmailAddressesResult` with a `status` and depending on the status, a list of matching `items` and a `nextToken` to support pagination. If no results matching the input are found, the result will contain empty items. There can be three possible statuses returned:

| Status  | Definition                                                                                                                                                                                                                          |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Success | A list of all requested email addresses are returned.                                                                                                                                                                               |
| Partial | A list of all email addresses that were successfully fetched and unencrypted are returned as well as a list of all email addresses that failed to unencrypt successfully, including an error indicating the reason for the failure. |
| Failure | All email addresses failed to be fetched or unencrypted. Contains an error indicating the reason for the failure.                                                                                                                   |

{% hint style="warning" %}
An email address may fail to be unencrypted if the version of the client is not up-to-date or if the required cryptographic key is missing from the client device.
{% endhint %}

#### All Provisioned Email Addresses

To retrieve multiple email addresses that are owned by the signed in user, call the `listEmailAddresses` method.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const result = await emailClient.listEmailAddresses({
        cachePolicy: CachePolicy.RemoteOnly,
        limit: 20,
        nextToken,
    })
    if (result.status === ListOperationResultStatus.Success) {
        // `result` contains the list of items matching the input.
        // Page through the results if result.nextToken != undefined. 
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
let listEmailAddressesInput = ListEmailAddressesInput(
    limit: 20,
    nextToken: nil
)
do {
    let output = try await emailClient.listEmailAddresses(
        withInput: input
    )
    /// `output` contains a `ListOutput` item of results matching the call.
    /// `output.items` contains an array of `EmailAddress`
} catch {
    /// Handle/notify user
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val input = ListEmailAddressesInput(
            limit = 20,
            nextToken = nextToken
        )
        val result = withContext(Dispatchers.IO) { 
            emailClient.listEmailAddresses(input)
        }
        when (result) {
            is ListAPIResult.Success -> {
                // [result.items] contains the list of items matching the input.
                // Page through the results if [output.nextToken] != null.
            }
            is ListAPIResult.Partial -> {
                // [result.items] contains the list of items matching the input that decrypted successfully.
                // [result.failed] contains the list of items that failed decryption with associated error.
                //  Page through the results if [partial.nextToken] != null.
            }
        }
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

**All Provisioned Email Addresses for a Sudo**

To retrieve multiple email addresses that are owned by a particular Sudo, call the `listEmailAddressesForSudoId` method by passing in the `id` of the Sudo to query.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input sudo id however makes sense for your implementation.
const sudoId = sudo.id
try {
    const result = await emailClient.listEmailAddressesForSudoId({
        sudoId,
        cachePolicy: CachePolicy.RemoteOnly,
        limit: 20,
        nextToken,
    })
    if (result.status === ListOperationResultStatus.Success) {
        // `result` contains the list of items matching the input.
        // Page through the results if result.nextToken != undefined. 
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input sudo id however makes sense for your implementation.
let sudoId = sudo.id
let listEmailAddressesForSudoIdInput = ListEmailAddressesForSudoIdInput(
    sudoId: sudoId,
    limit: 20,
    nextToken: nil
)
do {
    let output = try await emailClient.listEmailAddressesForSudoId(
        withInput: listEmailAddressesForSudoId
    )
    /// `output` contains a `ListOutput` item of results matching the call.
    /// `output.items` contains an array of `EmailAddress`
} catch {
    /// Handle/notify user
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input sudo id however makes sense for your implementation.
val sudoId = sudo.id
launch {
    try {
        val input = ListEmailAddressesForSudoIdInput(
            sudoId = sudoId,
            limit = 20,
            nextToken = nextToken
        )
        val result = withContext(Dispatchers.IO) { 
            emailClient.listEmailAddressesForSudoId(input)
        }
        when (result) {
            is ListAPIResult.Success -> {
                // [result.items] contains the list of items matching the input.
                // Page through the results if [output.nextToken] != null.
            }
            is ListAPIResult.Partial -> {
                // [result.items] contains the list of items matching the input that decrypted successfully.
                // [result.failed] contains the list of items that failed decryption with associated error.
                //  Page through the results if [partial.nextToken] != null.
            }
        }
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
For the best performance depending on your use case, look up email addresses by Sudo.
{% endhint %}

{% hint style="info" %}
By default, list email addresses API have a limit of 1MB of data when no limit is supplied.&#x20;
{% endhint %}


# Manage Email Folders

Allow your users the flexibility to manage their email address with email folders

## Email Folders

When an email address is [provisioned](/guides/email/email-addresses/manage-email-addresses#provision-email-address) through the **Email SDK,** a set of standard folders are also created and associated with the provisioned email address. The following table describes the standard folders:

| Folder | Function                                                                                                                                                                  |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| INBOX  | Inbound messages are assigned to the INBOX folder.                                                                                                                        |
| SENT   | Outbound messages which have completed sending are assigned to the SENT folder.                                                                                           |
| OUTBOX | Reserved for future use.                                                                                                                                                  |
| TRASH  | Messages can be moved to the TRASH folder. Messages in this folder will automatically be deleted after a set period of time depending on the application's configuration. |

### Retrieving Email Folders

Email folders associated with an email address are available in the `folders` attribute of that email address and are retrieved along with the email address.

Email folders associated with an email address can also be retrieved directly through the `listEmailFoldersForEmailAddressId` method by passing in the `id` of the email address to query. This returns a `ListOutput` object with the list of email folder objects which contain information used to identify each folder and to facilitate moving of messages between folders.

The `EmailFolder` object contains useful information such as the name of the folder, the total size of all messages assigned to the folder and the total count of unseen messages assigned to the folder.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input email address id however makes sense for your implementation.
const emailAddressId = emailAddress.id
try {
    const listOutput = await emailClient.listEmailFoldersForEmailAddressId({
        emailAddressId,
        cachePolicy: CachePolicy.RemoteOnly,
        nextToken,
    })
    // `listOutput` contains the list of items matching the input. 
    // Page through the results if listOutput.nextToken != nil.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the input email address id however makes sense for your implementation.
let emailAddressId = emailAddress.id
let input = ListEmailFoldersForEmailAddressIdInput(
    emailAddressId: emailAddressId,
    limit: 10,
    nextToken: nil
)
do {
    let output = try await emailClient.listEmailFoldersForEmailAddressId(
        withInput: input
    )
    // `output` contains the `ListOutput` of items matching the input.
    // `output.items` contains an array of `EmailFolder`
    // Page through the results if output.nextToken != undefined.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the input email address id however makes sense for your implementation.
val emailAddressId = emailAddress.id
launch {
    try {
        val input = ListEmailFoldersForEmailAddressIdInput(
            emailAddressId = emailAddressId,
            limit = 10,
            nextToken = null
        )
        val listOutput = withContext(Dispatchers.IO) { 
            emailClient.listEmailFoldersForEmailAddressId(input)
        }
        // [listOutput] contains the [ListOutput] of items matching the input.
        // [listOutput.items] contains a list of [EmailFolder]s
        // Page through the results if listOutput.nextToken != null.
    } catch (e: EmailFolderException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Move Email Messages between Folders

Email messages can also be moved between folders. For example an email message stored in the "inbox" folder can be moved to the "trash" folder. This is done through the update email messages API as described [here](/guides/email/email-addresses/send-an-email#updating-metadata-of-email-messages). The email message object contains a `previousFolderId` property which specifies the identifier of the folder that the message was previously in. An example implementation of using the `updateEmailMessages` method to move a message to another folder is:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input message ids and email address id however makes sense for your implementation.
const emailAddressId = emailAddress.id
const ids = ['message-id-1', 'message-id-2']
try {
   // List the email folders and find the "trash" folder.
   const trashFolder = await emailClient.listEmailFoldersForEmailAddressId({
     emailAddressId,
     cachePolicy: CachePolicy.RemoteOnly
   }).then((result) => result.items.find(folder) => folder.folderName === 'TRASH')
 
   // Update an email message using the "trash" folder id.
   await emailClient.updateEmailMessages({
        ids,
        values: { folderId: trashFolder.id },
    })
   // Each of the input messages should now be assigned to the "trash" folder.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the input message ids and email address id however makes sense for your implementation.
let emailAddressId = emailAddress.id
let ids = ['message-id-1', 'message-id-2']
let input = ListEmailFoldersForEmailAddressIdInput(
    emailAddressId: emailAddressId,
    limit: 10,
    nextToken: nil
)
do {
    // List the email folders and find the "trash" folder.
    let output = try await emailClient.listEmailFoldersForEmailAddressId(
        withInput: input
    )
    let trashFolderIds = output.items.filter { $0.id.contains("TRASH") }
    guard let trashFolderId = trashFolderIds.first else {
        return
    }
    let input = UpdateEmailMessagesInput(
        ids: ids,
        values: UpdateEmailMessagesValues(
            folderId: trashFolderId,
            seen: true
        )
    ) 
    // Update the email messages using the "trash" folder id.
    let result = try await emailClient.updateEmailMessages(withInput: input)
    switch result {
    case .success:
        // All email messages assigned to the TRASH folder.
    case. partial:
        // `result.successItems` contains an array of all the email message IDs 
        // that were assigned to the TRASH folder successfully.
        // `result.failureItems` cotains an array of all the email message IDs
        // that failed to be assigned to the TRASH folder.
    case .failure:
        // All email messages failed to be assigned to the TRASH folder.
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the input draft message ids and email address id however makes sense for your implementation.
val emailAddressId = emailAddress.id
val ids = listOf("message-id-1", "message-id-2")
launch {
    try {
        // List the email folders and find the "trash" folder.
        val listInput = ListEmailFoldersForEmailAddressIdInput(
            emailAddressId = emailAddressId,
            limit = 10,
            nextToken = null
        )
        val emailFoldersList = withContext(Dispatchers.IO) { 
            emailClient.listEmailFoldersForEmailAddressId(listInput)
        }
        val trashFolder = (emailFoldersList.filter { it.folderName == "TRASH" }).first()
        
        // Update the email messages using the "trash" folder id.
        val updateInput = UpdateEmailMessagesInput(
            ids = ids,
            values = UpdateEmailMessagesInput.UpdatableValues(folderId = trashFolder.id)
        )
        val result = withContext(Dispatchers.IO) {
            emailClient.updateEmailMessages(updateInput)
        }
        when (result) {
            is BatchOperationResult.SuccessOrFailureResult -> {
                // [result.status] contains the status of the batch update operation.
            }
            is BatchOperationResult.Partial -> {
                // [result.successValues] contains the list of items in which the update operation succeeded.
                // [result.failureValues] contains the list of items in which the update operation failed.
            }
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Creating Custom Email Folders

Users can create custom folders for whatever purpose suits them. These behave in much the same way as the standard folders. To create a custom email folder, call the `createCustomEmailFolder` method with the name of the custom folder and the id of the email address the folder should be associated with. Duplicate folder names will not be allowed. Once a folder has been created, messages can be moved in and out of them the same way as described above.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the email address id however makes sense for your implementation.
const emailAddressId = emailAddress.id
const customFolderName = "Custom"
try {
   // Create a custom email folder
   const customFolder = await emailClient.createCustomEmailFolder({
        emailAddressId,
        customFolderName,
    })
   // The customEmailFolder will contain the newly created email folder and all of its metadata
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the email address id however makes sense for your implementation.
let emailAddressId = emailAddress.id
let input = CreateCustomEmailFolderInput(
    emailAddressId: emailAddress.id, 
    customFolderName: "Custom"
)
do {
    let customEmailFolder = try await client.createCustomEmailFolder(withInput: input)
    // The customEmailFolder will contain the newly created email folder and all of its metadat
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the email address id however makes sense for your implementation.
val emailAddressId = emailAddress.id
val input = CreateCustomEmailFolderInput(emailAddress.id, "Custom")
launch {
    try {
        val customEmailFolder = withContext(Dispatchers.IO) {
            emailClient.createCustomEmailFolder(input)
        }
        // The customEmailFolder will contain the newly created email folder and all of its met
    } catch (e: EmailFolderException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Deleting Custom Email Folders

Users can also delete any custom email folders they have created. The standard folders associated with the email address cannot be deleted. To delete a custom email folder, call the `deleteCustomEmailFolder` method with the id of the custom email folder and the id of its associated email address. The method will return a copy of the deleted folder, or nothing if the folder could not be found. When a custom email folder is deleted, any messages in that folder will be moved to the TRASH folder.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the folder id and email address id however makes sense for your implementation.
const customFolderId = customEmailFolder.id
const emailAddressId = emailAddress.id
try {
   // Delete a custom email folder
   const customFolder = await emailClient.deleteCustomEmailFolder({
        emailFolderId: customFolderId,
        emailAddressId,
   })
   // The customEmailFolder will contain the deleted email folder or undefined
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the folder id and email address id however makes sense for your implementation.
let customFolderId = customEmailFolder.id
let emailAddressId = emailAddress.id
let input = DeleteCustomEmailFolderInput(
    emailFolderId: customFolderId, 
    emailAddressId: emailAddressId
)
do {
    let customEmailFolder = try await client.deleteCustomEmailFolder(withInput: input)
    // The customEmailFolder will contain the deleted email folder or nil
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the custom email folder id and email address id however makes sense for your implementation.
val customEmailFolderId = customEmailFolder.id
val emailAddressId = emailAddress.id
val input = DeleteCustomEmailFolderInput(customEmailFolderId, emailAddressId)
launch {
    try {
        val customEmailFolder = withContext(Dispatchers.IO) {
            emailClient.deleteCustomEmailFolder(input)
        }
        // The customEmailFolder will contain the deleted email folder or null
    } catch (e: EmailFolderException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Updating Custom Email Folders

Custom email folders can be updated via the `updateCustomEmailFolder` method. At this point, only the name of a custom folder can be updated.&#x20;

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the folder id and email address id however makes sense for your implementation
const emailAddressId = emailAddress.id
const customFolderId = customFolder.id
try {
    const inputValues: CustomEmailFolderUpdateValuesInput = {
          customFolderName: "new-folder-name",
    }
    
    const updatedFolder = await emailClient.updateCustomEmailFolder({
        emailAddressId: emailAddressId,
        emailFolderId: customFolderId,
        values: inputValues,
    })
    // updatedFolder will contain the folder with its updated values
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the folder id and email address id however makes sense for your implementation.
let customFolderId = customEmailFolder.id
let emailAddressId = emailAddress.id
let inputValues = UpdateCustomEmailFolderValues(customFolderName: "new-folder-name")
let input = UpdateCustomEmailFolderInput(
    emailFolderId: customFolderId,
    emailAddressId: emailAddressId,
    values: inputValues
)
do {
    let updatedEmailFolder = try await client.updateCustomEmailFolder(withInput: input)
    // The updatedEmailFolder will contain the email folder with its updated values
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the custom email folder id and email address id however makes sense for your implementation.
val customEmailFolderId = customEmailFolder.id
val emailAddressId = emailAddress.id
val input = UpdateCustomEmailFolderInput(
    emailAddressId,
    customEmailFolderId,
    "new-folder-name"
)
launch {
    try {
        val updatedEmailFolder = withContext(Dispatchers.IO) {
            emailClient.updateCustomEmailFolder(input)
        }
        // The updatedEmailFolder will contain the email folder with its updated values
    } catch (e: EmailFolderException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Deleting All Messages In A Folder

All messages can be deleted from a folder at once by calling the `deleteEmailMessagesForFolderId`  method. This will delete all the messages currently in the folder indicated. Because there could be a large number of messages, and it could take a significant amount of time to complete, this is done asynchronously. The method does not return any indication of the result of the deletions. Deletion can be confirmed via subscriptions, or polling the list message endpoints.

The method accepts a folder id, and an optional `hardDelete` parameter indicating what type of delete to perform, as indicated in the table below.

| \`hardDelete\` value | Behaviour                                                                           |
| -------------------- | ----------------------------------------------------------------------------------- |
| `true`(default)      | Messages are completely deleted and no longer appear in the users account anywhere. |
| `false`              | Messages are moved to the user's `TRASH` folder.                                    |

Note: If the folder the messages are being deleted from is the user's `TRASH` folder, the `hardDelete` parameter is always treated as `false` .

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the folder id and email address id however makes sense for your implementation
const emailAddressId = emailAddress.id
const emailFolderId = emailFolder.id
try {
    const result = await emailClient.deleteMessagesForFolderId({
      emailAddressId: emailAddressId
      emailFolderId: emailFolderId,
      hardDelete: false,
    })
    // result will be the emailFolderId if the deletion process was initiated successfully
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the folder id and email address id however makes sense for your implementation.
let emailFolderId = emailFolder.id
let emailAddressId = emailAddress.id
let deleteMessagesForFolderIdInput = DeleteMessagesForFolderIdInput(
    emailFolderId: emailFolderId,
    emailAddressId: emailAddressId,
    hardDelete: false
)

do {
    let result = try await client.deleteMessagesForFolderId(withInput: deleteMessagesForFolderIdInput)
    // The result will be the emailFolderId if the deletion process was initiated successfully
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the folder id and email address id however makes sense for your implementation.
val emailAddressId = emailAddress.id
val emailFolderId = emailFolder.id
launch {
    try {
        val input = DeleteMessagesForFolderIdInput(
            emailFolderId = emailFolderId,
            emailAddressId = emailAddressId,
            hardDelete = false
        )
        val deleteOutput = withContext(Dispatchers.IO) { 
            emailClient.deleteMessagesForFolderId(input)
        }
    // The deleteOutput will be the emailFolderId if the deletion process was initiated successfully
    } catch (e: EmailFolderException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}


# Send & Receive Email Messages

Provides your users the ability to perform email communications with a Sudo

## Sending Email Messages

The **Email SDK** can handle sending email messages. Sending messages requires a provisioned email address as discussed in [Manage Email Addresses](/guides/email/email-addresses/manage-email-addresses#provision-email-address).&#x20;

{% hint style="info" %}
We support using a raw [RFC 822](https://tools.ietf.org/html/rfc822) payload to allow flexible email composition within your application.
{% endhint %}

The `sendEmailMessage` method is used to send an email message. The first input property, `emailMessageHeader`, contains header fields such as `from`, `to`, `cc`, `bcc`, `replyTo` and `subject` which adhere to the RFC 822 standard. Further input properties include the message `body`, `attachments` and `inlineAttachments.` The last input property, `senderEmailAddressId`, must match the `id` of the email address from the `from` field in the header input. A call to this method returns the `id` and `created` timestamp of the sent email message to allow for a client to sync data from a remote source.

### Email Message End-to-end Encryption

When sending an email to in-network addresses (i.e. recipients also have a provisioned email address within the Sudo Platform), the email message will be end-to-end encrypted. When sending an email to out-of-network addresses (i.e. recipients email addresses are not registered email address within the Sudo Platform) the email message will not be end-to-end encrypted.&#x20;

{% hint style="warning" %}
Sending an email to a mixture of in-network and out-of-network recipients in a single email message will not be end-to-end encrypted.
{% endhint %}

End-to-end encrypted email messages contain attachments which hold the encrypted RFC 822 data and the cryptographic keys used to decrypt the message. These attachments are identified based on the properties in the following table:

<table><thead><tr><th width="195"></th><th width="157">fileName</th><th width="187">mimeType</th><th>contentId</th></tr></thead><tbody><tr><td><strong>Key Attachment/s</strong></td><td>"Secure Data <em>n</em>"</td><td>"application/x-sudoplatform-key"</td><td>"securekeyexchangedata@sudoplatform.com"</td></tr><tr><td><strong>Body Attachment</strong></td><td>"Secure Email"</td><td>"application/x-sudoplatform-body"</td><td>"securebody@sudoplatform.com"</td></tr></tbody></table>

{% hint style="danger" %}
A single email message can be sent to a maximum of 20 recipients at once by default. Adding recipients to the email message which exceed this limit will throw a `LimitExceededError`.
{% endhint %}

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input headers, body, attachments and sender email address id 
// however makes sense for your implementation.
const senderEmailAddressId: string = // ...
const emailMessageHeader: InternetMessageFormatHeader = {
    from: { emailAddress: "from@bar.com" },
    to: [{ emailAddress: "to@bar.com" }],
    cc: [{ emailAddress: "cc@bar.com" }],
    bcc: [{ emailAddress: "bcc@bar.com" }],
    replyTo: [{ emailAddress: "replyTo@bar.com" }],
    subject: "Example subject line",
}
const emailAttachment: EmailAttachment = {
    filename: "fooAttachment.pdf",
    contentId: uuid.v4(),
    mimeType: "application/pdf",
    inlineAttachment: false,
    data: // ... Some pdf data
}
const inlineAttachment: EmailAttachment = {
    filename: "fooImage.png",
    contentId: uuid.v4(),
    mimeType: "image/png",
    inlineAttachment: true,
    data: // ... Some image data
}
try {
    const input: SendEmailMessageInput = {
        senderEmailAddressId: senderEmailAddressId,
        emailMessageHeaders: emailMessageHeader,
        body: "An example email body",
        attachments: [emailAttachment],
        inlineAttachments: [inlineAttachment],
    }
    const result = await emailClient.sendEmailMessage(input)
    // `result` contains the identifier and created timestamp associated with the sent email message. You can use this to access the data of the email message.
} catch {
    // Handle/notify user of error 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input headers, body, attachments and sender email address id 
/// however makes sense for your implementation.
let senderEmailAddressId: String = // ...
let emailMessageHeader = InternetMessageFormatHeader(
    from: EmailAddressDetail(emailAddress: "from@bar.com"),
    to: [EmailAddressDetail(emailAddress: "to@bar.com")],
    cc: [EmailAddressDetail(emailAddress: "cc@bar.com")],
    bcc: [EmailAddressDetail(emailAddress: "bcc@bar.com")],
    replyTo: [EmailAddressDetail(emailAddress: "replyTo@bar.com")],
    subject: "Example subject line"
)
let emailAttachment = EmailAttachment(
    filename: "fooAttachment.pdf",
    contentId: UUID().uuidString,
    mimetype: "application/pdf",
    inlineAttachment: false,
    data: // ... Some pdf data
)
let inlineAttachment = EmailAttachment(
    filename: "fooImage.png",
    contentId: UUID().uuidString,
    mimetype: "image/png",
    inlineAttachment: true,
    data: // ... Some image data
)
do {
    let input = SendEmailMessageInput(
        senderEmailAddressId: senderEmailAddressId,
        emailMessageHeaders: emailMessageHeader,
        body: "An example email body",
        attachments: [emailAttachment],
        inlineAttachments: [inlineAttachment]
    )
    let result = try await emailClient.sendEmailMessage(
        withInput: input
    )
    /// `result` contains the identifier and created timestamp associated with the sent email message. You can use this to access the data of the email message.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input headers, body, attachments and sender email address id 
// however makes sense for your implementation.
val senderEmailAddressId: String = // ...
val emailMessageHeader = InternetMessageFormatHeader(
    from = EmailMessage.EmailAddress("from@bar.com"),
    to = listOf(EmailMessage.EmailAddress("to@bar.com")),
    cc = listOf(EmailMessage.EmailAddress("cc@bar.com")),
    bcc = listOf(EmailMessage.EmailAddress("bcc@bar.com")),
    replyTo = listOf(EmailMessage.EmailAddress("replyTo@bar.com")),
    subject = "Example subject line",
)
val emailAttachment = EmailAttachment(
    fileName = "fooAttachment.pdf",
    contentId = UUID.randomUUID().toString(),
    mimeType = "application/pdf",
    inlineAttachment = false,
    data = // ... Some pdf data
)
val inlineAttachment = EmailAttachment(
    fileName = "fooImage.png",
    contentId = UUID.randomUUID().toString(),
    mimeType = "image/png",
    inlineAttachment = true,
    data = // ... Some image data
)
launch {
    try {
        val input = SendEmailMessageInput(
            senderEmailAddressId = senderEmailAddressId,
            emailMessageHeaders = emailMessageHeader,
            body = "An example email body",
            attachments = listOf(emailAttachment),
            inlineAttachments = listOf(inlineAttachment),
        )
        val result = withContext(Dispatchers.IO) {
            emailClient.sendEmailMessage(input)
        }
        // [result] contains the identifier and created timestamp associated with the sent email message. You can use this to access the data of the email message.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Entitlements

In order to be able to send an email, the user must be entitled. To send an email the following entitlement checks are performed:

1. The user must be entitled to send emails by the`sudoplatform.email.emailMessageSendUserEntitled` entitlement.
2. The email address the user is sending from must have sufficient storage capacity available as determined by the `sudoplatform.email.emailStorageMaxPerEmailAddress` entitlement.

If these entitlements' checks do not succeed, the `sendEmailMessage` fails with an insufficient entitlements error.

{% hint style="info" %}
See [Email Entitlements](/guides/email/email-addresses/email-entitlements) for an overview of how the Email service integrates with the Sudo Platform Entitlements system.
{% endhint %}

## Receiving Email Messages

Email messages can be received via subscribing to email message events or through polling on the email message retrieval APIs. See the [Subscribing to Email Message Events](#subscribing-to-email-message-events) section for information on the subscription method and see the [Retrieving Email Messages](#retrieving-email-messages) section for information on the polling method.

### Entitlements

While previously received emails are always able to be retrieved, in order to be able to receive new emails, the user must be entitled. To receive an email the following entitlement checks are performed:

1. The user must be entitled to receive emails by the`sudoplatform.email.emailMessageReceiveUserEntitled` entitlement.
2. The email address the email is addressed to must have sufficient storage capacity available as determined by the `sudoplatform.email.emailStorageMaxPerEmailAddress` entitlement.

If these entitlements checks do not succeed, the incoming email is rejected with a reason indicating that the storage quota has been exceeded allowing the sending email service to retry.

{% hint style="info" %}
See [Email Entitlements](/guides/email/email-addresses/email-entitlements) for an overview of how the Email service integrates with the Sudo Platform Entitlements system.
{% endhint %}

## Retrieving Email Messages

Sent and received email messages can be accessed in two ways: via its identifier ([Single Email Message by Id](#single-email-message-by-id)), or via a multi access method ([Multiple Email Messages](#multi-access-messages)).

{% hint style="info" %}
These retrieval methods return the email message metadata but not the content of the message. Email message metadata consists of the information such as: addressees, sender, subject, folder, timestamps, direction, whether or not it has attachments.&#x20;

To learn how to access the RFC 822 message content of the email message, see [Accessing RFC 822 Message Data](#accessing-rfc-822-message-data).
{% endhint %}

{% hint style="warning" %}
If an email message has been deleted, it will no longer be available for access.
{% endhint %}

A fetch of single or multiple email messages can be performed remotely or locally by specifying the appropriate [CachePolicy](/guides/email/integrate-the-email-sdk/caching-1) as part of the input object.

### Single Email Message by Id

A single email message can be retrieved via its `id` using the `getEmailMessage` method. The `id` is typically returned from successfully [sending an email message](#sending-email). This method will return the record if it exists.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const emailMessage = await emailClient.getEmailMessage({
        id,
        cachePolicy: CachePolicy.RemoteOnly,
    })
    // `emailMessage` contains the email message object, else `undefined` if not found.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input id of the email message object.
let input = GetEmailMessageInput(id: input.messageId)
do {
    let emailMessage = try await emailClient.getEmailMessage(
        withInput: input
    )
    // `emailMessage` contains the email message object, else `nil` if not found.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val emailMessageId = emailMessage.id
launch {
    try {
        val input = GetEmailMessageInput(
            id = emailMessageId
        )
        val emailMessage = withContext(Dispatchers.IO) {
            emailClient.getEmailMessage(input)
        }
        // [emailMessage] contains the email message object, else [null] if not found.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Multiple Email Messages

The ability to retrieve multiple email messages available to the user is supported. These results can be [paginated](/guides/email/integrate-the-email-sdk/pagination).

A call to a list API will return a `ListEmailMessagesResult` with a `status` and depending on the status, a list of matching `items` and a `nextToken` to support pagination. If no results matching the input are found, the result will contain empty items. There can be three possible statuses returned:

| Status  | Definition                                                                                                                                                                                                                        |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Success | A list of all requested email messages are returned.                                                                                                                                                                              |
| Partial | A list of all email messages that were successfully fetched and unencrypted are returned as well as a list of all email messages that failed to unencrypt successfully, including an error indicating the reason for the failure. |
| Failure | All email messages failed to be fetched or unencrypted. Contains an error indicating the reason for the failure.                                                                                                                  |

{% hint style="warning" %}
An email message may fail to be unencrypted if the version of the client is not up-to-date or if the required cryptographic key is missing from the client device.
{% endhint %}

An optional `dateRange` can also be supplied as part of the input to organise results. For listing email messages, the `sortDate` or `updatedAt` timestamps can be specified in order to perform the date range query on. The `sortDate` refers to the time in which an email message is sent or received. The `updatedAt` date refers to the time in which the email message was last updated. This could be due to a change in the message's seen status or the folder that it is assigned to.

{% hint style="warning" %}
Note that both timestamps cannot be specified, otherwise an `InvalidArgument` error will occur.
{% endhint %}

#### All Email Messages

To retrieve multiple email messages that are owned by the signed in user, call the `listEmailMessages` method.&#x20;

{% tabs %}
{% tab title="Typescript" %}

```typescript
const dateRange: EmailMessageDateRange = {
    updatedAt: {
        startDate: // startDate as a `Date` object
        endDate: // endDate as a `Date` object
    }
}
try {
    const result = await emailClient.listEmailMessages({
        dateRange,
        cachePolicy: CachePolicy.RemoteOnly,
        limit: 20,
        nextToken,
    })
    if (result.status === ListOperationResultStatus.Success) {
        // `result` contains the list of items matching the input.
        // Page through the results if result.nextToken != undefined. 
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
let dateRange = EmailMessageDateRange(
    updatedAt: DateRange(
        startDate: // startDate as a `Date` object
        endDate: // endDate as a `Date` object
    )
)
let input = ListEmailMessagesInput(
    dateRange: dateRange,
    sortOrder: .asc,
    limit: 10,
    nextToken: nil
)
do {
    let result = try await emailClient.listEmailMessages(
        withInput: input
    )
    switch result {
    case .success(let successResult):
        // successResult.items contains an array of the email messages for the user.
        // successResult.nextToken will not be `nil` if there are more email messages to retrieve.
    case .partial(let partialResult):
        // partialResult.items contains an array of the email messages successfully 
        //  retrieved and unsealed for this user.
        // partialResult.failed contains an array of email messages retrieved 
        //  for this user, but unsuccessfully unsealed.
        // partialResult.nextToken will not be `nil` if there are more email messages to retrieve
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val dateRange = EmailMessageDateRange(
    updatedAt = DateRange(
        startDate = // startDate as a [Date]
        endDate = // endDate as a [Date]
    )
)
launch {
    try {
        val input = ListEmailMessagesInput(
            dateRange = dateRange,
            sortOrder = SortOrder.DESC,
            limit = 20,
            nextToken = nextToken
        )
        val result = withContext(Dispatchers.IO) { 
            emailClient.listEmailMessages(input)
        }
        when (result) {
            is ListAPIResult.Success -> {
                // [result.items] contains the list of items matching the input.
                // Page through the results if [output.nextToken] != null.
            }
            is ListAPIResult.Partial -> {
                // [result.items] contains the list of items matching the input that decrypted successfully.
                // [result.failed] contains the list of items that failed decryption with associated error.
                //  Page through the results if [partial.nextToken] != null.
            }
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

#### All Email Messages for an Email Address

To retrieve multiple email messages that are assigned to a certain email address, call the `listEmailAddressesForEmailAddressId` method by passing in the `id` of the email address to query.

{% tabs %}
{% tab title="TypeScript" %}

<pre class="language-typescript"><code class="lang-typescript"><strong>// Collect the input email address id however makes sense for your implementation.
</strong>const emailAddressId = emailAddress.id
const dateRange: EmailMessageDateRange = {
    updatedAt: {
        startDate: // startDate as a `Date` object
        endDate: // endDate as a `Date` object
    }
}
try {
    const result = await emailClient.listEmailMessagesForEmailAddressId({
        emailAddressId,
        dateRange,
        cachePolicy: CachePolicy.RemoteOnly,
        limit: 20,
        nextToken,
    })
    if (result.status === ListOperationResultStatus.Success) {
        // `result` contains the list of items matching the input.
        // Page through the results if result.nextToken != undefined. 
    }
} catch {
    // Handle/notify user of errors
}
</code></pre>

{% endtab %}

{% tab title="Swift" %}

```swift
// Collect the input email address id however makes sense for your implementation.
let emailAddressId = emailAddress.id
let dateRange = EmailMessageDateRange(
    updatedAt: DateRange(
        startDate: // startDate as a `Date` object
        endDate: // endDate as a `Date` object
    )
)
let input = ListEmailMessagesForEmailAddressInput(
    emailAddressId: emailAddressId,
    dateRange: dateRange,
    sortOrder: .asc,
    limit: 10,
    nextToken: nil
)
do {
    let result = try await emailClient.listEmailMessagesForEmailAddressId(
        withInput: input
    )
    switch result {
    case .success(let successResult):
        // successResult.items contains an array of the email messages for this email address.
        // successResult.nextToken will not be `nil` if there are more email messages to retrieve.
    case .partial(let partialResult):
        // partialResult.items contains an array of the email messages successfully 
        //  retrieved and unsealed for this email address.
        // partialResult.failed contains an array of email messages retrieved 
        //  for this email address, but unsuccessfully unsealed.
        // partialResult.nextToken will not be `nil` if there are more email messages to retrieve
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input email address id however makes sense for your implementation.
val emailAddressId = emailAddress.id
val dateRange = EmailMessageDateRange(
    updatedAt = DateRange(
        startDate = // startDate as a [Date]
        endDate = // endDate as a [Date]
    )
)
launch {
    try {
        val input = ListEmailMessagesForEmailAddressIdInput(
            emailAddressId = emailAddressId,
            dateRange = dateRange,
            sortOrder = SortOrder.DESC,
            limit = 20,
            nextToken = nextToken
        )
        val result = withContext(Dispatchers.IO) { 
            emailClient.listEmailMessagesForEmailAddressId(input)
        }
        when (result) {
            is ListAPIResult.Success -> {
                // [result.items] contains the list of items matching the input.
                // Page through the results if [output.nextToken] != null.
            }
            is ListAPIResult.Partial -> {
                // [result.items] contains the list of items matching the input that decrypted successfully.
                // [result.failed] contains the list of items that failed decryption with associated error.
                //  Page through the results if [partial.nextToken] != null.
            }
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

#### All Email Messages for an Email Folder

To retrieve multiple email messages that are assigned to a certain email folder (i.e. Trash), call the `listEmailAddressesForEmailFolderId` method by passing in the `id` of the email folder to query.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input email folder id however makes sense for your implementation.
const emailFolderId = emailFolder.id
const dateRange: EmailMessageDateRange = {
    updatedAt: {
        startDate: // startDate as a `Date` object
        endDate: // endDate as a `Date` object
    }
}
try {
    const result = await emailClient.listEmailMessagesForEmailFolderId({
        emailFolderId,
        dateRange,
        cachePolicy: CachePolicy.RemoteOnly,
        limit: 20,
        nextToken,
    })
    if (result.status === ListOperationResultStatus.Success) {
        // `result` contains the list of items matching the input.
        // Page through the results if result.nextToken != undefined. 
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Collect the input email folder id however makes sense for your implementation.
let emailFolderId = emailFolder.id
let dateRange = EmailMessageDateRange(
    updatedAt: DateRange(
        startDate: // startDate as a `Date` object
        endDate: // endDate as a `Date` object
    )
)
let input = ListEmailMessagesForEmailFolderIdInput(
    emailFolderId: emailFolderId,
    dateRange: dateRange,
    sortOrder: .asc,
    limit: 10,
    nextToken: nil
)
do {
    let result = try await emailClient.listEmailMessagesForEmailFolderId(
        withInput: input
    )
    switch result {
    case .success(let successResult):
        // successResult.items contains an array of the email messages for this email folder.
        // successResult.nextToken will not be `nil` if there are more email messages to retrieve.
    case .partial(let partialResult):
        // partialResult.items contains an array of the email messages successfully 
        //  retrieved and unsealed for this email folder.
        // partialResult.failed contains an array of email messages retrieved 
        //  for this email folder, but unsuccessfully unsealed.
        // partialResult.nextToken will not be `nil` if there are more email messages to retrieve
    }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input email folder id however makes sense for your implementation.
val emailFolderId = emailFolder.id
val dateRange = EmailMessageDateRange(
    updatedAt = DateRange(
        startDate = // startDate as a [Date]
        endDate = // endDate as a [Date]
    )
)
launch {
    try {
        val input = ListEmailMessagesForEmailFolderIdInput(
            folderId = emailFolderId,
            dateRange = dateRange,
            sortOrder = SortOrder.DESC,
            limit = 20,
            nextToken = nextToken
        )
        val result = withContext(Dispatchers.IO) { 
            emailClient.listEmailMessagesForEmailFolderId(input)
        }
        when (result) {
            is ListAPIResult.Success -> {
                // [result.items] contains the list of items matching the input.
                // Page through the results if [output.nextToken] != null.
            }
            is ListAPIResult.Partial -> {
                // [result.items] contains the list of items matching the input that decrypted successfully.
                // [result.failed] contains the list of items that failed decryption with associated error.
                //  Page through the results if [partial.nextToken] != null.
            }
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
By default, list email messages API have a limit of 1MB of data when no limit is supplied.
{% endhint %}

### Accessing RFC 822 Message Data

The **Email SDK** allows developers to interact with raw email data. The SDK's interfaces and documentation refer to this type of data as [RFC 822](https://tools.ietf.org/html/rfc822) data. While there are a number of internet standards, such as [RFC 2822](https://tools.ietf.org/html/rfc2822), [RFC 5322](https://tools.ietf.org/html/rfc5322) and [RFC 6854](https://datatracker.ietf.org/doc/html/rfc6854), that supersede the RFC 822 specification, the intent is that raw email data conforms to any of these specifications and is appropriate and interoperable with Sudo Platform emails.

To access the message body and headers of the email message, the RFC 822 data will need to be retrieved using the `getEmailMessageRfc822Data` method. The `id` of the email message is required to be passed into this method to access the data. The other methods previously supplied on this page only access the metadata of the email message.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const rfc822Data = await emailClient.getEmailMessageRfc822Data({
        id,
        cachePolicy = CachePolicy.REMOTE_ONLY
    })
    // If the `id` matches an email message, `rfc822Data` will be returned, else `undefined` is returned.
    if (rfc822Data) {
        const emailMessageWithBody = // parse the `rfc822Data`
    }
} catch {
    // Handle/notify user of error 
}
```

{% endtab %}

{% tab title="Swift" %}

<pre class="language-swift"><code class="lang-swift"><strong>// Collect the input email message and email address ids however makes sense for your implementation.
</strong>let emailMessageId = emailMessage.id
let emailAddressId = emailAddress.id
let input = GetEmailMessageRfc822DataInput(
    id: emailMessageId,
    emailAddressId: emailAddressId
)
do {
    let rfc822Data = try await emailClient.getEmailMessageRfc822Data(
        withInput: input
    )
    // rfc822Data contains the raw RFC 6854 (supersedes RFC 822) data of 
    //  an email message associated with the `emailMessageId`.
} catch {
    // Handle/notify user of error 
}
</code></pre>

{% endtab %}

{% tab title="Kotlin" %}

<pre class="language-kotlin"><code class="lang-kotlin">// Collect the input email message and email address ids however makes sense for your implementation.
<strong>val emailMessageId = emailMessage.id
</strong>val emailAddressId = emailAddress.id
launch {
    try {
        val input = GetEmailMessageRfc822Data(
            id = emailMessageId,
            emailAddressId = emailAddressId
        )
        val rfc822Data = withContext(Dispatchers.IO) {
            emailClient.getEmailMessageRfc822Data(input)
        }
        // If the [id] matches an email message, [rfc822Data] will be returned, else [null]
        if (rfc822Data != null) {
             val emailMessageWithBody = // parse the [rfc822Data]
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
</code></pre>

{% endtab %}
{% endtabs %}

### Retrieve Email Message with Body

To retrieve and access the contents of an email message body, attachments and headers, call the `getEmailMessageWithBody` method. The `id` of the email message and the `id` of the associated email address is required to be passed in as input to access the message data. The other methods previously supplied on this page only access the metadata of the email message.

End-to-end encrypted email messages are decrypted using the attached cryptographic keys belonging to each recipient as specified in the table in the [Sending Email Messages](#sending-email-messages) section and returned in the response.&#x20;

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input email message and email address ids however makes sense 
// for your implementation.
let emailMessageId = // ...
let emailAddressId = // ...
try {
    const input: GetEmailMessageWithBodyInput = {
        id: emailMessageId,
        emailAddressId: emailAddressId,
    }
    const result = await emailClient.getEmailMessageWithBody(input)
    // If the `id` matches an email message, `EmailMessageWithBody` will be returned, else `undefined` is returned.
} catch {
    // Handle/notify user of error 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Collect the input email message and email address ids however makes sense 
// for your implementation.
let emailMessageId = // ...
let emailAddressId = // ...
do {
    let input = GetEmailMessageWithBodyInput(
        id: emailMessageId,
        emailAddressId: emailAddressId
    )
    let result = try emailClient.getEmailMessageWithBody(input)
    // If the `id` matches an email message, `EmailMessageWithBody` will be returned, else `nil`
} catch {
    // Handle/notify user of error 
}
```

{% endtab %}

{% tab title="Kotlin" %}

<pre class="language-kotlin"><code class="lang-kotlin"><strong>// Collect the input email message and email address ids however makes sense 
</strong><strong>// for your implementation.
</strong><strong>val emailMessageId = // ...
</strong>val emailAddressId = // ...
launch {
    try {
        val input = GetEmailMessageWithBodyInput(
            id = emailMessageId,
            emailAddressId = emailAddressId
        )
        val result = withContext(Dispatchers.IO) {
            emailClient.getEmailMessageWithBody(input)
        }
        // If the [id] matches an email message, [EmailMessageWithBody] will be returned, else [null]
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
</code></pre>

{% endtab %}
{% endtabs %}

## Subscribing to Email Message Events

Consumers of the **Email SDK** may choose to subscribe to notifications of events and changes to email messages. This is done via the `subscribeToEmailMessages` method. Calling the email message subscription methods in the SDK ensures that data relating to email messages will be returned as they are received on the Email Service.

{% hint style="danger" %}
It is important to use subscriptions sparingly as they are resource intensive. It is recommended to only use them when a specific view is open. For example, it would be appropriate for the application to subscribe when a view of a list of emails is in the foreground. When the user navigates from this view, the application should cancel the subscription.
{% endhint %}

{% tabs %}
{% tab title="Typescript" %}
**Setting Up A Subscription**

The `subscribeToEmailMessages` function takes two parameters; a unique `subscriptionId` to identify the specific subscription and an `EmailMessageSubscriber` object. The `EmailMessageSubscriber` will contain the functions necessary to handle changes to `EmailMessage` objects as well as updates to the `ConnectionState` which will be either `Connected` or `Disconnected`.

The `EmailMessageSubscriber` object must contain the following four functions:

```typescript
const emailMessageSubscriber: EmailMessageSubscriber = {
    emailMessageCreated(emailMessage: EmailMessage): void {
        // Implement handling for email message created behaviour
    },
    emailMessageUpdated(emailMessage: EmailMessage): void {
        // Implement handling for email message updated behaviour
    },
    emailMessageDeleted(emailMessage: EmailMessage): void {
        // Implement handling for email message deleted behaviour
    },
    connectionStatusChanged(state: ConnectionState): void {
        // Implement handling for subscription connection status changed
    }
}
```

You can then call `subscribeToEmailMessages` as so:

```typescript
await emailClient.subscribeToEmailMessages(subscriptionId, emailMessageSubscriber)
```

**Cancelling A Subscription**

To cancel a subscription, call the `unsubscribeFromEmailMessages` method with the `subscriptionId`. For example:

```typescript
emailClient.unsubscribeFromEmailMessages(subscriptionId)
```

{% endtab %}

{% tab title="Swift" %}
To subscribe to email message changes, use the `subscribe` method. This method accepts a unique subscription identifier and a subscriber object which must implement the `notify(notification: SubscriptionNotification)` method.  This handler should contain application-specific implementation of behavior in the event of a email message change. The subscription object should also implement the `connectionStatusChanged(state: SubscriptionConnectionState)` method which is invoked in the event of subscription connection state changes between `connected` and `disconnected`. This handler allows the consumer to detect when the subscription connection has been lost and take appropriate corrective action.

**Setting Up A Subscription**

To setup the subscription for created events, create a `Subscriber` conforming instance and provide it to the subscription method as so:

```swift
class MessageCreatedSubscriber: Subscriber {

    func notify(notification: SubscriptionNotification) {
        if case .messageCreated(let createdMessage) = notification {
            // Handle the created message.
        }
    }

    func connectionStatusChanged(state: SubscriptionConnectionState) {
        switch state {
        case .connected:
            // The subscription has become active.
        case .disconnected:
            // The subscription has become inactive and the subscriber
            // has been automatically unsubscribed.
        }
    }
}    
let subscriber = MessageCreatedSubscriber()
let id = UUID().uuidString
try await emailClient.subscribe(id: id, notificationType: .messageCreated, subscriber: subscriber)
```

To setup the subscription for updated events, call the subscription method as so:

```swift
class MessageUpdatedSubscriber: Subscriber {

    func notify(notification: SubscriptionNotification) {
        if case .messageUpdated(let updatedMessage) = notification {
            // Handle the updated message.
        }
    }

    func connectionStatusChanged(state: SubscriptionConnectionState) {
        switch state {
        case .connected:
            // The subscription has become active.
        case .disconnected:
            // The subscription has become inactive and the subscriber
            // has been automatically unsubscribed.
        }
    }
}    
let subscriber = MessageUpdatedSubscriber()
let id = UUID().uuidString
try await emailClient.subscribe(id: id, notificationType: .messageUpdated, subscriber: subscriber)
```

To setup the subscription for deleted events, call the subscription method as so:

```swift
class MessageDeletedSubscriber: Subscriber {

    func notify(notification: SubscriptionNotification) {
        if case .messageDeleted(let deletedMessage) = notification {
            // Handle the deleted message.
        }
    }

    func connectionStatusChanged(state: SubscriptionConnectionState) {
        switch state {
        case .connected:
            // The subscription has become active.
        case .disconnected:
            // The subscription has become inactive and the subscriber
            // has been automatically unsubscribed.
        }
    }
}    
let subscriber = MessageDeletedSubscriber()
let id = UUID().uuidString
try await emailClient.subscribe(id: id, notificationType: .messageDeleted, subscriber: subscriber)
```

{% hint style="warning" %}
Any errors that arise during the setup of the subscription will be returned via a thrown exception, so make sure to wrap your calls in a do-catch block to handle any failed behavior.
{% endhint %}

**Cancelling A Subscription**

To cancel a subscription, pass the same `id` value  used to subscribe to the `unsubscribe` method:

<pre class="language-swift"><code class="lang-swift"><strong>await emailClient.unsubscribe(id: id)
</strong></code></pre>

This will ensure that the subscription is cancelled and system resources are freed.  Alternatively you can cancel all subscriptions at once:

```swift
await emailClient.unsubscribeAll()
```

{% endtab %}

{% tab title="Kotlin" %}
To subscribe to email messages, use the `subscribeToEmailMessages` method. This method accepts a unique subscription identifier and a subscriber object which must implement the `emailMessageChanged(emailMessage: EmailMessage, type: ChangeType)` method. This handler should contain application-specific implementation of behavior in the event of a email message change. The subscription object should also implement the `connectionStatusChanged(state: ConnectionState)` method which is invoked in the event of subscription connection state changes between `CONNECTED` and `DISCONNECTED`. This handler allows the consumer to detect when the subscription connection has been lost and take appropriate corrective action.

**Setting Up A Subscription**

```kotlin
launch {
    try {
        withContext(Dispatchers.IO) {
            emailClient.subscribeToEmailMessages('subscription-id', object : EmailMessageSubscriber {
                override fun emailMessageChanged(emailMessage: EmailMessage, type: ChangeType) {
                    when (type) {
                        ChangeType.CREATED -> {
                            // Implement handling for email message created behaviour
                        }
                        ChangeType.UPDATED -> {
                            // Implement handling for email message updated behaviour
                        }
                        ChangeType.DELETED -> {
                            // Implement handling for email message deleted behaviour
                        }
                    }
                }            
                override fun connectionStatusChanged(state: Subscriber.ConnectionState) {
                   // Implement handling for subscription connection status changed
                }
            })
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

**Cancelling A Subscription**

To cancel a subscription, use the `unsubscribeFromEmailMessages` or `unsubscribeAllFromEmailMessages` method. For example:

```kotlin
val subscriptionId = // From where the subscription was set up
launch {
    try {
        emailClient.unsubscribeFromEmailMessages(subscriptionId)
        // or
        emailClient.unsubscribeAllFromEmailMessages()
    } catch (e: EmailMessageException) {
        // Handle/notify the user of exception
    }
}
```

This will ensure that the subscription is cancelled and system resources are freed.
{% endtab %}
{% endtabs %}

If the `subscribe` method is called more than once with the same subscription id, subsequent invocations will replace the earlier subscriptions.

## Updating Metadata of Email Messages

Updating email message metadata is supported as a batch operation. Note that this API does not allow updating the email message content and headers but rather certain attributes which make up part of the metadata.

The update email messages API currently supports updating the following email message attributes:

|          |                                                                         |
| -------- | ----------------------------------------------------------------------- |
| folderId | Update this attribute to move email messages between different folders. |
| seen     | Update this attribute to set the seen status of the email message       |

One or more email message identifiers can be passed into the `updateEmailMessages` method to perform a batch update of the email messages associated with those identifiers.

{% hint style="danger" %}
Email messages can only be updated in batches of 100. Supplying identifiers to the input which exceed this limit will throw a `LimitExceededError`.
{% endhint %}

A `BatchOperationResult` type is returned from this method call which contains the status of the batch update operation. Three possible statuses can be returned:

| Status  | Definition                                                                                                                                                                                                        |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Success | All of the email messages succeeded to update. The return object will include a list of each email message's identifier, created and updated timestamps.                                                          |
| Partial | Only a subset of email messages succeeded to update. The return object will include a list of identifiers and error descriptions of email messages which failed the update and a list which succeeded the update. |
| Failure | All of the email messages failed to update. The return object will include a list of identifiers and error descriptions of email messages which failed the update.                                                |

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input message ids however makes sense for your implementation.
const ids = ['message-id-1', 'message-id-2']
try {
    const result = await emailClient.updateEmailMessages({
        ids,
        values: { seen: true },
    })
    // `result` contains the status of the batch update operation and associated success and failure metadata.
} catch {
    // Handle/notify user of error
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input message ids however makes sense for your implementation.
let ids = ['message-id-1', 'message-id-2']
let input = UpdateEmailMessagesInput(
    ids: ids,
    values: UpdateEmailMessagesValues(
        folderId: "\(emailAddressId)-TRASH",
        seen: true
    )
)
do {
    let result = try await emailClient.updateEmailMessages(withInput: input)
    // `result` contains the status of the batch update operation and associated success and failure metadata.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input message ids however makes sense for your implementation.
val ids = listOf("message-id-1", "message-id-2")
launch {
    try {
        val input = UpdateEmailMessagesInput(
            ids = ids,
            values = UpdateEmailMessagesInput.UpdatableValues(
                folderId = "$emailAddressId-TRASH", 
                seen = true
            )
        )
        val result = withContext(Dispatchers.IO) {
            emailClient.updateEmailMessages(input)
        }
        // [result] contains the status of the batch update operation and associated success and failure metadata.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Deleting Email Messages

Deleting email messages is supported as a batch operation. Deleting email messages is not the same as moving them to the trash folder. See the [Updating Metadata of Email Messages](#updating-metadata-of-email-messages) section for moving email messages to the trash folder and other folders.

{% hint style="warning" %}
By deleting the email message, the message data and metadata will no longer be available and all traces will be deleted permanently.
{% endhint %}

One or more email message identifiers can be passed into the `deleteEmailMessages` method to perform a batch delete of the email messages associated with those identifiers.

{% hint style="danger" %}
Email messages can only be deleted in batches of 100. Supplying identifiers to the input which exceed this limit will throw a `LimitExceededError`.
{% endhint %}

A `BatchOperationResult` type is returned from this method call which contains the status of the batch delete operation. Three possible statuses can be returned:

| Status  | Definition                                                                                                                                                                           |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Success | All of the email messages succeeded to delete. The return object will include a list of identifiers of messages which succeeded to delete.                                           |
| Partial | Only a subset of email messages succeeded to delete. The return object will include a list of identifiers of messages which failed the delete and a list which succeeded the delete. |
| Failure | All of the email messages failed to delete. The return object will include a list of identifiers of messages which failed to delete.                                                 |

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input message ids however makes sense for your implementation.
const ids = ['message-id-1', 'message-id-2']
try {
    const result = await emailClient.deleteEmailMessages(
        ids
    )
    // `result` contains the status of the batch delete operation and associated success and failure ids.
} catch {
    // Handle/notify user of error
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the email message ids however makes sense for your implementation.
let ids = ['message-id-1', 'message-id-2']
do {
    let result = try await self.emailClient.deleteEmailMessages(
        withIds: ids
    )
    // `result` contains the status of the batch delete operation and associated success and failure ids.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input message ids however makes sense for your implementation.
val ids = listOf("message-id-", "message-id-2")
launch {
    try {
        val result = withContext(Dispatchers.IO) {
            emailClient.deleteEmailMessages(ids)
        }
        // [result] contains the status of the batch delete operation and associated success and failure ids.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Forwarded/Replied To State

When a new email message is sent, a new entity is created, with its `repliedTo` and `forwarded` fields set to `false` by default; this indicates that it is the first message in a thread. If this new message is in reply to, or is forwarding an existing message, it references the ID of the original message. In such cases, the `repliedTo` field of the original message is updated to `true` if it is a reply, or the `forwarded` field is updated to `true` if it is a forward.

This approach helps to maintain an accurate record of the relationships between different email message entities within the conversation thread.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the original email message ID however makes sense
// for your implementation.
const messageId: string = // ...
const emailMessageHeader: InternetMessageFormatHeader = {
    from: { emailAddress: "from@bar.com" },
    to: [{ emailAddress: "to@bar.com" }],
    cc: [],
    bcc: [],
    replyTo: [],
    subject: "Example subject line",
}
```

To update the `repliedTo` property of the original email message, set the `replyingMessageId` property on the send input:

```typescript
const sendInput: SendEmailMessageInput = {
    senderEmailAddressId: "<sender-id>",
    emailMessageHeaders: emailMessageHeader,
    body: "An example email body",
    attachments: [],
    inlineAttachments: [],
    replyingMessageId: messageId,
}
```

To update the `forwarded` property of the original email message, set the `forwardingMessageId` property on the send input:

```typescript
const sendInput: SendEmailMessageInput = {
    senderEmailAddressId: "<sender-id>",
    emailMessageHeaders: emailMessageHeader,
    body: "An example email body",
    attachments: [],
    inlineAttachments: [],
    forwardingMessageId: messageId,
}
```

Once `sendEmailMessage` has successfully completed, the changes to `repliedTo` and/or `forwarded` properties can be observed on the original email message.

```typescript
try {
    await emailClient.sendEmailMessage(sendInput)
    
    const getEmailMessageInput: GetEmailMessageInput = {
        id: messageId,
        cachePolicy: CachePolicy.RemoteOnly
    }
    const emailMessage = await emailClient.getEmailMessage(
        getEmailMessageInput
    )
    // Change(s) can be observed in
    //     `emailMessage.repliedTo`
    //     `emailMessage.forwarded`
} catch {
    // ...
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Collect the original email message ID however makes sense
// for your implementation.
let messageId: String = // ...
let emailMessageHeader = InternetMessageFormatHeader(
    from: EmailAddressDetail(emailAddress: "from@bar.com"),
    to: [EmailAddressDetail(emailAddress: "to@bar.com")],
    cc: [],
    bcc: [],
    replyTo: [],
    subject: "Example subject line"
)
```

To update the `repliedTo` property of the original email message, set the `replyingMessageId` property on the send input:

```swift
var sendInput = SendEmailMessageInput(
    senderEmailAddressId: "<sender-id>",
    emailMessageHeaders: emailMessageHeader,
    body: "An example email body",
    attachments: [],
    inlineAttachments: [],
    replyingMessageId: messageId
)
```

To update the `forwarded` property of the original email message, set the `forwardingMessageId` property on the send input:

```swift
var sendInput = SendEmailMessageInput(
    senderEmailAddressId: "<sender-id>",
    emailMessageHeaders: emailMessageHeader,
    body: "An example email body",
    attachments: [],
    inlineAttachments: [],
    forwardingMessageId: messageId
)
```

Once `sendEmailMessage` has successfully completed, the changes to `repliedTo` and/or `forwarded` properties can be observed on the original email message.

```swift
do {
    await emailClient.sendEmailMessage(sendInput)

    let getEmailMessageInput = GetEmailMessageInput(id: messageId)
    let emailMessage = await emailClient.getEmailMessage(withInput: input)
    // Change(s) can be observed in
    //     `emailMessage.repliedTo`
    //     `emailMessage.forwarded`
} catch {
    // ...
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the original email message ID however makes sense
// for your implementation.
val messageId: String = // ...
val emailMessageHeader = InternetMessageFormatHeader(
    from = EmailMessage.EmailAddress("from@bar.com"),
    to = listOf(EmailMessage.EmailAddress("to@bar.com")),
    cc = emptyList<String>(),
    bcc = emptyList<String>(),
    replyTo = emptyList<String>(),
    subject = "Example subject line",
)
```

To update the `repliedTo` property of the original email message, set the `replyingMessageId` property on the send input:

```kotlin
val sendInput = SendEmailMessageInput(
    senderEmailAddressId = "<sender-id>",
    emailMessageHeaders = emailMessageHeader,
    body = "An example email body",
    attachments = emptyList<EmailAttachment>(),
    inlineAttachments = emptyList<EmailAttachment>(),
    replyingMessageId = messageId,
)
```

To update the `forwarded` property of the original email message, set the `forwardingMessageId` property on the send input:

```kotlin
val sendInput = SendEmailMessageInput(
    senderEmailAddressId = "<sender-id>",
    emailMessageHeaders = emailMessageHeader,
    body = "An example email body",
    attachments = emptyList<EmailAttachment>(),
    inlineAttachments = emptyList<EmailAttachment>(),
    forwardingMessageId = messageId,
)
```

Once `sendEmailMessage` has successfully completed, the changes to `repliedTo` and/or `forwarded` properties can be observed on the original email message.

```kotlin
launch {
    try {
        val sendResult = withContext(Dispatchers.IO) {
            emailClient.sendEmailMessage(sendInput)
        }
        
        val getEmailMessageInput = GetEmailMessageInput(
            id = emailMessageId
        )
        val emailMessage = withContext(Dispatchers.IO) {
            emailClient.getEmailMessage(input)
        }
        // Change(s) can be observed in
        //     `emailMessage.repliedTo`
        //     `emailMessage.forwarded`
    } catch (e: EmailMessageException) {
        // ...
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
No changes are made to the subject line, message body or headers; only the metadata of the email message is updated.
{% endhint %}


# Draft Email Messages

Allow your users to save draft emails securely

The **Email SDK** supports the notion of draft email messages. A separate set of APIs are available to handle the lifecycle of drafts.

### Creating a Draft Email Message

To create and save a draft email message, call the `createDraftEmailMessage` method. This method takes in a `rfc822Data` input property which contains the email message content formatted under RFC 822 as well as the `senderEmailAddressId` input property which must match the identifier of the email addess from the `from` field in the RFC 822 data. A call to this method returns metadata of the saved draft. Draft email message data provides the ID of the draft and an `updatedAt` timestamp representing the last time the draft was saved.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input RFC 822 data and sender email address id however makes sense for your implementation.
const rfc822Data: ArrayBuffer = // ...
const senderEmailAddressId = senderEmailAddress.id
try {
    const draftMetadata = await emailClient.createDraftEmailMessage({
        rfc822Data,
        senderEmailAddressId,
    })
    // `draftMetadata` is the metadata associated with the saved
    // draft email message including the id you can use this to access or
    // update the draft.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the input RFC 822 data and sender email address id however makes sense for your implementation.
let rfc822Data: Data = // ...
let senderEmailAddressId = senderEmailAddress.id
let input = CreateDraftEmailMessageInput(
    rfc822Data: rfc822Data,
    senderEmailAddressId: senderEmailAddressId
)
do {
    let draftMetadata = try await emailClient.createDraftEmailMessage(
        withInput: input
    )
    // `draftMetadata` is the `DraftEmailMessageMetadata` associated with the saved
    // draft email message including the id which you can use to access or
    // update the draft.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the input RFC 822 data and sender email address id however makes sense for your implementation.
val rfc822Data: ByteArray = // ...
val senderEmailAddressId = senderEmailAddress.id
launch {
    try {
        val input = CreateDraftEmailMessageInput(
            rfc822Data = rfc822Data,
            senderEmailAddressId = senderEmailAddressId
        )
        val draftId = withContext(Dispatchers.IO) {
            emailClient.createDraftEmailMessage(input)
        }
        // [draftId] is the unique id of the draft that you can use to access 
        // or update the draft
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Updating a Draft Email Message

Use the `updateDraftEmailMessage` method to update a previously created and saved draft email message. The input requires the `id` of the previously saved draft to perform the update. It is a requirement that the entire message content be replaced with an updated version as part of the `rfc822Data` input property. A call to this method returns the `id` of the draft that was updated which should match the `id` provided in the input.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input RFC 822 data, sender email address id and existing draft id however makes sense for your implementation.
const id: string = // ...
const rfc822Data: ArrayBuffer = // ...
const senderEmailAddressId = senderEmailAddress.id 
try {
    const draftMetadata = await emailClient.updateDraftEmailMessage({
        id,
        rfc822Data,
        senderEmailAddressId,
    })
    // `draftMetadata` is the metadata associated with the saved
    // draft email message including the id you can use this to access or
    // update the draft.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the input RFC 822 data, sender email address id and existing draft id however makes sense for your implementation.
let id: String = // ...
let rfc822Data: Data = // ...
let senderEmailAddressId = senderEmailAddress.id
let input = UpdateDraftEmailMessageInput(
    id: id,
    rfc822Data: rfc822Data,
    senderEmailAddressId: senderEmailAddressId
)
do {
    let draftMetadata = try await emailClient.updateDraftEmailMessage(
        withInput: input
    )
    // `draftMetadata` is the `DraftEmailMessageMetadata` associated with the
    // updated draft email message including the id which you can use to access
    // or update the draft.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the input RFC 822 data, sender email address id and existing draft id however makes sense for your implementation.
val id: String = // ...
val rfc822Data: ByteArray = // ...
val senderEmailAddressId = senderEmailAddress.id
launch {
    try {
        val input = UpdateDraftEmailMessageInput(
            id = id,
            rfc822Data = rfc822Data,
            senderEmailAddressId = senderEmailAddressId
        )
        val draftId = withContext(Dispatchers.IO) {
            emailClient.updateDraftEmailMessage(input)
        }
        // [draftId] is the unique id of the draft that you can use to access 
        // or update the draft
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Deleting Draft Email Messages

Draft email messages can be deleted in batches using the `deleteDraftEmailMessages` method by passing in one or more draft message identifiers and the `id` of the email address associated with the drafts.

{% hint style="warning" %}
Draft email messages that have been deleted will no longer be available and all traces of the message data and metadata will be deleted permanently.
{% endhint %}

{% hint style="danger" %}
Draft email messages can only be deleted in batches of 10. Supplying identifiers to the input which exceed this limit will return a `LimitExceededError`.
{% endhint %}

A `BatchOperationResult` type is returned from this method call which contains the status of the batch delete operation. Three possible statuses can be returned:

| Status  | Definition                                                                                                                                                                                                                    |
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Success | All of the draft email messages succeeded to delete. The return object will include a list of identifiers of messages which succeeded to delete.                                                                              |
| Partial | Only a subset of draft email messages succeeded to delete. The return object will include a list of identifiers and error descriptions of draft email messages which failed the delete and a list which succeeded the delete. |
| Failure | All of the draft email messages failed to delete.                                                                                                                                                                             |

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input draft message ids and email address id however makes sense for your implementation.
const ids = arrayOf('draft-msg-id-1', 'draft-msg-id-2')
const emailAddressId = emailAddress.id
try {
    const result = await emailClient.deleteDraftEmailMessages({
        ids,
        emailAddressId,
    })
    // `result` contains the status of the batch delete operation and associated success and failure metadata.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the input draft message ids and email address id however makes sense for your implementation.
let ids = ['draft-msg-id-1', 'draft-msg-id-2']
let emailAddressId = emailAddress.id
let input = DeleteDraftEmailMessagesInput(
    ids: ids,
    emailAddressId: emailAddressId
)
do {
    let result = try await self.emailClient.deleteDraftEmailMessages(
        withInput: input
    )
    // `result` contains the status of the batch delete operation and associated success and failure metadata.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the input draft message ids and email address id however makes sense for your implementation.
val ids = listOf("draft-msg-id-1", "draft-msg-id-2")
val emailAddressId = emailAddress.id
launch {
    try {
        val input = DeleteDraftEmailMessagesInput(
            ids = ids,
            emailAddressId = emailAddressId
        )
        val result = withContext(Dispatchers.IO) {
            emailClient.deleteDraftEmailMessages(input)
        }
        // [result] contains the status of the batch delete operation and associated success and failure metadata.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Retrieving Draft Email Messages

Draft email messages can be accessed using its identifier or by querying for a list of its identifiers.

{% hint style="warning" %}
If a draft email message has been deleted, it will no longer be available for access.
{% endhint %}

#### Single Draft Email Message by Id

To retrieve a single draft email message, use the `getDraftEmailMessage` method. This method takes in the `id` of the draft to retrieve as well as the `id` of the email address that is associated with the draft. This method returns the draft email message data if it exists.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input email address id and draft id however makes sense for
// your implementation.
const emailAddressId = emailAddress.id
try {
    const draftMessage = await emailClient.getDraftEmailMessage({
        id,
        emailAddressId,
    })
    // `draftMessage` containing draft message data and `updatedAt` timestamp
    // will be returned if a draft message corresponding to `id` is found,
    // else `undefined`.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the input draft message id and email address id however makes sense for your implementation.
let id = 'draft-msg-id-1'
let emailAddressId = emailAddress.id
let input = GetDraftEmailMessageInput(
    id: id,
    emailAddressId: emailAddressId
)
do {
    let draftEmailMessage = try await emailClient.getDraftEmailMessage(
        withInput: input
    )
    // `draftMessage` containing draft message data and `updatedAt` timestamp
    // will be returned if a draft message corresponding to `id` is found,
    // else `nil`.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the input draft message id and email address id however makes sense for your implementation.
val id = "draft-msg-id-1"
val emailAddressId = emailAddress.id
launch {
    try {
        val input = GetDraftEmailMessageInput(
            id = id,
            emailAddressId = emailAddressId
        )
        val draftMessage = withContext(Dispatchers.IO) {
            emailClient.getDraftEmailMessage(input)
        }
        // [draftMessage] containing draft message data and [updatedAt] timestamp
        // will be returned if a draft message corresponding to [id] is found,
        // else will throw an exception.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

#### List of All Draft Email Messages \[Deprecated]

To retrieve a list of all draft email messages associated with a user, call the `listDraftEmailMessages` method. \
\
**Deprecated:** use `listDraftEmailMessagesForEmailAddressId` instead

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const result = await emailClient.listDraftEmailMessages()
    // `result` contains a list of draft messages consisting of the 
    // draft message data and the `updatedAt` timestamp.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
do {
    let result = try await emailClient.listDraftEmailMessages()
    // `result` contains a list of draft messages consisting of the 
    // draft message data and the `updatedAt` timestamp.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val result = withContext(Dispatchers.IO) {
            emailClient.listDraftEmailMessages()
        }
        // [result] contains a list of draft messages consisting of the 
        // draft message data and the [updatedAt] timestamp.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

#### List of Draft Email Messages for an Email Address

To retrieve a list of multiple draft email messages associated with a certain email address, call the `listDraftEmailMessagesForEmailAddressId` method by passing in the `id` of the email address, the number of results you want (min: 1, max: 1000, default: 10) and the next token.\
\
The "next token" is returned from the previous call and is used to get the next set of results. When next token is undefined or nil or null, there are no further results. For the first set, pass a next token of undefined, nil, or null.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input email address id however makes sense for your implementation.
const emailAddressId = emailAddress.id
let allItems: DraftEmailMessage[] = []

try {
    do {
      const metadataList =        
        await sudoEmailClient.listDraftEmailMessagesForEmailAddressId({
          emailAddressId: emailAddressId,
          limit: 5,
          nextToken: nextToken,
        })

      allItems = [...allItems, ...metadataList.items]

      nextToken = metadataList.nextToken
    } while (nextToken !== undefined && allItems.length < 50)
    // `allItems` contains a list of all of the draft email messages'
    // metadata.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```kotlin
// Obtain the input email address id however makes sense for your implementation.
let emailAddressId = emailAddress.id
do {
    let result = try await emailClient.listDraftEmailMessagesForEmailAddressId(
        emailAddressId
    )
    // `result` contains a list of draft messages consisting of the 
    // draft message data and the `updatedAt` timestamp.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val allItems = mutableListOf<DraftEmailMessageWithContent>()
var nextToken: String? = null
do {
    val draftEmailMessages =
        emailClient.listDraftEmailMessagesForEmailAddressId(
            ListDraftEmailMessagesForEmailAddressIdInput(emailAddressId, 5, nextToken),
        ) 
    allItems.addAll(draftEmailMessages.items)
    nextToken = draftEmailMessages.nextToken
} while (nextToken != null)
// `allItems` contains a list of all of the draft email messages
```

{% endtab %}
{% endtabs %}

#### List of All Draft Email Message Metadata \[Deprecated]

Use `listDraftEmailMessageMetadata` to retrieve a list of all the metadata of the draft email messages associated with a user. The metadata returned in the list can be used to retrieve individual draft email message content, perform an update to an already saved draft, or identify which drafts to perform full retrieval of based on `updatedAt` timestamp.\
\
**Deprecated:** use `listDraftEmailMessageMetadataForEmailAddressId` instead

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const result = await emailClient.listDraftEmailMessageMetadata()
    // `result` contains a list of all of the draft email messages'
    // metadata.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
do {
    let result = try await emailClient.listDraftEmailMessageMetadata()
    // `result` contains an array of `DraftEmailMessageMetadata` of all of the
    // draft email messages' metadata.
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val result = withContext(Dispatchers.IO) {
            emailClient.listDraftEmailMessageMetadata()
        }
        // [result] contains a list of [DraftEmailMessageMetadata] of all of the
        // draft email messages' metadata.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

#### List of Draft Email Message Metadata for an Email Address

To retrieve a list of all the metadata of the draft messages associated with an email address, call the `listDraftEmailMessageMetadataForEmailAddressId` method by passing in the `id` of the email address, the number of results you want (min: 1, max: 1000, default: 10) and the next token.\
\
The "next token" is returned from the previous call and is used to get the next set of results. When next token is undefined or nil or null, there are no further results. For the first set, pass a next token of undefined, nil, or null.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the input email address id however makes sense for your implementation.
const emailAddressId = emailAddress.id
let allItems: DraftEmailMessageMetadata[] = []

try {
    do {
      const metadataList =        
        await sudoEmailClient.listDraftEmailMessageMetadataForEmailAddressId({
          emailAddressId: emailAddressId,
          limit: 5,
          nextToken: nextToken,
        })

      allItems = [...allItems, ...metadataList.items]

      nextToken = metadataList.nextToken
    } while (nextToken !== undefined && allItems.length < 50)
    // `allItems` contains a list of all of the draft email messages'
    // metadata.
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```kotlin
var allDraftMetadata: [DraftEmailMessageMetadata] = []
var nextToken: String?

// Loop through all pages of draft metadata
repeat {
    let input = ListDraftEmailMessageMetadataForEmailAddressIdInput(
        emailAddressId: emailAddressId,
        limit: 5,
        nextToken: nextToken
    )
    let draftsMetadata = try await emailClient.listDraftEmailMessageMetadataForEmailAddressId(
        withInput: input
    )
    allDraftMetadata.append(contentsOf: draftsMetadata.items)
    nextToken = draftsMetadata.nextToken
} while nextToken != nil
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val allItems = mutableListOf<DraftEmailMessageMetadata>()
var nextToken: String? = null
do {
    val draftEmailMessagesMetadata =
        emailClient.listDraftEmailMessageMetadataForEmailAddressId(
            ListDraftEmailMessageMetadataForEmailAddressIdInput(emailAddressId, 5, nextToken),
        ) 
    allItems.addAll(draftEmailMessagesMetadata.items)
    nextToken = draftEmailMessagesMetadata.nextToken
} while (nextToken != null)
// `allItems` contains a list of all of the draft email messages'
// metadata.
```

{% endtab %}
{% endtabs %}


# Schedule Email Messages

Schedule a message to be sent at a specified date and time in the future.

The **Email SDK** supports scheduling draft email messages to be sent at a specified time in the future. The Email Service will periodically check for messages that have been scheduled whose `sendAt` times have passed and will send them as normal. Your runtime instance can be configured to specify how often you want this check to occur. More frequent checks will increase the chances that the messages will be sent as close to the specified time as possible. If a message fails to send for any reason, it may also be retried a configurable number of times.

### Scheduling a Draft Message

To schedule a draft message to be sent in the future, call the `scheduleSendDraftMessage` method. This method takes in the `id` of the draft message to schedule, the `emailAddressId` of the email address that owns the draft message and the `sendAt` timestamp of when to send the message. The call will return a record of the scheduled message with useful information including the `sendAt` timestamp, the `updatedAt` timestamp and the `state` of the scheduled message. There are four states that the scheduled message may be in:

<table><thead><tr><th width="185.20703125">State</th><th>Meaning</th></tr></thead><tbody><tr><td>SCHEDULED</td><td>The message has been scheduled and will be sent once the <code>sendAt</code> time has passed.</td></tr><tr><td>FAILED</td><td>The message failed to send after the <code>sendAt</code> passed.</td></tr><tr><td>SENT</td><td>The message has successfully been sent. It should appear in the <code>SENT</code> folder of the user's email address.</td></tr><tr><td>CANCELLED</td><td>The message was cancelled and will not be sent.</td></tr></tbody></table>

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the draftId and emailAddressId however makes sense for your application
const id = draft.id
const emailAddressId = senderEmailAddress.id
const sendAt = DateTime.now().plus({ day: 1 }).toJSDate() // Adjust this to the required time in the future
try {
    const scheduledDraftMessage = await emailClient.scheduleSendDraftMessage({
        id,
        emailAddressId,
        sendAt,
    })
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the draftId and emailAddressId however makes sense for your application
let id = draft.id
let emailAddressId = senderEmailAddress.id
let sendAt = Date().addingTimeInterval(+86400) // Adjust this to the required time in the future
let input = ScheduleSendDraftMessageInput(
    id: id,
    emailAddressId: emailAddressId,
    sendAt: sendAt
)
do {
    let scheduledDraftMessage = try await emailClient.scheduleSendDraftMessage(withInput: input)
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the draftId and emailAddressId however makes sense for your application
val id = draft.id
val emailAddressId = senderEmailAddress.id
val sendAt = Date(Date().time + Duration.ofDays(1).toMillis()) // Adjust this to the required time in the future
launch {
    try {
        val input = ScheduleSendDraftMessageInput(
            id,
            emailAddressId,
            sendAt,
        )
        val scheduledDraftMessage = withContext(Dispatchers.IO) {
            emailClient.scheduleSendDraftMessage(input)
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Cancelling a Scheduled Draft Message

A previously scheduled draft message can be cancelled at any time before it's specified `sendAt` time by calling the `cancelScheduledDraftMessage` method. This method takes the draft message `id` and the `emailAddressId` of the email address that owns the draft and will return the draft `id` on successful cancellation.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the draftId and emailAddressId however makes sense for your application
const id = draft.id
const emailAddressId = senderEmailAddress.id
try {
    const cancelledScheduledDraftId = await client.cancellScheduledDraftMessage({
        id,
        emailAddressId,
    })
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the draftId and emailAddressId however makes sense for your application
let id = draft.id
let emailAddressId = senderEmailAddress.id
let input = CancelScheduledDraftMessageInput(
    id: id,
    emailAddressId: emailAddressId,
)
do {
    let cancelledScheduledDraftId = try await emailClient.cancelScheduledDraftMessage(withInput: input)
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the draftId and emailAddressId however makes sense for your application
val id = draft.id
val emailAddressId = senderEmailAddress.id
launch {
    try {
        val input = CancelScheduledDraftMessageInput(
            id,
            emailAddressId,
        )
        val cancelledScheduledDraftId = withContext(Dispatchers.IO) {
            emailClient.cancelScheduledDraftMessage(input)
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Listing Scheduled Draft Messages

To retrieve a list of previously scheduled draft messages, call the `listScheduledDraftMessagesForEmailAddressId` method. This method takes the `emailAddressId` , an optional `filters` argument to filter your results and pagination arguments (See [Email SDK Pagination](/guides/email/integrate-the-email-sdk/pagination) for details). It will return a list of Scheduled Draft Messages.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the emailAddressId however makes sense for your application
const emailAddressId = senderEmailAddress.id
const filter: ScheduledDraftMessageFilterInput = {
    state: {
        notEqual: ScheduledDraftMessageState.CANCELLED,
    },
}
try {
    const listResult = await emailClient.listScheduledDraftMessagesForEmailAddressId({
        emailAddressId,
        filter,
    })
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the emailAddressId however makes sense for your application
let emailAddressId = senderEmailAddress.id
let input = ListScheduledDraftMessagesForEmailAddressIdInput(
    emailAddressId: emailAddressId,
    limit: nil,
    nextToken: nil,
    filter: ScheduledDraftMessageFilter(
        state: ScheduledDraftMessageStateFilter.notEqual(.cancelled)
    )
)
do {
    let listResult = try await emailClient.listScheduledDraftMessagesForEmailAddressId(withInput: input)
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the draftId and emailAddressId however makes sense for your application
val emailAddressId = senderEmailAddress.id
launch {
    try {
        vval input = ListScheduledDraftMessagesForEmailAddressIdInput(
            emailAddressId,
            filter = ScheduledDraftMessageFilterInput(
                state = NotEqualStateFilter(
                    notEqual = ScheduledDraftMessageState.CANCELLED,
                ),
            ),
        )
        val listResult = withContext(Dispatchers.IO) {
            emailClient.listScheduledDraftMessagesForEmailAddressId(input)
        }
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

The `listScheduledDraftMessagesForEmailAddressId` method currently supports filtering results by `state`. The above example would list all results where the `state` is not equal to `CANCELLED` , but there are other options as defined below:

<table><thead><tr><th width="112.578125">Filter Option</th><th width="320.21484375">Type</th><th>Meaning</th></tr></thead><tbody><tr><td><code>equal</code></td><td><code>ScheduledDraftMessageState</code></td><td>Return only results that match the given state.</td></tr><tr><td><code>oneOf</code></td><td><code>Array&#x3C;ScheduledDraftMessageState></code></td><td>Return only results that match one of the given states.</td></tr><tr><td><code>notEqual</code></td><td><code>ScheduledDraftMessageState</code></td><td>Return only results that do not match the given state.</td></tr><tr><td><code>notOneOf</code></td><td><code>Array&#x3C;ScheduledDraftMessageState></code></td><td>Return only results that do not match any of the given states.</td></tr></tbody></table>


# Email Address Public Information

Provides the ability to lookup public information for email addresses in the same network.

When an email address is [provisioned](/guides/email/email-addresses/manage-email-addresses#provision-email-address), matching public and private keys are also generated for encrypting messages addressed to and sent from that address.&#x20;

Clients of the Sudo Platform have the ability to lookup public information for email addresses as a means of verifying that an email address exists and to determine the public key to use to encrypt messages addressed to that email address.

Only email addresses that belong to the same domain(s) for a particular Sudo Platform Email Service configuration may be retrieved, i.e., if configured to use `*@domain-1.com`, a lookup for`foo@domain-1.com` would be performed, whereas `bar@domain-2.com` would fail.

The following table describes the available public information for an email address:

| Data          | Definition                                                                                        |
| ------------- | ------------------------------------------------------------------------------------------------- |
| Email Address | Fully qualified email address of the form \<localPart\@domain>, e.g., `example@sudoplatform.com`. |
| Public Key    | Public key associated with the email address.                                                     |

## Lookup Email Address Public Information

Public information for email addresses can be retrieved in batches by passing a list of email addresses to the `lookupEmailAddressesPublicInfo` method. This returns a list of `EmailAddressPublicInfo` entries containing `emailAddress` and `publicKey` for each input email address. Items in the result list are returned in the same order as requested.

Failure to return a `EmailAddressPublicInfo` entry for an email address means that either the email address was not found in the system, or it belongs to an unsupported domain. In the event of a partial failure such as this, the result list will contain entries for the success cases only. Failure to retrieve *all* email addresses in a request will result in an empty list.

{% hint style="danger" %}
The maximum number of email addresses provided to the input cannot be more than **50**. Supplying a list larger than this will return a `LimitExceededError`.
{% endhint %}

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Email addresses to be looked up
const emailAddresses = [
  'email-address-1@domain.com',
  'email-address-2@domain.com',
]

try {
  const input = LookupEmailAddressesPublicInfoInput(
    emailAddresses
  )
  const emailAddressesPublicInfo = await emailClient
    .lookupEmailAddressesPublicInfo(input)
    
  // Assuming `emailAddresses` are valid email addresses and the
  // client shares the same (*@domain.com in this case) network,
  // `emailAddressesPublicInfo` will contain `EmailAddressPublicInfo`
  // objects for each email address, e.g.:
    
  // emailAddressesPublicInfo[0] --> {
  //   emailAddress: 'email-address-1@domain.com',
  //   publicKey: <public key string>
  // }
    
  // emailAddressesPublicInfo[1] --> {
  //   emailAddress: 'email-address-2@domain.com',
  //   publicKey: <public key string>
  // }
} catch (e) {
  // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Email addresses to be looked up
let emailAddresses = [
    "email-address-1@domain.com",
    "email-address-2@domain.com"
]

try {
    let input = LookupEmailAddressesPublicInfoInput(
        emailAddresses: emailAddresses
    )
    let emailAddressesPublicInfo = await emailClient
        .lookupEmailAddressesPublicInfo(input)
    
    // Assuming `emailAddresses` are valid email addresses and the
    // user shares the same (*@domain.com in this case) network,
    // `emailAddressesPublicInfo` will contain `EmailAddressPublicInfo`
    // objects for each email address, e.g.:
      
    // emailAddressesPublicInfo[0] --> {
    //   emailAddress: 'email-address-1@domain.com',
    //   publicKey: <public key string>
    // }
      
    // emailAddressesPublicInfo[1] --> {
    //   emailAddress: 'email-address-2@domain.com',
    //   publicKey: <public key string>
    // }
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Email addresses to be looked up
val addresses = listOf(
    "email-address-1@domain.com",
    "email-address-2@domain.com"
)

launch {
    try {
        val input = LookupEmailAddressesPublicInfoInput(
            emailAddresses = emailAddresses
        )
        val emailAddressesPublicInfo = withContext(Dispatchers.IO) {
            emailClient.lookupEmailAddressesPublicInfo(input)
        }
        
        // If `emailAddresses` are valid email addresses and the
        // user shares the same (*@domain.com in this case) network,
        // `emailAddressesPublicInfo` will contain `EmailAddressPublicInfo`
        // objects for each email address, e.g.:
          
        // emailAddressesPublicInfo[0] --> {
        //   emailAddress: 'email-address-1@domain.com',
        //   publicKey: <public key string>
        // }
          
        // emailAddressesPublicInfo[1] --> {
        //   emailAddress: 'email-address-2@domain.com',
        //   publicKey: <public key string>
        // }
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}

```

{% endtab %}
{% endtabs %}


# Manage Email Blocklists

Allow your user to block specific email addresses from being able to send messages to any of their Sudo email addresses

When a user blocks a sender's email address through the **Email SDK**, they can choose what action to take for future messages from that sender, as well as if they should be blocked when sent to any of the user's addresses, or just a specific one.

## Blocking Email Addresses

Email addresses can be blocked in batches. Blocking addresses can be done through the `blockEmailAddresses` method by passing in an array of the addresses to be blocked. The addresses should be in the format `foo-bar@domain.com.`&#x20;

By default, email addresses in the batch are blocked for all addresses that the user owns. The optional `emailAddressId` parameter can be passed to specify that the block only applies to a specific address owned by the user.

There are two actions that can be taken when a message is received from a blocked email address. These can be specified by passing the optional `action` parameter with one of the enum values below.

| Action | Definition                                                                                                                                                   |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| DROP   | The incoming message will be dropped and not appear in the user's account.                                                                                   |
| SPAM   | The incoming message will be redirected to the user's SPAM folder, if your environment is configured to do so. Otherwise, it will revert to the DROP action. |

The method will respond with a `BatchOperationResult` type which contains the status of the batch operation. Three possible statuses can be returned:

| Status  | Definition                                                                                                                                                                             |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Success | All of the email addresses were successfully to blocked.                                                                                                                               |
| Partial | Only a subset of the email addresses could be blocked successfully. The return object will include a list of addresses which failed to be blocked and a list of those which succeeded. |
| Failure | All of the email addresses failed to be blocked.                                                                                                                                       |

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Obtain the email addresses to be blocked
const addressesToBlock = [
  'email-address-1@domain.com', 
  'email-address-2@domain.com',
]
let emailAddressId: string | undefined
// Set the emailAddressId if you want to block for just a specific address
emailAddressId = emailAddress.id
let action: BlockedAddressAction | undefined
// Set the action if you want to specify an action.
action = BlockedAddressAction.Spam
try {
    const blockingResult = await emailClient.blockEmailAddresses({
        addressesToBlock,
        emailAddressId,
        action,
    })
    switch (blockingResult.status){
        case UpdateEmailMessagesStatus.Success:
          // All addresses were blocked successfully
          break
        case UpdateEmailMessagesStatus.Failed:
          // None of the addresses were blocked successfully
          break
        case UpdateEmailMessagesStatus.Partial:
          // `blockingResult.successItems` contains an array of all the addresses
          // blocked successfully.
          // `blockingResult.failureItems` cotains an array of all the addresses
          // that failed to be blocked successfully.
          break
      }
} catch (e) {
    // Handle/notify user of errors
}  
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the email addresses to be blocked
let addresses = ["email-address-1@domain.com", "email-address-2@domain.com"]
let emailAddressId: String?
// Set the emailAddressId if you want to block for just a specific address
emailAddressId = emailAddress.id
let action: UnsealedBlockedAddress.BlockedAddressAction?
// Set the action if you want to specify an action.
action = .spam
do {
    let blockingResult = try await self.emailClient.blockEmailAddresses(
        addresses: addresses,
        emailAddressId: emailAddressId,
        action: action ?? .drop
    )
    switch blockingResult {
        case .success:
            // All addresses were blocked successfully
        case .failure:
            // None of the addresses were blocked successfully
        case .partial:
            // `blokcingResult.successItems` contains an array of all the addresses
            // blocked successfully.
            // `blockingResult.failureItems` cotains an array of all the addresses
            // that failed to be blocked successfully.
    }
} catch {
    // Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the email addresses to be blocked
val addressesToBlock = listOf(
  "email-address-1@domain.com", 
  "email-address-2@domain.com"
)
var emailAddressId: String?
// Set the emailAddressId if you want to block for just a specific address
emailAddressId = emailAddress.id
var action: BlockedEmailAddressAction?
// Set the action if you want to specify an action.
action = BlockedEmailAddressAction.SPAM
launch {
    try {
        val blockingResult = withContext(Dispatcher.IO) {
            emailClient.blockEmailAddresses(
                addresses = addressesToBlock,
                emailAddressId = emailAddressId,
                action = action,
            )
        }
        when (blockingResult) {
            is BatchOperationResult.SuccessOrFailureResult -> {
                // [blockingResult.status] contains the status of the batch block operation.
            }
            is BatchOperationResult.Partial -> {
                // [blockingResult.successValues] contains the list of items for which the block operation succeeded.
                // [blockingResult.failureValues] contains the list of items for which the block operation failed.
            }
        }
    } catch (e: EmailBlocklistException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Unblocking Email Addresses

Unblocking email addresses works much the same way as blocking them does. There are two methods for achieving this.  The first is by passing an array of the addresses, in the format in the format `foo-bar@domain.com`, to be unblocked to the `unblockEmailAddresses` method. This method only works for addresses that were blocked without an `emailAddressId` parameter included.

The method will respond with the same `BatchOperationResult` type with the same possible statuses.

{% tabs %}
{% tab title="Typescript" %}

```typescript
// Obtain the email addresses to be unblocked
const addressesToUnblock = arrayOf('email-address-1@domain.com', 'email-address-2@domain.com')
try {
    const unblockingResult = await emailClient.unblockEmailAddresses({
        addressesToUnblock
    })
    switch (unblockingResult.status){
        case UpdateEmailMessagesStatus.Success:
          // All addresses were unblocked successfully
          break
        case UpdateEmailMessagesStatus.Failed:
          // None of the addresses were unblocked successfully
          break
        case UpdateEmailMessagesStatus.Partial:
          // `unblockingResult.successItems` contains an array of all the addresses
          // unblocked successfully.
          // `unblockingResult.failureItems` cotains an array of all the addresses
          // that failed to be unblocked successfully.
          break
      }
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the email addresses to be unblocked
let addresses = ["email-address-1@domain.com", "email-address-2@domain.com"]
do {
    let unblockingResult = try await self.emailClient.unblockEmailAddresses(
        addresses: addresses
    )
    switch unblockingResult {
        case .success:
            // All addresses were unblocked successfully
        case .failure:
            // None of the addresses were unblocked successfully
        case .partial:
            // `unblockingResult.successItems` contains an array of all the addresses
            // unblocked successfully.
            // `unblockingResult.failureItems` cotains an array of all the addresses
            // that failed to be unblocked successfully.
    }
} catch {
    // Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the email addresses to be unblocked
val addressesToUnblock = listOf("email-address-1@domain.com", "email-address-2@domain.com")
launch {
    try {
        val unblockingResult = withContext(Dispatcher.IO) {
            emailClient.unblockEmailAddresses(addressesToUnblock)
        }
        when (unblockingResult) {
            is BatchOperationResult.SuccessOrFailureResult -> {
                // [unblockingResult.status] contains the status of the batch unblock operation.
            }
            is BatchOperationResult.Partial -> {
                // [unblockingResult.successValues] contains the list of items for which the unblock operation succeeded.
                // [unblockingResult.failureValues] contains the list of items for which the unblock operation failed.
            }
        }
    } catch (e: EmailBlocklistException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

Alternatively, you can also unblock email addresses by the hashedBlockedValue (returned from the `getEmailAddressBlocklist` endpoint below). This is the method that must be used for addresses that were blocked with the `emailAddressId`parameter included and can also be useful if the user has lost their cryptographic key and is unable to unseal the plaintext address. The `unblockEmailAddressesByHashedValue` method works the same way, except it accepts an array of `hashedValue`s instead of plaintext addresses.

{% tabs %}
{% tab title="Typescript" %}

```typescript
// Obtain the hashedValues from the getEmailAddressBlocklist endpoint
const hashedValues = arrayOf('hashedValue1', 'hashedValue2')
try {
    const unblockingResult = await emailClient.unblockEmailAddressesByHashedValue({
        hashedValues
    })
    switch (unblockingResult.status){
        case UpdateEmailMessagesStatus.Success:
          // All addresses were unblocked successfully
          break
        case UpdateEmailMessagesStatus.Failed:
          // None of the addresses were unblocked successfully
          break
        case UpdateEmailMessagesStatus.Partial:
          // `unblockingResult.successItems` contains an array of all the addresses
          // unblocked successfully.
          // `unblockingResult.failureItems` cotains an array of all the addresses
          // that failed to be unblocked successfully.
          break
      }
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Obtain the hashedValues from the getEmailAddressBlocklist endpoint
let hashedValues = ["hashedValue1", "hashedValue2"]
do {
    let unblockingResult = try await self.emailClient.unblockEmailAddressesByHashedValue(
        hashedValues: hashedValues
    )
    switch unblockingResult {
        case .success:
            // All addresses were unblocked successfully
        case .failure:
            // None of the addresses were unblocked successfully
        case .partial:
            // `unblockingResult.successItems` contains an array of all the addresses
            // unblocked successfully.
            // `unblockingResult.failureItems` cotains an array of all the addresses
            // that failed to be unblocked successfully.
    }
} catch {
    // Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Obtain the hashedValues from the getEmailAddressBlocklist endpoint
val hashedValues = listOf("hashedValue1", "hashedValue2")
launch {
    try {
        val unblockingResult = withContext(Dispatcher.IO) {
            emailClient.unblockEmailAddressesByHashedValue(hashedValues)
        }
        when (unblockingResult) {
            is BatchOperationResult.SuccessOrFailureResult -> {
                // [unblockingResult.status] contains the status of the batch unblock operation.
            }
            is BatchOperationResult.Partial -> {
                // [unblockingResult.successValues] contains the list of items for which the unblock operation succeeded.
                // [unblockingResult.failureValues] contains the list of items for which the unblock operation failed.
            }
        }
    } catch (e: EmailBlocklistException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Listing Blocked Email Addresses

To obtain a list of blocked email addresses for a user, call the `getEmailAddressBlocklist` method. It will return an array of `UnsealedBlockedAddress` objects. They will have five properties:

| Property           | Description                                                                                                                                                                                                                                                      |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| status             | The status of the unsealing operation to access the plaintext address. Possible values are `Completed` or `Failed`. If it is `Failed` there will be an error type indicating the reason for the failure and the `address` property will contain an empty string. |
| address            | The plaintext version of the blocked address. This will be an empty string if the unsealing process fails.                                                                                                                                                       |
| hashedBlockedValue | The unique hash value representing the blocked address. This can be used for unblocking in the event that the plaintext address cannot be unsealed.                                                                                                              |
| action             | The action to take for incoming messages from blocked senders. (See above)                                                                                                                                                                                       |
| emailAddressId     | If included, the sender is only blocked for the email address associated with this id.                                                                                                                                                                           |

{% tabs %}
{% tab title="Typescript" %}

```typescript
try {
    const blockedAddresses = await emailClient.getEmailAddressBlocklist()
    // `blockedAddresses` is an array of UnsealedBlockedAddress objects
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
try {
    let blockedAddresses = try await self.emailClient.getEmailAddressBlocklist()
    // `blockedAddresses` is an array of UnsealedBlockedAddress objects
} catch {
    // Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val blockedAddresses = withContext(Dispatchers.IO) {
            emailClient.getEmailAddressBlocklist()
        }
        // `blockedAddresses` is an array of of UnsealedBlockedAddress objects
    } catch (e: EmailBlocklistException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}


# Email Notifications

The Sudo Platform Email Service sends push notifications on receipt of each email message to registered devices. These notifications contain metadata about the received email message encrypted using the public key registered with the email address that received the message.

There are three aspects to adding Sudo Platform Email Service push notification support to your application:

1. Notification registration\
   This aspect records your application's interest in Sudo Platform Email Service notifications with the Sudo Platform Notification Service.
2. Notification configuration\
   This aspect allows you to configure filtering of specific notifications. In particular, Sudo Platform Email SDK allows you to enable and disable transmission of message received notifications for particular email addresses.
3. Notification reception\
   This aspect allows you to decrypt the push notification payload and decide how to render the information in a notification displayed to the user.

## Notification Registration

Registering for notifications is performed using the Sudo Platform Notification SDK. Each platform has a slightly different way of doing this. See the platform specific descriptions below for the specific way required to integrate notifications for a particular language environment.

{% tabs %}
{% tab title="iOS" %}
The Sudo Notification Platform SDK for iOS takes as input a`NotificationFilterClient` provided by each Sudo Platform service that an application will receive notifications for. Each service's SDK provides a service specific filter client for this purpose.

The Sudo Platform Email Service SDK for iOS provides the `DefaultSudoEmailNotificationFilterClient` . The following sample code shows how this is registered with the Sudo Platform Notification Client SDK for iOS.

```swift
import SudoNotification
import SudoEmail

let emailNotificationFilterClient = DefaultSudoEmailNotificationFilterClient()

let notificationClient = try DefaultSudoNotificationClient(
  userClient: userClient,
  notifiableServices: [emailNotificationFilterClient]
)
```

{% endtab %}

{% tab title="Android" %}
The Sudo Notification Platform SDK for Android takes as input a`SudoNotificationClient` provided by each Sudo Platform service that an application will receive notifications for. Each service's SDK provides a service specific notification client for this purpose.

The Sudo Platform Email Service SDK provides the `SudoEmailNotificationClient` . The following sample code shows how this is registered with the Sudo Platform Notification Client SDK within the context of your `android.app.Application`:

```kotlin
import com.sudoplatform.sudoemail.SudoEmailClient
import com.sudoplatform.sudoemail.SudoEmailNotifiableClient
import com.sudoplatform.sudologging.AndroidUtilsLogDriver
import com.sudoplatform.sudologging.LogLevel
import com.sudoplatform.sudologging.Logger
import com.sudoplatform.sudonotification.SudoNotificationClient
import com.sudoplatform.sudouser.SudoUserClient

val logger = Logger("my-sudo-platform-application", AndroidUtilsLogDriver(LogLevel.DEBUG))

val sudoUserClient = SudoUserClient.builder(this)
    .setNamespace("my-sudo-platform-application")
    .setLogger(logger)
    .build()

val sudoEmailNotifiableClient = SudoEmailNotifiableClient.builder()
    .setContext(this)
    .setNotificationHandler(notificationHandler)
    .build()

val sudoNotificationClient = SudoNotificationClient.builder()
    .setContext(this)
    .setSudoUserClient(sudoUserClient)
    .setLogger(logger)
    .setNotifiableClients(listOf(sudoEmailNotifiableClient))
    .build()
```

The `notificationHandler` is an instance of a `SudoEmailNotificationHandler` that you will write to handle reception of Sudo Platform Email Service notifications. See [#notification-reception](#notification-reception "mention") for information on writing a `SudoEmailNotificationHandler` for your application.
{% endtab %}
{% endtabs %}

## Notification Configuration

Before your application will receive Sudo Platform Email Service notifications, you must enable them for the device's notification configuration. In addition to that you may choose to allow suppression sending of notifications for particular email addresses. Both of these tasks are accomplished with extensions to the Sudo Platform Notification SDK `NotificationConfiguration` class.

### Enabling Sudo Platform Email Notifications

The `.initEmailNotifications` extension is used to ensure a `NotificationConfiguration` object includes registering interest in all the notification types sent by the Sudo Platform Email Service.

This operation is non-destructive if other services' notification configuration is also present. It is also non-destructive to any email address specific configuration that may already be present in the notification configuration object.

{% tabs %}
{% tab title="iOS" %}

```swift
import SudoNotification
import SudoEmail

var configuration = NotificationConfiguration(configs: [])
do {
    configuration = try await sudoNotificationClient
        .getNotificationConfiguration(device: device)
} catch let error as SudoNotificationError {
    // Tolerate .notFound error
    guard case .notFound = error else {
        // Handle error
    }
}

// Ensure Sudo Platform Email Service notifications are enabled
configuration = configuration.initEmailNotifications()

let services = sudoNotificationClient.notifiableServices.map { $0.getSchema() }

do {
    configuration = try await sudoNotificationClient.setNotificationConfiguration(
        config: NotificationSettingsInput(
            bundleId: "<bundle-identifier>",
            deviceId: "<device-identitifier>",
            filter: configuration.configs,
            services: services
        )
    )
} catch {
   // Handle error
}
```

{% endtab %}

{% tab title="Android" %}

```kotlin
import com.sudoplatform.sudonotification.SudoNotificationClient

val configuration =
    try {
        // Retrieve this device's current configuration and ensure it is
        // initialized to receive email notifications
        sudoNotificationClient
            .getNotificationConfiguration(deviceInfo)
            .initEmailNotifications()
    } catch (e: SudoNotificationClient.NotificationException.NoDeviceNotificationException) {
        // If there is no current configuration, set default configuration which enables
        // all notifications
        NotificationConfiguration(configs = emptyList())
            .initEmailNotifications()
    }

sudoNotificationClient.setNotificationConfiguration(
    NotificationSettingsInput(
        bundleId = "<bundle-identitifier>",
        deviceId = "<device-identifier>",
        services = listOf(sudoEmailNotifiableClient.getSchema()),
        filter = configuration.configs,
    ),
)
```

{% endtab %}
{% endtabs %}

### Managing notification configuration for specific Sudos

Whether or not notifications for email addresses associated with a particular Sudo are sent by the Sudo Platform Email Service can be controlled by adjusting the notification configuration for the device.

Whether or not notifications are currently enabled for a particular Sudo can be determined as follows for each platform:

{% tabs %}
{% tab title="iOS" %}

```swift
import SudoNotification
import SudoEmail

let configuration: NotificationConfiguration

if configuration.areNotificationsEnabled(
       forSudoWithId: sudoId
   ) {
   // Notifications are enabled
} else {
   // Notifications are not enabled
}
```

{% endtab %}

{% tab title="Android" %}

```kotlin
import com.sudoplatform.sudoemail.isEmailNotificationForSudoIdEnabled
import com.sudoplatform.sudonotification.types.NotificationConfiguration

val configuration: NotificationConfiguration

if (configuration.isEmailNotificationForSudoIdEnabled(sudoId)) {
   // Notifications are enabled
} else {
   // Notifications are not enabled
}
```

{% endtab %}
{% endtabs %}

Notifications can be enabled or disabled for a particular Sudo by modifying and updating the `NotificationConfiguration` for the device:

{% tabs %}
{% tab title="iOS" %}

```swift

import SudoNotification
import SudoEmail

let configuration: NotificationConfiguration

let newConfiguration = configuration.setEmailNotificationsForSudoId(
    sudoId: sudoId,
    enabled: true or false
)

try await notificationClient.setNotificationConfiguration(
    config: NotificationSettingsInput(
        bundleId: "<bundle-id>",
        deviceId: "<device-id>",
        filter: newConfiguration.configs,
        services: [sudoEmailNotificationFilterClient.getSchema()]
    )
)
```

{% endtab %}

{% tab title="Android" %}

```kotlin
import com.sudoplatform.sudoemail.setEmailNotificationsForSudoId
import com.sudoplatform.sudonotification.types.NotificationConfiguration

val configuration: NotificationConfiguration

val newConfiguration = configuration.setEmailNotificationsForSudoId(
    sudoId = sudoId,
    enabled = true or false
)

// Call setNotificationConfiguration in IO context
sudoNotificationClient.setNotificationConfiguration(
    NotificationSettingsInput(
        bundleId = "<bundle-id>",
        deviceId = "<device-id>",
        services = listOf(sudoEmailNotifiableClient.getSchema()),
        filter = newConfiguration.configs,
    ),
)
```

{% endtab %}
{% endtabs %}

### Managing notification configuration for specific email addresses

Whether or not notifications that pertain to a particular email address are sent by the Sudo Platform Email Service can be controlled by adjusting the notification configuration for the device.

Whether or not notifications are currently enabled for a particular email address can be determined as follows for each platform:

{% tabs %}
{% tab title="iOS" %}

```swift
import SudoNotification
import SudoEmail

let configuration: NotificationConfiguration

if configuration.areNotificationsEnabled(
       forEmailAddressWithId: emailAddressId
   ) {
   // Notifications are enabled
} else {
   // Notifications are not enabled
}
```

{% endtab %}

{% tab title="Android" %}

```kotlin
import com.sudoplatform.sudoemail.isEmailNotificationForAddressIdEnabled
import com.sudoplatform.sudonotification.types.NotificationConfiguration

val configuration: NotificationConfiguration

if (configuration.isEmailNotificationForAddressIdEnabled(emailAddressId)) {
   // Notifications are enabled
} else {
   // Notifications are not enabled
}
```

{% endtab %}
{% endtabs %}

Notifications can be enabled or disabled for a particular email address by modifying and updating the `NotificationConfiguration` for the device:

{% tabs %}
{% tab title="iOS" %}

```swift

import SudoNotification
import SudoEmail

let configuration: NotificationConfiguration

let newConfiguration = configuration.setEmailNotificationsForAddressId(
    emailAddressId: emailAddressId,
    enabled: true or false
)

try await notificationClient.setNotificationConfiguration(
    config: NotificationSettingsInput(
        bundleId: "<bundle-id>",
        deviceId: "<device-id>",
        filter: newConfiguration.configs,
        services: [sudoEmailNotificationFilterClient.getSchema()]
    )
)
```

{% endtab %}

{% tab title="Android" %}

```kotlin
import com.sudoplatform.sudoemail.setEmailNotificationsForAddressId
import com.sudoplatform.sudonotification.types.NotificationConfiguration

val configuration: NotificationConfiguration

val newConfiguration = configuration.setEmailNotificationsForAddressId(
    emailAddressId = emailAddressId,
    enabled = true or false
)

// Call setNotificationConfiguration in IO context
sudoNotificationClient.setNotificationConfiguration(
    NotificationSettingsInput(
        bundleId = "<bundle-id>",
        deviceId = "<device-id>",
        services = listOf(sudoEmailNotifiableClient.getSchema()),
        filter = newConfiguration.configs,
    ),
)
```

{% endtab %}
{% endtabs %}

## Notification Reception

Sudo Platform notification messages contain encrypted content that needs to be decoded. The specific ways that notifications are received and decoded is platform specific. See the platform specific sections below for information on receiving and decoding Sudo Platform Email Service notifications.

{% tabs %}
{% tab title="iOS" %}
On iOS, in order to decode the notification you must implement a notification service app extension as described in [Modifying content in newly delivered notifications](https://developer.apple.com/documentation/usernotifications/modifying-content-in-newly-delivered-notifications) in the iOS developer documentation. This allows you to use information in the encrypted payload of the notification to modify the notification for display to the user.

If you receive notifications from other sources, you must first determine the source of the notification.

The Sudo Platform Notification SDK for iOS provides a `DefaultSudoNotifiableClient` class that decodes all Sudo Platform notifications. It is initializes with a service SDK specific implementation of `SudoNotifiableClient`. The Sudo Platform Email SDK provides `DefaultSudoEmailNotifiableClient` for this purpose.

To initialize the `DefaultSudoNotifiableClient` to decode email service notifications, do the following within your `UNNotificationServiceExtension` implementation's `didReceive` method.

Separate `SudoNotificationExtension` and `SudoEmailNotificationExtension` PODs are provided containing the minimal code needed to implement the notification service app extension with minimal overhead.

```swift
import SudoNotificationExtension
import SudoEmailNotificationExtension

class MyNotificationServiceExtension: UNNotificationServiceExtension {

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {

    let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    guard let bestAttemptContent = bestAttemptContent else {
        contentHandler(request.content)
        return
    }

    let sudoEmailNotifiableClient = DefaultSudoEmailNotifiableClient(keyNamespace: "eml")
    let sudoNotifiableClient = try DefaultSudoNotifiableClient(
        notifiableClients: [sudoEmailNotifiableClient]
    )

```

The `keyNamespace` specified in the construction of the `DefaultSudoEmailNotifiableClient` must match the `keyNamespace` you use to construct `DefaultSudoEmailClient` in the main application. If the key namespaces do not match then the `DefaultSudoEmailNotifiableClient` will not be able to access the keys needed to decrypt the notification payload.

{% hint style="info" %}
To access the keys, the application and the notification service app extension must both have the Keychain Sharing capability and have the same Keychain group.
{% endhint %}

Once you have initialized the `DefaultSudoNotifiableClient` you can then extract any Sudo Platform notification data from the notification. If no data is extracted then the notification has come from a source other than a Sudo Platform service.

```swift
    guard let sudoNotificationData = sudoNotifiableClient.extractData(
        fromNotificationContent: bestAttemptContent
    ) else {
        // Handle non-Sudo Platform notification
    }
```

Once you have identified the notification as originating at a Sudo Platform service, you can then decode it to a specific notification class:

```swift
    switch sudoNotifiableClient.decodeData(data: sudoNotificationData) {
    case let decoded as EmailMessageReceivedNotification:
        // handle EmailMessageReceivedNotification
    case let decoded as ....
        // handle other Sudo Platform notification
    }
```

{% endtab %}

{% tab title="Android" %}
On Android, a `SudoEmailNotificationHandler` specific to your application is required to be implemented.

This class will contain method of the form `on<notification-type>` for each type of notification your application is interested in handling.

An instance of this class is passed to `SudoEmailNotifiableClient` when it is constructed as part of initializing the `SudoNotificationClient`. See [Notification Registration](#notification-registration).

Within your application's `FirebaseMessagingService` implementation you call the `SudoNotificationClient`'s `process` method to examine the notification payload, extract any Sudo Platform notification payload, decrypt and decode it, and then pass it to the appropriate registered `SudoNotifiableClient`.

```kotlin
class MyFirebaseMessagingService implements FirebaseMessagingService {
    override fun onMessageReceived(message: RemoteMessage) {
        sudoNotificationClient.process(message)
    }
}

```

{% endtab %}
{% endtabs %}

### Notification Types

The Sudo Platform Email Service supports the following notification types

EmailMessageReceivedNotification

Sent on receipt of an email message, this notification contains metadata about the message that was received.

<table data-full-width="false"><thead><tr><th>Property</th><th>Description</th></tr></thead><tbody><tr><td>emailAddressId</td><td>ID of the email address to which the notification pertains</td></tr><tr><td>sudoId</td><td>ID of the Sudo owner of the email address to which the notification pertains</td></tr><tr><td>messageId</td><td>ID of the message to which the notification pertains</td></tr><tr><td>folderId</td><td>ID of the folder in which the message to which the notification pertains is stored</td></tr><tr><td>encryptionStatus</td><td>End-to-end encryption status of the message to which the notification pertains</td></tr><tr><td>subject</td><td>Subject, if any, of the message to which the notification pertains</td></tr><tr><td>from</td><td>Sender of the message to which the notification pertains</td></tr><tr><td>replyTo</td><td>Reply-To address, if any, of the message to which the notification pertains</td></tr><tr><td>hasAttachments</td><td>Whether or not the message to which the notification pertains has any attachments</td></tr><tr><td>sentAt</td><td>Date when the message to which the notification pertains was sent - the message's <code>Date</code> header</td></tr><tr><td>receivedAt</td><td>Date when the message to which the notification pertains was received</td></tr></tbody></table>


# Email Masks

Email Masks are an email relay capability to power your use cases with "privacy by design" built in.

**Email Masks** are a light weight alternative to [Email Addresses](/guides/email/email-addresses) for your use cases where email address obfuscations is necessary but you do not want to manage email message data.

Email Masks act as a relay mechanism that forward messages to and from your users personal email address (e.g. MySudo, Gmail, Outlook, Yahoo) via an intermediary email address (the "mask").

Email Masks can be used in multiple use cases including:

* Building consumer email masking solutions for general privacy enhancement.
* Protecting users personal email addresses from workers in:
  * "gig economy" type software platforms.
  * Booking confirmation platforms, etc.

## Key Features

<table><thead><tr><th width="245.5"></th><th></th></tr></thead><tbody><tr><td><a href="/pages/Gw7l9RZBkxrL2Tu576fw">Email Masks</a></td><td>The starting point into the email masks capability, provision email mask for your domain.</td></tr><tr><td><a href="/pages/gkrshIfALBwP2YsZyjpK">Verify Address Ownership</a></td><td>Creating an email mask pointed to an external address is a privileged operation that requires scrutiny.  Use the verification capability to prove ownership of the personal destination address.</td></tr><tr><td><a href="/pages/-M8mjWYRGqVgBfn_jeFY">Send Email Messages</a></td><td>Send and receive email messages in both E2EE use cases or to external email providers.  In either case, your personal email address is protected by our Email Masks.</td></tr></tbody></table>


# Email Mask Entitlements

Managing a user's Email Mask capabilities

The Sudo Platform entitlements system is used to control consumption of Sudo Platform resources, Email Masks are a feature who's usage is controlled by entitlements.  This allows you to control what and how many of each capability your users can consume within your own 'user plans' structures.

{% hint style="info" %}
Read more about the Sudo Platform entitlements system and how to integrate with it in the [Entitlements](/guides/entitlements) section of the Sudo Platform documentation.
{% endhint %}

The following table describes the entitlements used to control access to the Email Mask feature:

<table><thead><tr><th width="383.40234375">Entitlement</th><th width="106.5">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>sudoplatform.email.emailMaskUserEntitled</code></td><td>Boolean</td><td>Whether or not the user is entitled to provision new email masks</td></tr><tr><td><code>sudoplatform.email.emailMaskMaxPerUser</code></td><td>Numeric</td><td>The maximum number of active email masks per user.</td></tr></tbody></table>


# Managing Email Masks

Give your users the ability to secure their existing email addresses behind Email Masks.

**Email Masks** can be provisioned for different configured domains to provide more options to your users.  To provision an Email Mask the sequence is:

1. Retrieve the list of available domains
2. Check if the desired mask address for a given domain is valid and available
3. Provision the Email Mask address

### Get Email Mask Domains

The Sudo Platform Email Service can be configured to support multiple domains for your email masking needs.

{% hint style="info" %}
Each project in your Sudo Platform account can be configured to support a set of email domains. Your solutions engineer can assist you in configuring DNS entries for domains you own or you can have the Sudo Platform do that on your behalf.
{% endhint %}

A call to the `getEmailMaskDomains` method provides a list of domains that are available for provisioning Email Masks.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
try {
    const maskDomains = await emailClient.getEmailMaskDomains()
    // `maskDomains` contains an array of supported domains to be used for provisioning Email Masks
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
do {
    let maskDomains = try await client.getEmailMaskDomains()
    // `maskDomains` contains alist of domains available for provisioning Email Masks
catch {
    /// Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val maskDomains = withContext(Dispatchers.IO) {
            emailClient.getEmailMaskDomains()
        }
        // [maskDomains] contains a list of configured domains for provisioning Email Masks.
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
 }
```

{% endtab %}
{% endtabs %}

### Check Email Mask Availability

In order to ensure your users can choose a unique, available, address for their Email Mask, you can use the same `checkEmailAddressAvailability` method as you would for a regular Sudo Platform email address. More information on that method can be found [here](https://docs.sudoplatform.com/guides/email/manage-email-addresses#check-email-address-availability).

### Provision Email Mask

Once your user has the address they would like to use for their Email Mask, it can be provisioned. The `provisionEmailMask` method will return the newly provisioned Email Mask object.

An `ownershipProofToken` is required as part of the input. This ties together the Sudo and Email Mask such that the Sudo becomes the owner of the Mask. Use the `getOwnershipProof` method on the `SudoProfilesClient` in the **Sudo** **Profiles SDK** in order to obtain an `ownershipProofToken`. See the [Sudo](https://docs.sudoplatform.com/guides/sudos) section for more information.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const maskAddress: string = // The email address to use for the Mask, checked via the checkEmailAddressAvailability method
const ownershipProofToken: string = // Found via the get ownership proof API (See "Sudos" section)
const realAddress: string = // The email address that the Mask will forward to
try {
    const emailMask = await emailClient.provisionEmailMask({
        maskAddress,
        realAddress,
        ownershipProofToken,
    })
} catch (e) {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
let maskAddress: String = /// The email address to use for the Mask, checked via the checkEmailAddressAvailability method
let ownershipProofToken: String = /// Found via the get ownership proof API (See "Sudos" section)
let realAddress: String = /// The real email address that the Mask will forward to
do {
    let provisionEmailMaskInput = ProvisionEmailMaskInput(
        maskAddress: maskAddress,
        ownershipProofToken: ownershipProofToken,
        realAddress: realAddress
    )
    let emailMask = try await emailClient.provisionEmailMask(
        withInput: provisionEmailMaskInput
    )
    /// `emailMask` contains the newly provisioned Email Mask
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val maskAddress: String = /// The email address to use for the Mask, checked via the checkEmailAddressAvailability method
val ownershipProofToken: String = /// Found via the get ownership proof API (See "Sudos" section)
val realAddress: String = /// The real email address that the Mask will forward to
launch {
    try {
        val input = ProvisionEmailMaskInput(
            maskAddress = maskAddress,
            ownershipProofToken = ownershipProofToken,
            realAddress = realAddress,
        )
        val emailMask = withContext(Dispatchers.IO) {
            emailClient.provisionEmailMask(input)
        }
        // The returned [emailMask] has been provisioned
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

#### Verify Address Ownership

The `realAddress` property of the input must be the exact `local-part@domain` email address that the mask should forward to.  For Sudo Platform Email Addresses, the address must be owned by the same user trying to provision the Mask.

If your use case requires to allow your users to set up Masks for external email addresses (e.g. gmail, yahoo etc), then the email mask that is provisioned will initially have its `state` set to `PENDING`. While in this state, it cannot receive messages. In order to change the `state` to `ENABLED`, the user must verify ownership of their external email address. See [Verifying External Email Addresses](/guides/email/email-masks/verifying-external-email-addresses).

#### Entitlements <a href="#entitlements" id="entitlements"></a>

In order to be able to provision an Email Mask, the user must be entitled. The following entitlement checks are performed:

1. The user is entitled to provision Email Masks by the `sudoplatform.email.emailMaskUserEntitled` entitlement.
2. The user against which the new email address is to be provisioned has at least 1 remaining `sudoplatform.email.emailMaskMaxPerUser` entitlement to consume.

If these entitlements' checks do not succeed, the `provisionEmailMask` API fails with an insufficient entitlements error.

{% hint style="info" %}
See [Email Entitlements](https://docs.sudoplatform.com/guides/email/email-entitlements) for an overview of how the Email service integrates with the Sudo Platform Entitlements system.
{% endhint %}

#### Optional Parameters

The `provisionEmailMask` method's input object can take up to two additional optional parameters:

1. `expiresAt` - If set, the mask will automatically be deprovisioned at the given date.
2. `metadata` - This is a string to string map that can be used to store information about the Email Mask such as the purpose of it. The data in this property gets encrypted with the user's default key pair before being sent to the server, and decrypted upon retrieval.

Both of these optional properties can be updated, or deleted via the `updateEmailMask` method.

### Deprovision Email Mask

An Email Mask can be deprovisioned using the `deprovisionEmailMask` method by passing in the `id` of an existing Email Mask object.

{% hint style="warning" %}
By deprovisioning an Email Mask, the user will no longer receive messages sent to that Mask, and a bounce will be returned to anyone sending messages to it.
{% endhint %}

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const emailMaskId: string = emailMask.id // Collect the Email Mask however makes sense for your application
try {
    const deprovisionedEmailMask = await emailClient.deprovisionEmailMask({
        emailMaskId: emailMaskId
    })
} catch (e) {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input email mask id however makes sense for your implementation.
let emailMaskId = emailMask.id
do {
    let deprovisionEmailMaskInput = DeprovisionEmailMaskInput(
        emailMaskId: emailMaskId
    )
    let deprovisionedEmailMask = try await emailClient.deprovisionEmailMask(
        withInput: deprovisionEmailMaskInput
    )
    /// `deprovisionedEmailMask` is the record that was just deprovisioned.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
/// Collect the input email mask id however makes sense for your implementation.
val emailMaskId = emailMask.id
launch {
    try {
        val input = DeprovisionEmailMaskInput(
            emailMaskId = emailMaskId
        )
        val emailMask = withContext(Dispatchers.IO) {
            emailClient.deprovisionEmailMask(input)
        }
        // The returned [emailMask] has been deprovisioned
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Updating an Email Mask

An existing email mask can have a subset of its data updated by the user by calling the `updateEmailMask` method. The properties that can be updated are outlined in the table below.

<table><thead><tr><th width="105.00390625">Property</th><th width="339.94921875">To ignore</th><th>To delete</th></tr></thead><tbody><tr><td>expiresAt</td><td>TypeScript: <code>undefined</code> (or exclude from object)<br>Swift: <code>nil</code><br>Kotlin: <code>null</code></td><td>Typescript: <code>null</code><br>Swift: <code>Date(timeIntervalSince1970: 0)</code><br>Kotlin: <code>Date(0)</code></td></tr><tr><td>metadata</td><td>TypeScript: <code>undefined</code> (or exclude from object)<br>Swift: <code>nil</code><br>Kotlin: <code>null</code></td><td>Typescript: <code>null</code><br>Swift: <code>[:]</code><br>Kotlin: <code>emptyMap()</code></td></tr></tbody></table>

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const emailMaskId: string = emailMask.id // Collect the Email Mask however makes sense for your application
try {
    const metadata = { prop1: 'abc', prop2: 123 }
    const expiresAt = new Date() // Construct the expiry date however makes sense
    let updatedMask = await emailClient.updateEmailMask({
        emailMaskId: emailMaskId,
        metadata: metadata,
        expiresAt: expiresAt,
    })
    // The Email Mask now has the updated properties
    
    updatedMask = await emailClient.updateEmailMask({
        emailMaskId: emailMaskId,
        metadata: null,
        expiresAt: null,
    })
    // The Email Mask no longer has the `metadata` or `expiresAt` set
} catch (e) {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Collect the input email mask id however makes sense for your implementation.
let emailMaskId = emailMask.id
do {
    let metadata = [ "prop1": "abc", "prop2": String(123) ]
    let expiresAt = Date() /// Construct the expiry date however makes sense
    var input = UpdateEmailMaskInput(
        emailMaskId: emailMaskId,
        metadata: metadata,
        expiresAt: expiresAt
    )
    var updatedMask = try await emailClient.updateEmailMask(withInput: input)
    // The Email Mask now contains the updated properties
    
    input = UpdateEmailMaskInput(
        emailMaskId: emailMaskId,
        metadata: [:],
        expiresAt: Date(timeIntervalSince1970: 0)
    )
    updatedMask = try await emailClient.updateEmailMask(withInput: input)
    // The Email Mask no longer has metadata or expiresAt
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
/// Collect the input email mask id however makes sense for your implementation.
val emailMaskId = emailMask.id
launch {
    try {
        val metadata = mapOf(
            "prop1" to "abc",
            "prop2" to "${123}"
        )
        val expiresAt = Date() // Construct the expiry date however makes sense
        var input = UpdateEmailMaskInput(
            emailMaskId = emailMaskId,
            metadata = metadata,
            expiresAt = expiresAt
        )
        var updatedMask = withContext(Dispatchers.IO) {
            emailClient.updateEmailMask(input)
        }
        // The Email Mask now contains the updated properties
        
        input = UpdateEmailMaskInput(
            emailMaskId = emailMaskId,
            metadata = emptyMap(),
            expiresAt = Date(0)
        )
        updatedMask = withContext(Dispatchers.IO) {
            emailClient.updateEmailMask(input)
        }
        // The Email Mask no longer has metadata or expiresAt
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Retrieving Existing Email Masks

Previously provisioned Email Masks can be retrieved as a paginated list via the `listEmailMasksForOwner` method.

A call to the API will return a `ListOutput<EmailMask>` object. The `items` property will contain a list of the masks and the `nextToken` property will contain a token that can be used for retrieving the next page of results if there are more.

The list can also be filtered by passing in an optional `filter` property to the method call. The call can be filtered by `status` and/or `realAddressType`. Each of those must further be specified as either `equal`, `oneOf`, `notEqual` or `notOneOf`.

The list can also be given a `limit` on how many results to return. By default, this limit is 10.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const emailMaskId: string = emailMask.id // Collect the Email Mask however makes sense for your application
try {
    const result = await emailClient.listEmailMasksForOwner({
        filter: {
            status: { notOneOf: [EmailMaskStatus.PENDING, EmailMaskStatus.LOCKED] },
            realAddressType: { equal: EmailMaskRealAddressType.INTERNAL },
        }
    }
    // `result.items` contains a list of Masks that are not PENDING or LOCKED and have a real
    // address type of INTERNAL
    // Page through the results if result.nextToken != undefined.
} catch (e) {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Collect the input email mask id however makes sense for your implementation.
let emailMaskId = emailMask.id
do {
    let filter = EmailMaskFilter(
        status: .notOneOf([.pending, .locked]),
        realAddressType: .equal(.internal),
    )
    let input = ListEmailMasksForOwnerInput(
        filter: filter
    )
    let result = try await emailClient.listEmailMasksForOwner(withInput: input)
    // `result.items` contains a list of Masks that are not PENDING or LOCKED and have a real
    // address type of INTERNAL
    // Page through the results if result.nextToken != nil.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
/// Collect the input email mask id however makes sense for your implementation.
val emailMaskId = emailMask.id
launch {
    try {
        val filter = EmailMaskFilterInput(
            status = NotOneOfStatusFilter(
                listOf(EmailMaskStatus.PENDING, EmailMaskStatus.LOCKED)
            ),
            realAddressType = EqualRealAddressTypeFilter(EmailMaskRealAddressType.INTERNAL)
        )
        val input = ListEmailMasksForOwnerInput(filter = filter)
        val result = withContext(Dispatchers.IO) { 
            emailClient.listEmailMasksForOwner(input)
        }
        // `result.items` contains a list of Masks that are not PENDING or LOCKED and have a real
        // address type of INTERNAL
        // Page through the results if result.nextToken != null.
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Disabling and Enabling Email Masks

An Email Mask can be disabled by the user to temporarily stop it from receiving emails without fully deprovisioning it.  While in the `DISABLED` state, messages sent to the mask will not be delivered to the real address, and the sender will **not** receive a bounce in response until it is enabled again.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const emailMaskId: string = emailMask.id // Collect the Email Mask however makes sense for your application
try {
    const disabledMask = await emailClient.disableEmailMask({
        emailMaskId: emailMaskId,
    })
    // Mask is now disabled; disabledMask.status == EmailMaskStatus.DISABLED
    
    const enabledMask = await emailClient.enableEmailMask({
        emailMaskId: emailMaskId,
    })
    // Mask is now enabled; enabledMask.status == EmailMaskStatus.ENABLED
} catch (e) {
    // Handle/notify user of errors 
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
// Collect the input email mask id however makes sense for your implementation.
let emailMaskId = emailMask.id
do {
    let disableInput = DisableEmailMaskInput(emailMaskId: emailMaskId)
    let disabledMask = try await client.disableEmailMask(withInput: disableInput)
    /// Email Mask is now disabled; disabledMask.status == .disabled
    
    let enableInput = EnableEmailMaskInput(emailMaskId: emailMaskId)
    let enabledMask = try await client.enableEmailMask(withInput: enableInput)
    /// Email Mask is now enabled; disabledMask.status == .enabled
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
/// Collect the input email mask id however makes sense for your implementation.
val emailMaskId = emailMask.id
launch {
    try {
        val disableInput = DisableEmailMaskInput(emailMaskId)
        val disabledMask = withContext(Dispatchers.IO) {
            emailClient.disableEmailMask(disableInput)
        }
        // Email Mask is now disabled; disabledMask.status == EmailMaskStatus.DISABLED
        
        val enableInput = EnableEmailMaskInput(emailMaskId)
        val enabledMask = withContext(Dispatchers.IO) {
            emailClient.enableEmailMask(disableInput)
        }
        // Email Mask is now enabled; disabledMask.status == EmailMaskStatus.ENABLED
    } catch (e: EmailAddressException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}


# Verifying External Email Addresses

Ensuring that the user owns an external email address is a critical step in setting up an Email Mask to forward to that address.

If you provide the option to provision Email Masks that are associated with an external (i.e. Non-Sudo Platform Email Service) email address, the Mask will initially have its `state` set to `PENDING`. While in this state, the Mask will not be able to receive (or reply to) messages. In order to change its `state` to `ENABLED` , you must verify that the user owns the email address. To do this, you must call the `verifyExternalEmailAddress` method. This will trigger a One-Time Passcode (OTP) verification flow in which an email will be sent to the email address with a six-digit code that must then be passed into another call to `verifyExternalEmailAddress`  for verification.

{% hint style="info" %}
Note that this process is **not** needed for Email Masks associated with a Sudo Platform email address.
{% endhint %}

### Sending the One-Time Passcode

To initiate the OTP flow, first call `verifyExternalEmailAddress` with the `id` of the Email Mask being enabled and the email address to verify.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const emailMaskId: string = emailMask.id // Get the Email Mask from the `provisionEmailMask` method
const emailAddress: string // The email address must match the one passed in as the `realAddress` property of `provisionEmailMask`
try {
    await emailClient.verifyExternalEmailAddress({
        emailMaskId: emailMaskId,
        emailAddress: emailAddress,
    })
    // At this point, the method should return `undefined`
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```
// To be added
```

{% endtab %}

{% tab title="Kotlin" %}

```
// To be added
```

{% endtab %}
{% endtabs %}

After this call, the user should receive an email from the Sudo Platform Email Service with the OTP that will need to be passed into the next call.

{% hint style="info" %}
In order to use this functionality, you must define a domain for the email to come from, as well as provide an HTML template for the email body. Your solutions engineer can assist you in setting this up.
{% endhint %}

### Verifying the One-Time Passcode

Once the user has received the OTP from their email address, it must be passed in to another call to `verifyExternalEmailAddress` to be verified by the service. If that is successful, the Email Mask will have its `state` updated to `ENABLED` and it will be ready to use.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const emailMaskId: string = emailMask.id // Get the Email Mask from the `provisionEmailMask` method
const emailAddress: string // The email address must match the one passed in as the `realAddress` property of `provisionEmailMask`
const verificationCode: string // The code that the user has retrieved from the email sent to their real email address
try {
    const result = await emailClient.verifyExternalEmailAddress({
        emailMaskId: emailMaskId,
        emailAddress: emailAddress,
        verificationCode: verificationCode
    })
    // `result.isVerified` will be true if verification was successful, and the Email Mask will have its state set to ENABLED
    // If it was not, then `result.reason` will contain a string with the reason it failed
} catch (e) {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Swift" %}

```
// To be added
```

{% endtab %}

{% tab title="Kotlin" %}

```
// To be added
```

{% endtab %}
{% endtabs %}


# Send & Receive Masked Email Messages

Email Mask addresses automatically route email messages to and from the user's personal email address they have chosen to associate with the mask via the verification process.

The handling of email messages being routed via an Email Mask is different depending on where the users personal email address is hosted, those are:

* [**Internal Email Mask**](#internal-email-masks)**:** Where the personal address is another Sudo Platform owned **Email Address** hosted in your Sudo Platform deployment.
* [**External Email Masks**](#external-email-masks)**:**  Where the personal address is an externally hosted email account e.g. Gmail, Yahoo, Outlook etc.

### Internal Email Masks

An **Internal Email Mask** allows the user to freely send and receive messages via their Mask, without exposing their personal email address to other participants in the email chain.

#### Inbound Message Receiving

When the Sudo Platform Email Service receives an email message to an **Internal Email Mask** it is processes in almost an identical manner as emails sent directly to [Email Address](/guides/email/email-addresses/send-an-email) i.e. It will be placed in the `INBOX` of the Email Account and can be distinguished from other messages by the presence of the `emailMaskId` property on the `emailMessage` object.  These messages can be retrieved the same way as any other message as described in [Send & Receive Email Messages](/guides/email/email-addresses/send-an-email#receiving-email-messages)

#### Outbound Message Sending

Users can send messages directly using their **Internal Email Mask**. To do this, invoke the `sendMaskedEmailMessage` method.  This works in much the same way as usual, including with the ability to send end-to-end encrypted messages where possible. See [Send & Receive Email Messages](/guides/email/email-addresses/send-an-email) for more detail

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Collect the input headers, body, attachments and sender email mask id 
// however makes sense for your implementation.
const senderEmailMaskId: string = emailMask.id 
const emailMessageHeader: InternetMessageFormatHeader = {
    from: { emailAddress: "from@bar.com" }, // Must match emailMask.maskAddress
    to: [{ emailAddress: "to@bar.com" }],
    cc: [{ emailAddress: "cc@bar.com" }],
    bcc: [{ emailAddress: "bcc@bar.com" }],
    replyTo: [{ emailAddress: "replyTo@bar.com" }],
    subject: "Example subject line",
}
const emailAttachment: EmailAttachment = {
    filename: "fooAttachment.pdf",
    contentId: uuid.v4(),
    mimeType: "application/pdf",
    inlineAttachment: false,
    data: // ... Some pdf data
}
const inlineAttachment: EmailAttachment = {
    filename: "fooImage.png",
    contentId: uuid.v4(),
    mimeType: "image/png",
    inlineAttachment: true,
    data: // ... Some image data
}
try {
    const input: SendMaskedEmailMessageInput = {
        senderEmailMaskId: senderEmailMaskId,
        emailMessageHeaders: emailMessageHeader,
        body: "An example email body",
        attachments: [emailAttachment],
        inlineAttachments: [inlineAttachment],
    }
    const result = await emailClient.sendMaskedEmailMessage(input)
    // `result` contains the identifier and created timestamp associated with the sent email message. You can use this to access the data of the email message.
} catch {
    // Handle/notify user of error 
}

```

{% endtab %}

{% tab title="Swift" %}

```swift
/// Collect the input headers, body, attachments and sender email mask id 
/// however makes sense for your implementation.
let senderEmailMaskId: String = emailMask.id
let emailMessageHeader = InternetMessageFormatHeader(
    from: EmailAddressDetail(emailAddress: "from@bar.com"), // Must match emailMask.maskAddress
    to: [EmailAddressDetail(emailAddress: "to@bar.com")],
    cc: [EmailAddressDetail(emailAddress: "cc@bar.com")],
    bcc: [EmailAddressDetail(emailAddress: "bcc@bar.com")],
    replyTo: [EmailAddressDetail(emailAddress: "replyTo@bar.com")],
    subject: "Example subject line"
)
let emailAttachment = EmailAttachment(
    filename: "fooAttachment.pdf",
    contentId: UUID().uuidString,
    mimetype: "application/pdf",
    inlineAttachment: false,
    data: // ... Some pdf data
)
let inlineAttachment = EmailAttachment(
    filename: "fooImage.png",
    contentId: UUID().uuidString,
    mimetype: "image/png",
    inlineAttachment: true,
    data: // ... Some image data
)
do {
    let input = SendMaskedEmailMessageInput(
        senderEmailMaskId: senderEmailMaskId,
        emailMessageHeaders: emailMessageHeader,
        body: "An example email body",
        attachments: [emailAttachment],
        inlineAttachments: [inlineAttachment]
    )
    let result = try await emailClient.sendMaskedEmailMessage(
        withInput: input
    )
    /// `result` contains the identifier and created timestamp associated with the sent email message. You can use this to access the data of the email message.
} catch {
    /// Handle/notify user of error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// Collect the input headers, body, attachments and sender email mask id 
// however makes sense for your implementation.
val senderEmailMaskId: String = emailMask.id
val emailMessageHeader = InternetMessageFormatHeader(
    from = EmailMessage.EmailAddress("from@bar.com"), // Must match emailMask.maskAddress
    to = listOf(EmailMessage.EmailAddress("to@bar.com")),
    cc = listOf(EmailMessage.EmailAddress("cc@bar.com")),
    bcc = listOf(EmailMessage.EmailAddress("bcc@bar.com")),
    replyTo = listOf(EmailMessage.EmailAddress("replyTo@bar.com")),
    subject = "Example subject line",
)
val emailAttachment = EmailAttachment(
    fileName = "fooAttachment.pdf",
    contentId = UUID.randomUUID().toString(),
    mimeType = "application/pdf",
    inlineAttachment = false,
    data = // ... Some pdf data
)
val inlineAttachment = EmailAttachment(
    fileName = "fooImage.png",
    contentId = UUID.randomUUID().toString(),
    mimeType = "image/png",
    inlineAttachment = true,
    data = // ... Some image data
)
launch {
    try {
        val input = SendMaskedEmailMessageInput(
            senderEmailMaskId = senderEmailMaskId,
            emailMessageHeaders = emailMessageHeader,
            body = "An example email body",
            attachments = listOf(emailAttachment),
            inlineAttachments = listOf(inlineAttachment),
        )
        val result = withContext(Dispatchers.IO) {
            emailClient.sendMaskedEmailMessage(input)
        }
        // [result] contains the identifier and created timestamp associated with the sent email message. You can use this to access the data of the email message.
    } catch (e: EmailMessageException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### External Email Masks

An **External Email Mask** handles messages significantly differently to [Internal Email Masks](#internal-email-masks). The Sudo Platform Email Service does **not** store a copy of the body or other headers of messages it receives for External Email Masks. Instead, it adjusts the headers then forwards the message to the user's personal email address. There are no methods in the **Sudo Email SDK** associated with sending or receiving messages associated with an External Email Mask.e

#### Inbound Message Receiving

The Sudo Platform Email Service must make some changes to the headers of an email before forwarding it to the user's personal email address.

For the following example, assume the following:

* The domain set up for Email Masks in the Sudo Platform is `mask.sudoplatform.com`
* **Email Mask**
  * Mask email address: `superman@mask.sudoplatform.com`
  * External email address: `clark.kent@gmail.com`
* **Sender's address:** `lex.luthor@yahoo.com`

When a message is received that has been sent to `superman@mask.sudoplatform.com` from `lex.luthor@yahoo.com`, the Sudo Platform Email Service makes the following changes to the message headers before forwarding the message to `clark.kent@gmail.com`:

* The `From` header is changed to `"lex.luthor@yahoo.com via" <superman@mask.sudoplatform.com>`&#x20;
* The `To` header (or `Cc` if that was used) is changed to `clark.kent@gmail.com`
* The `Reply-To` header is changed to `replies@mask.sudoplatform.com`

The service will then forward the message to `clark.kent@gmail.com` and store the message identifier along with the `emailMaskId` and the sender's address securely in order to allow for future replies to be handled.

#### Outbound Message Sending

A user can reply to messages received via their **External Email Mask** from their preferred email client, by the standard email reply flow. The message will be sent to `replies@mask.sudoplatform.com` , processed by the Sudo Platform Email Service, and forwarded to the original sender.

{% hint style="info" %}
Important: A user **cannot initiate an email chain via an External Email Mask**. They can only reply to messages they have already received. That is, there is no way for `superman@mask.sudoplatform.com` to send an email to `lois.lane@gmail.com` without having first received at least one email from `lois.lane@gmail.com`.
{% endhint %}

Continuing with the above example, when a message is received that has been sent to `replies@mask.sudoplatform.com`, from `clark.kent@gmail.com`, the Sudo Platform Email Service will read the `In-Reply-To` header of the message to get the id of the original message and attempt to retrieve the record associated with it. If it is available, it will look up the Email Mask associated with the `emailMaskId` and the original sender's email address. It will then make the following changes to the message headers:

* The `From` header is changed to `superman@mask.sudoplatform.com`
* The `To` header is changed to `lex.luthor@yahoo.com`

The service will also make an effort to remove any other headers that may contain `clark.kent@gmail.com` . The message is then forwarded to `lex.luthor@yahoo.com` .


# SDK Releases

Additional resources for the Email SDKs

Use the links below to learn about significant new features and fixes added in each release of the **Email SDK** and to access the source code for each platform's SDK and sample app.

## Email JS SDK

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-email-js/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-email-js)
* [Sample app source code](https://github.com/sudoplatform/samples-js)

## Email iOS SDK

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-email-ios/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-email-ios)
* [Sample app source code](https://github.com/sudoplatform/samples-ios/tree/master/email)

## Email Android SDK

* [SDK releases and changelog](https://github.com/sudoplatform/sudo-email-android/releases)
* [SDK source code](https://github.com/sudoplatform/sudo-email-android)
* [Sample app source code](https://github.com/sudoplatform/samples-android/tree/master/email)


# API Reference

API references for the Email SDKs

The links below provide a detailed API reference for the **Email SDKs**.

* [JS API Reference](https://sudoplatform.github.io/sudo-email-js/)
* [iOS API Reference](https://sudoplatform.github.io/sudo-email-ios/)
* [Android API Reference](https://sudoplatform.github.io/sudo-email-android/)


# Decentralized Identity

Leverage the power of Decentralized Identity in your application

The **Decentralized Identity SDKs** allow you to offer next generation digital identities to your users. Decentralized identity takes an approach that removes the need for a trusted third-party service altogether. In its place is blockchain/ledger technology, the key innovation that can be used to anchor the identity and provide a trusted source.

The main difference with decentralized identity is that every identity in the system is a peer to every other identity. Whether it’s a user or a server, each has its own identity and can partake in decentralized identity operations with any other identity.

## Key Features

|                                       |                                                                                                                                                        |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Identity Wallet                       | An identity wallet is used to store and manage encryption keys, peer connections, and verifiable credentials associated with a decentralized identity. |
| DIDs                                  | Decentralized Identifiers (DIDs) are a W3C standard that underpin verifiable, decentralized digital identities.                                        |
| Pairwise Key Exchange                 | Key exchange protocols between two decentralized identities allowing private and trusted peer-to-peer connections.                                     |
| Peer-to-Peer Encrypted Communications | Inherent to decentralized identities is the ability to perform end to end encrypted communications between peers.                                      |
| Verifiable Credentials                | Identities can make assertions which are cryptographically verifiable by the receiving party.                                                          |

## Next Steps

* Learn more about the Sudo Platform Decentralized Identity [Edge Agent](/guides/decentralized-identity/decentralized-identity) and [Cloud Agent](/guides/decentralized-identity/cloud-agent)


# Edge Agent

Providing Decentralized Identity edge agent solutions to your users.

Sudo Platform provides a collection of Decentralized Identity Edge Agent based SDKs, which can be combined to create customised Edge Agent applications. These SDKs consist of the following:

<table><thead><tr><th width="212.4102564102564">Component</th><th>Description</th></tr></thead><tbody><tr><td><a href="/pages/fk9CfYV6wuSFxdyOjdWf">Relay SDK</a></td><td>A native mobile SDK for an "always-on" communication endpoint for a mobile Wallet.</td></tr><tr><td><a href="/pages/OZkTOiZUh086DflmyFre">Edge Agent SDK</a></td><td>A native mobile SDK for Edge Agent protocols and data management for a mobile Wallet. Including the ability to easily establish peer to peer encrypted connections, accept credentials and present them in alignment with Hyperledger Aries and OpenID4VC Protocols.</td></tr></tbody></table>


# Relay SDK

Providing a scalable relay for your decentralized communications.

The **Relay SDK** allows your users to receive messages from their peers. Your users can create, enable, disable and delete a postbox whenever they need to. They can receive notifications of received messages, and retrieve and delete those messages as required.

‌Decentralized identity relays allow for messages to be sent to your users even when they are offline. Relays receive and store messages for when your users come back online. The **Relay SDK** lets your users set up a persistent endpoint for peers to send messages to, regardless of the users' network availability.

## Key Features

|                                                                                                                                                      |                                                                                      |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [Postboxes](/guides/decentralized-identity/decentralized-identity/relay-sdk/manage-relay-postboxes)                                                  | Give your users their own postbox that is dedicated to storing their messages.       |
| [Retrieve Messages](/guides/decentralized-identity/decentralized-identity/relay-sdk/storing-and-retrieving-relay-messages#retrieving-relay-messages) | Read messages stored in the postboxes.                                               |
| [Receive Messages](/guides/decentralized-identity/decentralized-identity/relay-sdk/receiving-messages)                                               | Receiving real-time messages arriving into a user's postbox from peers is made easy. |

## Next Steps

* Integrate the **Relay SDK** into your [iOS](/guides/decentralized-identity/decentralized-identity/relay-sdk/integrate-the-relay-sdk#integrate-the-ios-sdk) and [Android ](/guides/decentralized-identity/decentralized-identity/relay-sdk/integrate-the-relay-sdk#integrate-the-ios-sdk-1)applications


# Integrate the Relay SDK

Integrate the Relay SDK into your application

## Prerequisites <a href="#prerequisites" id="prerequisites"></a>

1. [​Complete the Getting Started Guide​](/guides/getting-started)
2. [Integrate the User SDK](/guides/users/integrate-the-user-sdk)
3. [Integrate the Sudo SDK](/guides/sudos/integrate-the-sudo-sdk)

## Get Started <a href="#get-started" id="get-started"></a>

* [​Integrate the iOS SDK​](#integrate-the-ios-sdk)
* [​Integrate the Android SDK​](#integrate-the-ios-sdk-1)

## Integrate the iOS SDK <a href="#integrate-the-ios-sdk" id="integrate-the-ios-sdk"></a>

To use the Relay SDK in an iOS app, you need to install the SudoDIRelay package via Swift Package Manager

Open your project settings in XCode, and go to the `Package Dependencies` tab. Click on the `+` sign to add a dependency.&#x20;

Enter the repository URL [`https://github.com/sudoplatform/sudo-di-relay-ios`](https://github.com/sudoplatform/sudo-di-relay-ios) in the top right search box and select the `sudo-di-relay-ios` repository.&#x20;

Select the required version and `Add Package`.&#x20;

Sudo Platform SDKs conform to semantic versioning so in most cases you will leave the Dependency Rule as `Up to Next Major Version` to receive regular updates without introducing any breaking changes.

This will resolve the local package dependency and install the latest version of the Relay SDK.

{% hint style="info" %}
In order to instantiate a Relay client, make sure you have followed instructions for [Getting Started](/guides/getting-started), [User SDK](/guides/users/integrate-the-user-sdk) and [Sudo Profiles SDK](/guides/sudos/integrate-the-sudo-sdk) (see [Prerequisites](#prerequisites) above).
{% endhint %}

To instantiate the relay client in your application, add the following:

{% tabs %}
{% tab title="Swift" %}

```swift
import SudoDIRelay

let userClient = // ... see "Users" docs
let relayClient = try DefaultSudoDIRelayClient(
    sudoUserClient: sudoUserClient
)
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one `DefaultSudoDIRelayClient` instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open-source iOS sample app ](https://github.com/sudoplatform/samples-ios)on GitHub that our team published as a reference for integrating the **Relay SDK** into your app as quickly and seamlessly as possible.
{% endhint %}

## Integrate the Android SDK <a href="#integrate-the-ios-sdk" id="integrate-the-ios-sdk"></a>

The Android SDK is open source and compatible with Android 6 (API level 23) and above.

{% tabs %}
{% tab title="Gradle" %}
Add this line to the dependencies section of the app module `build.gradle` and synchronize your project with Android Studio.

```groovy
dependencies {
    implementation 'com.sudoplatform:sudodirelay:$latest_version'
}
```

{% hint style="info" %}
The latest version of the SDK can be found at [SDK Releases](/guides/decentralized-identity/decentralized-identity/relay-sdk/sdk-releases).
{% endhint %}

{% hint style="info" %}
In order to instantiate a Relay client, make sure you have followed instructions for [Getting Started](/guides/getting-started), [User SDK](/guides/users/integrate-the-user-sdk) and [Sudo Profiles SDK](/guides/sudos/integrate-the-sudo-sdk) (see [Prerequisites](#prerequisites) above).
{% endhint %}
{% endtab %}
{% endtabs %}

### Client Initialization

To instantiate a client in your application add the following:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// val appContext: Context // The Android application context
val userClient = // ... see "Users" docs

val relayClient = SudoDIRelayClient.builder()
    .setContext(appContext)
    .setSudoUserClient(userClient)
    .build()
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
You only need one `SudoDIRelayClient` instance for a given user per device. Instantiating multiple clients with the same configuration and using them at the same time may cause unexpected runtime errors to occur.
{% endhint %}

{% hint style="success" %}
**See it in action.** Be sure to take a look at the [open-source Android sample app](https://github.com/sudoplatform/samples-android) on GitHub that our team published as a reference for integrating the Decentralized Identity Relay SDK into your app as quickly and seamlessly as possible.
{% endhint %}


# Relay Entitlements

Managing consumption of Relay capabilities

There are no entitlements associated with creation of relay postboxes or messages.&#x20;

Management of entitlements associated with the owning sudo is done via the [Sudo entitlements ](/guides/sudos/sudo-entitlements)infrastructure.


# Manage Relay Postboxes

Provides the essentials to give your users their postbox.

Postboxes are a fundamental component of the **Relay SDK**. Postboxes are a store of peer based messages to your user. These postboxes can be provisioned to receive messages from the user's peers.&#x20;

Once the postbox is provisioned, it returns the service endpoint associated with that postbox. This value can be provided to users who wish to write messages to the postbox.

Provisioning a postbox requires that the postbox be created, and the the postbox's service endpoint be shared with the peer wishing to send messages to the user. The sharing mechanism is client-specific and outside the scope of the Relay SDK

## Creating a Relay Postbox <a href="#get-email-domains" id="get-email-domains"></a>

A new postbox can be created for managing messages associated with a decentralized identity connection by calling the `createPostbox` function with the connection's unique identifier. The postbox may be created in an `enabled` or `disabled` state; disabled postboxes will not accept and store messages although existing messages may still be retrieved.&#x20;

Additionally, a Sudo `ownershipProofToken`, used to validate the ownership of the current sudo by the user, must be provided to the API. This can be obtained via the [SudoProfiles getOwnershipProof API](/guides/sudos/managing-sudos#get-ownership-proof) using an audience of `sudoplatform.relay.postbox`.

{% tabs %}
{% tab title="Swift" %}

```swift
// currentSudo: the Sudo object which will own the created postbox

let connectionId = UUID().uuidString
let ownershipProofToken = sudoProfilesClient.getOwnershipProof(
    sudo: currentSudo, 
    audience: "sudoplatform.relay.postbox")
var postbox: Postbox? 

do {
    postbox = try await relayClient.createPostbox(
    withConnectionId: connectionId, 
    ownershipProofToken: ownershipProofToken
    isEnabled: true)
} catch {
    // Handle error;
    // If an error occurs, the error.localizedDescription can be 
    // presented to the user to notify them as to what caused the error.
}
// Store the postbox. The postbox unique identifier is required to update
// postbox status and delete the postbox when required.

```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val connectionId: String = UUID.randomUUID().toString()
var postbox: Postbox

launch {
    val ownershipProof = withContext(Dispatchers.IO) {
        app.sudoProfilesClient.getOwnershipProof(
            sudo, 
            "sudoplatform.relay.postbox")
    }
    try {
        postbox = withContext(Dispatchers.IO) {
            relayClient.createPostbox(connectionId, ownershipProofToken)
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
// Store the postbox. The postbox unique identifier is required to update
// postbox status and delete the postbox when required.
```

{% endtab %}
{% endtabs %}

## Updating a Relay Postbox <a href="#get-email-domains" id="get-email-domains"></a>

A postbox can be updated via the `updatePostbox` function. Updating a postbox consists of modifying its enabled state. No other attributes are modifiable.

The update call will return the resultant postbox on success, or throw an exception upon failure.

{% hint style="info" %}
Postboxes which are not enabled will not accept new messages.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
// postboxId: returned in the result of the createPostbox call
do {
    // if no value is provided for the optional value isEnabled, 
    // no changes will be made to the postbox
    let updatedPostbox = try await relayClient.updatePostbox(
        withPostboxId: postboxId, 
        isEnabled: false)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// postboxId: returned in the result of the createPostbox call
launch {
    try {
        val updatedPostbox = withContext(Dispatchers.IO) {
            relayClient.updatePostbox(postboxId, false)
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Deleting a Relay Postbox <a href="#get-email-domains" id="get-email-domains"></a>

A postbox can be deleted by using the `deletePostbox` function.

The deletion call will return the identifier of the deleted postbox on success, or throw an error upon failure.

{% hint style="warning" %}
By deleting the postbox, the user's postbox and all stored messages will be deleted; the postbox will no longer be usable.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
// postboxId: returned in the result of the createPostbox call
do {
    _ = try await relayClient.deletePostbox(withPostboxId: postboxId)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// postboxId: returned in the result of the createPostbox call
launch {
    try {
        withContext(Dispatchers.IO) {
            relayClient.deletePostbox(postboxId)
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Retrieving Postboxes

The `listPostboxes` function should be used to retrieve a list of postboxes owned by the current user. The list will be sorted from oldest to newest.

Since the number of postboxes may be large, this function allows pagination of the returned results. The caller may optionally specify the maximum number of postboxes to return in a single iteration. If more are available than fit in that limit, then the returned construct will include a token which can be supplied to a subsequent iteration to fetch the next page.

If no limit is supplied, the server will impose a value that it deems appropriate.

{% tabs %}
{% tab title="Swift" %}

<pre class="language-swift"><code class="lang-swift"><strong>var allPostboxes: [Postbox] = []
</strong><strong>do {
</strong>    var postboxResult = 
        try await relayClient.listPostboxes(limit: nil, nextToken: nil)
    allPostboxes.append(contentsOf: postboxes.items)
    // If you wish to handle all retrievals in a single task context
    while postboxResult.nextToken != nil {
      postboxResult = 
          try await relayClient.listPostboxes(limit: nil, nextToken: postboxResult.nextToken)
      allPostboxes.append(contentsOf: postboxes.items)    
    }
} catch {
    // Handle errors
}
</code></pre>

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
var allPostboxes = mutableListOf<Postbox>()

launch {
    try {
        var postboxesList: OutputList<Postbox> = withContext(Dispatchers.IO) {
            relayClient.listPostboxes()
        }
        allPostboxes.addAll(postboxesList.items)
        // If you wish to handle all retrievals in a single task context
        while(postboxesList.nextToken != null) {
            postboxesList = withContext(Dispatchers.IO) {
                relayClient.listPostboxes(null, postboxesList.nextToken)
            }
            allPostboxes.addAll(postboxesList.items)
        }
        
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}


# Manage Relay Messages

Allow your users to retrieve and manage messages within their postbox.

The message retrieval and management APIs allow users to access messages which have been sent to their postbox. Generally users will retrieve messages as soon after delivery as possible, and delete each message from the postbox once it has been handled.&#x20;

## Retrieving Relay Messages

Messages delivered to the user's postboxes can be retrieved using the single `listMessages` function. Messages are retrieved in order from oldest to newest.

Since the number of messages may be large, this function allows pagination of the returned results. The caller may optionally specify the maximum number of messages to return in a single iteration. If more are available than fit in that limit, then the returned construct will include a token which can be supplied to a subsequent iteration to fetch the next page.

If no limit is supplied, only a single message will be returned.

{% tabs %}
{% tab title="Swift" %}

```swift
var allMessages: [Message] = []
do {
    var messageResult = 
        try await relayClient.listMessages(limit: nil, nextToken: nil)
    allMessages.append(contentsOf: messages.items)
    // If you wish to handle all retrievals in a single task context
    while messageResult.nextToken != nil {
      messageResult = 
          try await relayClient.listMessages(limit: nil, nextToken: messageResult.nextToken)
      allMessages.append(contentsOf: messageResult.items)    
    }
} catch {
    // Handle errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
var allMessages = mutableListOf<Message>()

launch {
    try {
        var messagesList: OutputList<Message> = withContext(Dispatchers.IO) {
            relayClient.listMessages()
        }
        allMessages.addAll(messagesList.items)
        // If you wish to handle all retrievals in a single task context
        while(messagesList.nextToken != null) {
            messagesList = withContext(Dispatchers.IO) {
                relayClient.listMessages(null, messagesList.nextToken)
            }
            allMessages.addAll(messagesList.items)
        }
        
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Deleting a Relay Message

A message can be deleted by using the `deleteMessage` function.

The deletion call will return the identifier of the deleted message on success, or throw an error upon failure.

{% tabs %}
{% tab title="Swift" %}

```swift
// messageId: returned in the result of the listMessages call
do {
    _ = try await relayClient.deleteMessage(withMessageId: messageId)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// messageId: returned in the result of the listMessages call
launch {
    try {
        withContext(Dispatchers.IO) {
            relayClient.deleteMessage(messageId)
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Bulk Deleting Relay Messages

Up to 25 messages can be deleted with a single call to the `bulkDeleteMessage` function.

This call will return the identifiers of the deleted messages on success, or throw an error upon failure to delete *any* message.

{% tabs %}
{% tab title="Swift" %}

```swift
// messageIds: returned in the result of the listMessages call
do {
    _ = try await relayClient.bulkDeleteMessage(withMessageIds: messageIds)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// messageIds: returned in the result of the listMessages call
launch {
    try {
        withContext(Dispatchers.IO) {
            relayClient.bulkDeleteMessage(messageIds)
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

## Subscribing to Incoming Relay Messages

Real-time data notification is supported in the **Relay SDK**. Calling the subscription method in the SDK ensures that data relating to relay messages will be posted to the client as they are received in the relay, as long as the client is connected.

Subscribing to Relay messages is done on a per-user basis, so all messages for all postboxes for the user will be delivered to the same notification method. Clients are able to demultiplex received messages using the postbox identifier or the owning Sudo identifier of the message.

Subscriptions are only active when the application is active. If the user navigates away from the application, the subscription will be closed, and the SDK notified, and the client must reconnect when the application is reactivated.

{% hint style="danger" %}
It is important to use subscriptions sparingly as they are resource intensive. It is recommended to only use them when a specific view is open. For example, it would be appropriate for the application to subscribe when a view of a list of relay messages is in the foreground. When the user navigates from this view, the application should cancel the subscription.  &#x20;
{% endhint %}

{% tabs %}
{% tab title="Swift" %}
To subscribe to Relay messages, the client should specify a message received handler and a connection status change handler to deal with subscription status changes to ensure it is aware when the subscription becomes active or inactive. If the connection status changes to `disconnected`, the client must manually resubscribe in order to continue to receive notifications of new messages.

The input to the status change handler is the following:

```swift
public enum PlatformSubscriptionStatus: Equatable {
    case initialized
    case connecting
    case connected
    case disconnected
    case error(Error)
}
```

### Subscribing to message received notifications

```swift
// A list to hold our notified messages
private var messagesReceived: [Message] = []
// If this token is cleaned up or goes out of scope then the subscription
// will be terminated
private var subscriptionToken: SubscriptionToken?

do {
    subscriptionToken = try await self.relayClient.subscribeToMessageCreated(
        statusChangeHandler: { result in
            switch result {
            case .connected:
                // handle any implementation required
                // for sucessful subscription connection
                return
            case .disconnected, .error:
                subscriptionToken?.cancel()
                subscriptionToken = nil
            default:
                // no action required
            }
        },
        resultHandler: { result in
            switch result {
                case .success(message):
                    messagesReceived.addItem(message)
                default:
                    return
            }
        }
    )
} catch {
    // Handle error
}
```

{% hint style="warning" %}
Note that for the connection to remain open, a strong reference needs to be kept to the subscription token.
{% endhint %}

### Unsubscribing from message received notifications

To cancel a subscription, cancel the subscription Token or call the `unsubscribeAll` method to cancel all subscriptions.&#x20;

```swift
// Either
    subscriptionToken?.cancel()
// or
    relayClient.unsubscribeAll()
```

This will ensure that the subscription is cancelled and system resources are freed.
{% endtab %}

{% tab title="Kotlin" %}
To subscribe to incoming relay messages, use the `subscribeToRelayEvents` method. You must pass an identifier of the subscription and an instance of a class that implements the `MessageSubscriber` interface. This class will be notified of incoming messages and any changes in the status of the subscription. If the connection status changes to `DISCONNECTED`, the client must manually resubscribe in order to continue to receive notifications of new messages.

The valid set of subscription status values is:

```kotlin
enum class ConnectionState {
    /**
     * Connected and receiving updates.
     */
    CONNECTED,
    /**
     * Disconnected and won't receive any updates. When disconnected all subscribers will be
     * unsubscribed so the consumer must re-subscribe.
     */
    DISCONNECTED
}
```

### Subscribing to message received notifications

```kotlin
// A list of received messages
private var relayMessagesReceived = mutableListOf<Message>()

val subscriber = object : MessageSubscriber {
    override fun messageCreated(message: Message) {
        // Handle incoming message.
        relayMessagesReceived.add(message)
    }
    
    override fun connectionStatusChanged(state: Subscriber.ConnectionState) {
        // Handle changes in the state of the subscription connection
    }
}

launch {
    try {
        withContext(Dispatchers.IO) {
            relayClient.subscribeToRelayEvents(
                "unique-subscription-id",
                subscriber
            )
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

There is also an extension function that allows you to pass lambdas instead of an object that implements the `MessageSubscriber` interface.

```kotlin
// A list of received messages
private var relayMessagesReceived = mutableListOf<Message>()

launch {
    try {
        withContext(Dispatchers.IO) {
            relayClient.subscribeToRelayEvents(
                "unique-subscription-id",
                onConnectionChange = { state: Subscriber.ConnectionState ->
                    // Handle changes in the state of the subscription connection.
                },
                messageCreated = { message: Message ->  
                    // Handle incoming message.
                    relayMessagesReceived.add(message)
                }
            )
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {
        // Handle/notify user of exception
    }
}
```

### Unsubscribing from message received notifications

To cancel a subscription, use the `unsubscribeToRelayEvents` or `unsubscribeAll` method.&#x20;

```kotlin
launch {
    try {
        withContext(Dispatchers.IO) {
            relayClient.unsubscribeToRelayEvents("unique-subscription-id")
            // or
            relayClient.unsubscribeAll()
        }
    } catch (e: SudoDIRelayClient.DIRelayException) {

    }
}
```

This will ensure that the subscription is cancelled and system resources are freed.
{% endtab %}
{% endtabs %}


# Receiving Messages

Use an always-on endpoint to receive messages from peers.

The **Relay SDK** allows your users to receive messages even when their device is offline. Postboxes created in the Relay receive messages via HTTP POST to a unique endpoint. The body of the POST request is stored in the postbox. No headers or encoding information is saved.

The endpoint associated with each postbox is accessible via the return value from `createPostbox` or `listPostboxes` and should be shared with the peer wishing to send messages to the user.

The peer must then POST a message to the endpoint and it will be stored in the postbox.&#x20;

{% hint style="info" %}
Note that the message payload will be stored as provided, without any encryption. Clients should ensure that their message is encrypted appropriately before POSTing.
{% endhint %}


# SDK Releases

Additional resources for the Decentralized Identity Relay SDKs

Use the links below to learn about significant new features and fixes added in each release of the **Decentralized Identity Relay SDK** and to access the source code and sample apps.

## Decentralized Identity Relay SDK for iOS <a href="#decentralized-identity-relay-sdk-for-ios" id="decentralized-identity-relay-sdk-for-ios"></a>

* ​[SDK releases and changelog](https://github.com/sudoplatform/sudo-di-relay-ios/releases)​
* ​[SDK source code](https://github.com/sudoplatform/sudo-di-relay-ios)​
* ​[Sample app source code](https://github.com/sudoplatform/samples-ios/tree/master/di-relay)​

## Decentralized Identity Relay SDK for Android <a href="#decentralized-identity-relay-sdk-for-android" id="decentralized-identity-relay-sdk-for-android"></a>

* ​[SDK releases and changelog](https://github.com/sudoplatform/sudo-di-relay-android/releases)​
* ​[SDK source code](https://github.com/sudoplatform/sudo-di-relay-android)​
* ​[Sample app source code](https://github.com/sudoplatform/samples-android/tree/master/di-relay)​


# Edge Agent SDK

Decentralized Identity Connection, Verifiable Credential and Proof Presentation management for mobile agents.

The **Edge Agent SDK** allows your users to engage in Decentralized Identity protocols and securely manage their sensitive Decentralized Identity (DI) data. The SDK supports connection establishment (DIDComm), verifiable credential acceptance and storage, and credential presentation using both Aries RFCs and OpenID4VC protocols. With more to come in the near future.

## Key Features

|                                                                                                                                                               |                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Agent Management](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management)                                                     | Construct and run an Edge Agent to easily access Decentralized Identity flows and receive incoming messages automatically.                                                                                                   |
| [Manage Wallets](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-wallets)                                                         | Protect your users' Decentralized Identity data with encrypted and password protected wallets.                                                                                                                               |
| [Establish Connections](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/establishing-connections)                                        | Establish secure end to end encrypted connections with Decentralized Identity peers with DIDComm.                                                                                                                            |
| [Manage Connections](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-connections)                                                 | View and manage the set of secure connections with peers.                                                                                                                                                                    |
| [Connection Messaging](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/messaging)                                                        | Send, view and manage chat messages exchanged with connected peers.                                                                                                                                                          |
| [Manage DIDs](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-dids)                                                               | Create and manage Decentralized Identifiers (DIDs) owned by the Agent.                                                                                                                                                       |
| [Accept Credentials](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/accepting-new-credentials)                                          | View credentials being offered by Aries & OpenID4VC issuers, and accept them. Including W3C Verifiable Credentials (VCs), IETF SD-JWT VCs and Hyperledger Anoncreds.                                                         |
| [Manage Credentials](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-credentials)                                                 | View and manage the set of verifiable credentials owned by the agent.                                                                                                                                                        |
| [Present Credentials for Verification](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/present-credentials-for-verification)             | Respond to proof requests from Aries & OpenID4VC verifiers, allowing for owned verifiable credentials to be presented and verified. Including W3C Verifiable Presentations (VPs), IETF SD-JWT VCs and Hyperledger Anoncreds. |
| [Utilize Alternative Cryptography Providers](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/utilize-alternative-cryptography-providers) | Achieve custom use cases and higher levels of assurance in the agent by utilizing alternative cryptography providers, such as iOS/Android hardware security, or your own custom key management system.                       |

Please see the [standards and protocols](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/standards-and-protocols) section for the technical details on the range of DI feature variants that are supported by the Edge Agent.

### Next Steps

* Integrate the **Edge Agent SDK** into your [iOS](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/integrate-the-wallet-sdk#integrate-the-ios-sdk) and [Android](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/integrate-the-wallet-sdk#integrate-the-ios-sdk-1) applications


# Integrate the Edge Agent SDK

Integrate the Edge Agent SDK into your application

## Get Started <a href="#get-started" id="get-started"></a>

* [​Integrate the iOS SDK​](#integrate-the-ios-sdk)
* [​Integrate the Android SDK​](#integrate-the-ios-sdk-1)

## Integrate the iOS SDK <a href="#integrate-the-ios-sdk" id="integrate-the-ios-sdk"></a>

{% hint style="success" %}
To gain access to the iOS SDK and sample app providing a reference for integrating the SDK, please [contact us](https://sudoplatform.com/#lets-chat).
{% endhint %}

Add this line to your [Podfile](/guides/getting-started#ios-only-setup-cocoapods-in-a-project):

```
pod 'SudoDIEdgeAgent'
```

Install pod dependencies by running the following command in your project directory:

```
pod install --repo-update
```

This will update the local CocoaPods repository and install the latest version of the **Edge Agent SDK**.

{% hint style="info" %}
In order to instantiate an Agent, make sure you have followed the instructions for [Getting Started](/guides/getting-started).
{% endhint %}

## Integrate the Android SDK <a href="#integrate-the-ios-sdk" id="integrate-the-ios-sdk"></a>

{% hint style="success" %}
To gain access to the Android SDK and sample app providing a reference for integrating the SDK, please [contact us](https://sudoplatform.com/#lets-chat).
{% endhint %}

The Android SDK is compatible with Android 8 (API level 26) and above.

{% tabs %}
{% tab title="Gradle" %}
Add this line to the dependencies section of the app module `build.gradle` and synchronize your project with Android Studio.

```groovy
dependencies {
    implementation 'com.sudoplatform:sudodiedgeagent:$latest_version'
    // required transitive dep of Edge Agent SDK
    implementation("net.java.dev.jna:jna:5.14.0@aar")
}
```

{% hint style="info" %}
In order to instantiate an Agent, make sure you have followed the first page of instructions for [Getting Started](/guides/getting-started).
{% endhint %}
{% endtab %}
{% endtabs %}


# Agent Management

Manage instances of mobile agents encapsulating DI protocol engagements and wallet data

The **Edge Agent SDK** revolves around instances of `SudoDIEdgeAgent`s. Agent instances provide access to modules for accessing data or DI protocol engagements. As well as APIs for managing the agent's configuration, managing the message receiver loop, and hooks for subscribing to agent events.

## Initializing an Agent

### Configuration

Before initializing an agent, an `AgentConfiguration` object must be created to dictate some of the agent's behavior. Using the fields in `AgentConfiguration`, the following agent behavior can be configured:

#### Required:

* `networkConfiguration` controls how the agent should handle network interactions and which ledgers the agent should opt-in to. This configuration allows tweaking of general network settings (such as ledger re-try attempts), and ledger-specific configurations for the Edge Agent's supported ledger types: **Cheqd** and **Sovrin-based** (did:sov + unqualified indy DIDs). The configurations for these ledgers will be used when resolving DIDs or Anoncreds objects from those ledgers during protocols. For ledger-specific configurations, `null` can be explicitly provided to opt-out of that ledger type if it's not needed by the agent.

#### Optional:

* `storageDirectory` can be used to specify a directory location on the device where agent data will be stored. If `null` is provided (or if unprovided), a persistent data directory within the application's environment will be used.
* `peerConnectionConfiguration` is the default configuration for how the agent should present itself to peers it connects with. It includes a `label` field for a self-assigned unverified label or nickname.
* `exchangePreservationConfiguration` controls preservation of exchange records in the wallet after a protocol (connection, credential or proof presentation protocols) is complete.
* `cacheConfiguration` controls how the agent should cache different data it uses (such as immutable data it receives from the ledger it uses).
* *+ more*. The configurable parameters on the agent are growing over time. To see the full set of configurable fields, please refer to the [API Reference](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/sdk-releases).

### Builder

To initialize a `SudoDIEdgeAgent`, the associated `Builder` class can be used along with the configuration you wish to use ([see above](#configuration)).

{% tabs %}
{% tab title="Swift" %}
An `AgentConfiguration` must be set in the builder. A logger can also optionally be set.

The following example demonstrates how to build an agent using a default configuration.&#x20;

```swift
let genesisFile: URL // a URL pointing to a indy-ledger genesis file on disk

let networkConfiguration = NetworkConfiguration(
    sovConfiguration: NetworkConfiguration.Sov(
        genesisFiles: [genesisFile],
        namespace: "testnet"
    ),
    cheqdConfiguration: NetworkConfiguration.Cheqd()
)
let agentConfiguration = AgentConfiguration(networkConfiguration: networkConfiguration)
agent = try SudoDIEdgeAgentBuilder()
            .setAgentConfiguration(agentConfiguration: agentConfiguration)
            .build()
```

{% endtab %}

{% tab title="Kotlin" %}
Note that only `agentConfiguration` and `context` are **required** to be set in the Builder. A custom `logger` and `dispatcher` for coroutine methods can be passed into the builder if desired as well.

```kotlin
val appContext: Context // Android application context
val genesisFile: File // a File reference to a indy-ledger genesis file on disk

val networkConfiguration = NetworkConfiguration(
    sovConfiguration = NetworkConfiguration.Sov(
        genesisFiles = listOf(genesisFile),
        namespace = "testnet"
    ),
    cheqdConfiguration = NetworkConfiguration.Cheqd()
)

val agentConfiguration = AgentConfiguration(
    networkConfiguration = networkConfiguration
)

val agent: SudoDIEdgeAgent = SudoDIEdgeAgent.builder()
    .setContext(appContext)
    .setAgentConfiguration(agentConfiguration)
    .build()
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
This builder also notably allows for custom cryptography providers to be injected into the agent, such that the agent can utilize this provider during operations such as DID creation and presentation signing. For high assurance use cases, we also provide default cryptography providers that utilize iOS and Android Hardware security.&#x20;

Please see [Utilize Alternative Cryptography Providers](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/utilize-alternative-cryptography-providers) for guidance.
{% endhint %}

## Changing Agent Configuration

After an [agent is initialized](#initializing-an-agent), the configuration can be changed on the fly if desired. This can be achieved via the `setAgentConfiguration` API. This will allow certain behavior, such as credential auto acceptance, agent connection appearance, and more to be changed.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent // agent to change configuration for
let newConfiguration: AgentConfiguration // the new configuration to set the agent to

try agent.setAgentConfiguration(agentConfiguration: newConfiguration)
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent // agent to change configuration for
val newConfiguration: AgentConfiguration // the new configuration to set the agent to

agent.setAgentConfiguration(newConfiguration)
```

{% endtab %}
{% endtabs %}

{% hint style="danger" %}
Some configuration fields should **not** be changed whilst an agent has a wallet open. The SDK inline code documentation notes these fields. These fields include `storageDirectory` and `networkConfiguration`. To reconfigure these fields on an agent, it is recommended to `stop` the agent's run loop, close the wallet and re-initialize the agent before setting a new config for these values.
{% endhint %}

## Receiving a URL Request

As part of the protocols supported by the Edge Agent, external requests (from Issuers, Verifiers, etc) are often made to the Wallet App in the form of a URL request. For instance, a deep link to the app or a QR Code scanned by the app. The Edge Agent API `receiveUrl` can be used to process these incoming URL requests.&#x20;

The following request types are supported:

* [OpenID4VCI Credential Offers](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-10.1.1):  via the `openid-credential-offer://` URL scheme
* [OpenID4VP Authorization Requests](https://openid.net/specs/openid-4-verifiable-presentations-1_0-21.html#section-11.1.2): via the `openid4vp://` or `openid://` URL scheme
* [Out of Band DIDComm message](https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband#standard-out-of-band-message-encoding): explicitly via the `didcomm://` URL scheme, or as a fallback for when other known schemes are not caught

When a URL is successfully processed and received by the agent, the agent's data state is updated, and a corresponding event may trigger (see [#subscribe-to-events](#subscribe-to-events "mention")). After this, and depending on the type of request received, the agent may be ready to respond to the request via protocol interactions (refer to later relevant sections in this documentation such as [Establishing Connections](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/establishing-connections), [Accepting New Credentials](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/accepting-new-credentials) & [Present Credentials for Verification](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/present-credentials-for-verification)).

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let requestUrl: String // string URL received from some channel (QR Code, deep link)

do {
    try await agent.receiveUrl(url: requestUrl)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val requestUrl: String // string URL received from some channel (QR Code, deep link)

launch {
    try {
        agent.receiveUrl(requestUrl)
    } catch (e: ReceiveUrlException) {
        // handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Most URLs will only be processable if the agent has their wallet storage open before receiving. Please see the [documentation](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-wallets) on wallet management.
{% endhint %}

{% hint style="info" %}
How URL requests are captured before being passed into the  `receiveUrl` API is a responsibility of the application itself. The application may wish to register known deep link schemes for the application, or implement a QR Code scanner.

See our [sample apps](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/sdk-releases) as a reference.
{% endhint %}

## Receiving a DIDComm Message

With an instantiated `SudoDIEdgeAgent`, you are ready to begin receiving and processing DIDComm messages. The agent API `receiveMessage` can be used to receive supported Aries RFC DIDComm messages. Typically, the messages passed into `receiveMessage` are encrypted (Aries RFC 0019) and come from some communication channel with a peer (e.g. via a Relay or Mediator source). However, some non-encrypted messages are supported as well, most notably Connection invitations received through some other channel (e.g. out of band channels).

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let rawMessage: Data // bytes of a raw DIDComm message received from some channel
let metadata: ReceivedMessageMetadata // metadata of the received DIDComm message. Usually generated by the message source

do {
    try await agent.receiveMessage(
        rawMessage: rawMessage,
        metadata: metadata
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val rawMessage: ByteArray // bytes of a raw DIDComm message received from some channel 
val metadata: ReceivedMessageMetadata // metadata of the received DIDComm message. Usually generated by the message source

launch {
    try {
        agent.receiveMessage(rawMessage, metadata)
    } catch (e: ReceiveMessageException) {
        // handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Most messages will only be processable if the agent has their wallet storage open before receiving a message. Please see the [documentation](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-wallets) on wallet management.
{% endhint %}

The `receiveMessage` API allows for manual processing of messages to be achieved. If automatic message receiving and processing is desired, configuring a `MessageSource` and using the agent `run` method will be more fitting for this use case (see below).

## Running DIDComm Agent Management

In addition to [manually receiving DIDComm messages](#receiving-a-didcomm-message), the agent can be run in a loop to poll for and process messages that come in via a `MessageSource`. Information on `MessageSource`s and using the default provided implementations can be [found below](#message-sources).

Note that similar to `receiveMessage`, the agent must have a wallet open before starting the `run` loop. Additionally, only one `run` loop for an agent can be run at once.

The run loop performs any pre-checks and then begins the loop (polling the `MessageSource` in a background task). The API can be called like so:

{% tabs %}
{% tab title="Swift" %}

<pre class="language-swift"><code class="lang-swift">let agent: SudoDIEdgeAgent
let messageSource: MessageSource // some implementation of a MessageSource

<strong>do {
</strong>    try agent.run(messageSource: messageSource)
} catch {
    // handle error
}
</code></pre>

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val messageSource: MessageSource // some implementation of a MessageSource

try {
    agent.run(messageSource)
} catch (e: AgentRunException) {
    // handle exception
}
```

{% endtab %}
{% endtabs %}

After an agent is running, several APIs can be used to manage the run loop. These APIs are:

* `isRunning(): Boolean` - reports whether the agent is actively running and not attempting to stop. False indicates that the loop is stopped.
* `stop()` - triggers the agent to begin its stop sequence (finishing whatever message it was processing, if any)
* `isStopping(): Boolean` - reports whether the agent is in the stop sequence (as requested via `stop()`).

## DIDComm Message Sources

The agent's `run` loop is powered by implementations of the `MessageSource` interface. As a consumer, you can choose to implement your own message source, or use our provided implementations.

To implement your own `MessageSource`, all you need to implement is 2 methods - `getMessage(): Message?` and `finalizeMessage(id: String)`. Quite simply, your implementation of `getMessage` should attempt to fetch the next available `Message` from your message source channel, and then `finalizeMessage` should take the provided `id` (from `Message.id`) and make the necessary action to 'finalize' or remove the message from the message source). The message body returned by the `getMessage` call should encrypted or unencrypted Aries DIDComm messages.

Our provided `MessageSource`s, particularly [Relay Message Source](#relay-message-source), can be used as a reference for designing your own `MessageSource`.

### Relay Message Source

To integrate the **Edge Agent SDK** with our [Sudo DI Relay SDK](/guides/decentralized-identity/decentralized-identity/relay-sdk) , you can use the `SudoDIRelayMessageSource` implementation. The `SudoDIRelayMessageSource` can be created using the constructor:

{% tabs %}
{% tab title="Swift" %}

```swift
let relayClient: SudoDIRelayClient
let logger: Logger

let messageSource = SudoDIRelayMessageSource(relayClient: relayClient, logger: logger)

do {
    try agent.run(messageSource: messageSource)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val relayClient: SudoDIRelayClient
val logger: SudoLogger

val messageSource = SudoDIRelayMessageSource(relayClient, logger)

try {
    agent.run(messageSource)
} catch (e: AgentRunException) {
    // handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
To get a `SudoDIRelayClient` instance, please refer to the [Sudo DI Relay SDK integration documentation](/guides/decentralized-identity/decentralized-identity/relay-sdk/integrate-the-relay-sdk)
{% endhint %}

### Other Provided Message Sources

In addition to the relay message source, you may use a URL DIDComm message processing message source (`UrlMessageSource`) and a simple aggregator message source (`RoundRobinMultiMessageSource`).

The `UrlMessageSource` can be used to parse and queue DIDComm messages that are encoded in a URL (i.e. an [Out of Band 0434 Message](https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband#standard-out-of-band-message-encoding), or a [Connection Protocol 0160 Invitation](https://github.com/hyperledger/aries-rfcs/tree/main/features/0160-connection-protocol#standard-invitation-encoding)). These messages can be passed into the `queueUrlMessage` method to queue them for usage in the agent:

{% tabs %}
{% tab title="Swift" %}

```swift
let logger: Logger 
let urlEncodedMessage: String // (e.g. from scanning an invitation QR code)

let messageSource = UrlMessageSource(logger: logger)

do {
    try agent.run(messageSource: messageSource)
} catch {
    // handle error
}

do {
    try await messageSource.queueUrlMessage(url: urlEncodedMessage)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val logger: SudoLogger 
val urlEncodedMessage: String // (e.g. from scanning an invitation QR code)

val messageSource = UrlMessageSource(logger)

try {
    agent.run(messageSource)
} catch (e: AgentRunException) {
    // handle exception
}

launch {
    try {
        messageSource.queueUrlMessage(urlEncodedMessage)
    } catch (e: QueueUrlMessageException) {
        // handle exception
    }
}
```

{% endtab %}
{% endtabs %}

The `RoundRobinMultiMessageSource` can be used to aggregate together multiple message sources. As the name implies, the implementation simply polls the `MessageSource`s it has been given in a fair round-robin approach. For more use-case specific aggregations of `MessageSource`s, a custom implementation may be more suitable.

The `RoundRobinMultiMessageSource` can be used like so:

{% tabs %}
{% tab title="Swift" %}

```swift
let urlMessageSource: UrlMessageSource
let relayMessageSource: SudoDIRelayMessageSource
let logger: Logger

let multiMessageSource = RoundRobinMultiMessageSource(logger: logger)
multiMessageSource.addMessageSource(messageSource: urlMessageSource)
multiMessageSource.addMessageSource(messageSource: relayMessageSource)

do {
    try agent.run(messageSource: multiMessageSource)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val urlMessageSource: UrlMessageSource
val relayMessageSource: SudoDIRelayMessageSource
val logger: SudoLogger

val multiMessageSource = RoundRobinMultiMessageSource(logger)
multiMessageSource.addMessageSource(urlMessageSource)
multiMessageSource.addMessageSource(relayMessageSource)

try {
    // agent.run will call `getMessage` on the `multiMessageSource`, which will
    // alternate between calling the `urlMessageSource` and `relayMessageSource`
    // message sources it was provided in this example.
    agent.run(multiMessageSource)
} catch (e: AgentRunException) {
    // handle exception
}
```

{% endtab %}
{% endtabs %}

## Subscribe to Events

It is important for consuming applications to be aware of updates to the agent's protocol states in real-time, rather than polling for updates. Important protocol state updates and events include:

* A new incoming Connection establishment request, or an update to an existing Connection establishment protocol
* A new incoming Credential offer from a credential Issuer, or an update to an existing Credential exchange protocol
* A new incoming Credential Presentation request from a credential Verifier, or an update to an existing Presentation exchange protocol
* A new incoming basic message from a connection

These state changes can be subscribed to via the `subscribeToAgentEvents` API. This API takes an implementation of `AgentEventSubscriber`, which has methods that will be invoked on different event types.&#x20;

The `subscribeToAgentEvents` API can be called multiple times to attach many subscribers. Each call to the API will return a unique `String` identifier, which can be passed into `unsubscribeToAgentEvents` in order to remove the subscriber from receiving event updates. `unsubscribeAll` is an API that can also be used to quickly remove **all** subscribers from receiving event updates.

{% tabs %}
{% tab title="Swift" %}

```swift
class CustomSubscriber : AgentEventSubscriber {
    func connectionExchangeStateChanged(connectionExchange: ConnectionExchange) {
        // the following will be invoked whenever a connection protocol state change occurs.
        // ConnectionExchange will contain the updated state
        print("connection update: \(connectionExchange)")
    }
    func credentialExchangeStateChanged(credentialExchange: CredentialExchange) {
        // the following will be invoked whenever a credential issuance protocol state change occurs.
        // CredentialExchange will contain the updated state
        print("credential update: \(credentialExchange)")
    }
    func proofExchangeStateChanged(proofExchange: ProofExchange) {
        // the following will be invoked whenever a proof presentation protocol state change occurs.
        // ProofExchange will contain the updated state
        print("proof update: \(proofExchange)")
    }
    func inboundBasicMessage(basicMessage: BasicMessage.Inbound) {
        // the following will be invoked whenever a new basic message is received.
        print("new message: \(basicMessage)")
    }
    func messageProcessed(messageId: String) {
        // the following will be invoked whenever the agent run loop processes a new message.
        print("message processed: \(messageId)")
    }
}


let customSubscriber = CustomSubscriber()
let subscriberId = agent.subscribeToAgentEvents(subscriber: customSubscriber)
```

```swift
let subscriberId: String // captured from `subscribeToAgentEvents`
agent.unsubscribeToAgentEvents(subscriptionId: subscriberId)
```

```swift
agent.unsubscribeAll()
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val subscriber = object : AgentEventSubscriber {
    override fun connectionExchangeStateChanged(connectionExchange: ConnectionExchange) {
        // the following will be invoked whenever a connection protocol state change occurs.
        // ConnectionExchange will contain the updated state
        println("connection update: $connectionExchange")
    }
    override fun credentialExchangeStateChanged(credentialExchange: CredentialExchange) {
        // the following will be invoked whenever a credential issuance protocol state change occurs.
        // CredentialExchange will contain the updated state
        println("credential update: $credentialExchange")
    }
    override fun proofExchangeStateChanged(proofExchange: ProofExchange) {
        // the following will be invoked whenever a proof presentation protocol state change occurs.
        // ProofExchange will contain the updated state
        println("proof update: $proofExchange")
    }
    
    override fun inboundBasicMessage(basicMessage: BasicMessage.Inbound) {
        // the following will be invoked whenever a new basic message is received.
        println("new message: $basicMessage")
    }
}

val subscriberId = agent.subscribeToAgentEvents(subscriber)
```

```kotlin
val subscriberId: String // captured from `subscribeToAgentEvents`
agent.unsubscribeToAgentEvents(subscriberId)
```

```kotlin
agent.unsubscribeAll()
```

{% endtab %}
{% endtabs %}

## Managing Ledger Cache

As a decentralized identity agent, lots of read requests to ledgers to retrieve immutable data are performed. These read requests can be costly depending on the reception of nodes on the ledger network. To help reduce this cost, the agent features APIs to help management the caching of these ledger read requests. As mentioned [above](#configuration), the agent configuration contains a field to turn on or off caching of immutable ledger data. By default, caching is turned on.

If caching is being used, you can clear the cache at your own discretion via the `clearLedgerCache` API. This API will succeed regardless of whether any cache was cleared or not.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

do {
    try await agent.clearLedgerCache()
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

launch {
    try {
        agent.clearLedgerCache()
    } catch (e: ClearLedgerCacheException) {
        // handle exception
    }
}
```

{% endtab %}
{% endtabs %}


# Manage Wallets

Provides the abilities to allow users to create and manage their own secure wallets for agent data

An agent needs a secure persistent place to store their cryptographic keys, credentials, protocol data and more. Before using most agent APIs, a wallet must be created and opened for usage. The important wallet management functionality is highlighted below.

Note that all "wallet" functionality is found under the agent's `WalletModule` implementation (within `agent.wallet`).

## Creating a Wallet

To effectively manage a user's DI data, a wallet must be created to securely store that data.

Each wallet must be created with a unique `id` along with a secure `passphrase`. To create a new wallet on the user's device, call the `wallet.create` method with those parameters. The chosen unique `id` should be remembered by consuming apps, as it is required to [open the wallet](#opening-a-wallet).

Configurable `id`s allow for developers to create "multi-wallet" applications if desired.

The chosen passphrase is used to encrypt the wallet's data, and as such, should be a secure value. Passphrase's also cannot be changed after wallet creation. Due to these reasons, an application may wish to consider a flow such as:&#x20;

1. Randomly generate a secure permanent passphrase for the wallet, &#x20;
2. Store this passphrase in the app's secure device storage,&#x20;
3. add some "user friendly" authentication to the app to protect access to the passphrase: e.g. biometrics etc.

{% tabs %}
{% tab title="Swift" %}

<pre class="language-swift"><code class="lang-swift"><strong>let id: String // the unique id for the wallet
</strong><strong>let passphrase: String // the secure passphrase used to open the wallet
</strong><strong>let agent: SudoDIEdgeAgent // the instantiated agent
</strong><strong>
</strong><strong>let walletConfiguration = WalletConfiguration(id: id, passphrase: passphrase)
</strong>do {
    try await agent.wallet.create(walletConfiguration: walletConfiguration)
} catch {
    // handle error
}
</code></pre>

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // Unique wallet identifier
val passphrase: String // Secure passphrase
val agent: SudoDIEdgeAgent // Instantiated agent

launch {
    try {
        val walletConfig = WalletConfiguration(id, passphrase)
        agent.wallet.create(walletConfig)
    } catch (e: WalletModule.CreateException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Opening a Wallet

To access and manage the data within a wallet, it must first be opened successfully by calling `agent.open` with the correct `id` and `passphrase` used when the wallet was created. If the wallet is not successfully opened, an exception/error will be thrown.

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // the existing id of the wallet
let passphrase: String // the existing passphrase used to open the wallet
let agent: SudoDIEdgeAgent // the instantiated agent

let walletConfiguration = WalletConfiguration(id: id, passphrase: passphrase)
do {
    try await agent.wallet.open(walletConfiguration: walletConfiguration)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // Existing wallet identifier
val passphrase: String // Secure passphrase
val agent: SudoDIEdgeAgent // Instantiated agent

launch {
    try {
        val walletConfig = WalletConfiguration(id, passphrase)
        agent.wallet.open(walletConfig)
    } catch (e: WalletModule.OpenException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
An instance of `SudoDIEdgeAgent` can only have 1 wallet open at a time. To "switch wallets", close the current wallet and re-open a different one.
{% endhint %}

## Closing a Wallet

The currently open wallet can be closed by calling `wallet.close`. Closing a wallet will disallow read and write operations to the wallet until it is subsequently opened again. A wallet may be deleted once closed via the `wallet.delete` API.&#x20;

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent // the instantiated agent

do {
    try await agent.wallet.close()
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent // Instantiated agent

launch {
    try {
        agent.wallet.close()
    } catch (e: WalletModule.CloseException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Deleting a Wallet

To delete a wallet from storage on the user's device, call `wallet.delete` with the `id` and `passphrase` that was used to originally create the wallet. Deleting the wallet will erase all of the user's DI data within that wallet. The wallet must be closed before deleting it.

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // the existing id of the wallet
let passphrase: String // the existing passphrase used to open the wallet
let agent: SudoDIEdgeAgent // the instantiated agent

do {
    let walletConfiguration = WalletConfiguration(id: id, passphrase: passphrase)
    try await agent.wallet.delete(walletConfiguration: walletConfiguration)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String = // Existing wallet identifier
val passphrase: String // Secure passphrase
val agent: SudoDIEdgeAgent = // Instantiated agent

launch {
    try {
        val walletConfig = WalletConfiguration(id, passphrase)
        agent.wallet.delete(walletConfig)
    } catch (e: WalletModule.DeleteException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Check if a Wallet Exists

The existence of a wallet identified by `id` can be checked using `wallet.exists`.

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // the id of the wallet
let agent: SudoDIEdgeAgent // the instantiated agent

do {
    let exists = try await agent.wallet.exists(walletId: id)
    if exists {
        // handle wallet existing
    } else {
        // handle wallet not existing
    }
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String = // Wallet identifier
val agent: SudoDIEdgeAgent = // Instantiated agent

launch {
    try {
        val exists: Boolean = agent.wallet.exists(id)
        if (exists) {
            // Handle wallet existing
        } else {
            // Handle wallet not existing
        }
    } catch (e: WalletModule.ExistsException) {
        // Handle/notify user of exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Note that all `wallet` module operations are performed within the environment of the `storageDirectory` selected during [agent configuration](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#configuration). As such, custom behavior with changing `storageDirectory` may lead to wallets no longer "existing" according to the API (until the original `storageDirectory` is chosen again).
{% endhint %}


# Establishing Connections

Accept connection invitations, establish peer connections, and manage pending connections.

In Decentralized Identity, establishing an end-to-end encrypted communication channels with a peer is an essential step before receiving or proving credentials via DIDComm/Aries. As such, the agent's `ConnectionExchangeModule` provides all functionality needed to establish those connections with peers, and manage pending connections.

The functionality of the `ConnectionExchangeModule` is accessed via the `agent`'s fields: `agent.connections.exchange`. The functionality provided is described below.

## Receive a Connection Invitation

A `ConnectionExchange`, representing a pending connection, will be created by the agent once a [connection invitation](https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband) is received. To have the agent 'receive' an invitation message, this can be achieved in multiple ways. An invitation JSON payload message can be pre-processed and passed to the agent via the `receiveMessage` [API](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#receiving-a-message). Or, if the invitation is in its URL form (as it typically is when scanned from a QR Code), our [URL Message Source](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#other-provided-message-sources) can be used to process and receive that payload.

After an invitation is received by the agent, a `ConnectionExchange` is created in the agent's wallet and can be accessed and used by the other APIs mentioned below. If the agent is [subscribed to agent events](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#subscribe-to-events), a new `ConnectionExchange` will invoke a connection update event.

A `ConnectionExchange` created as a result of receiving an invitation will have a role of `INVITEE`.

## Create a Connection Invitation

Alternatively, the Edge Agent can *create* connection invitations to be received by other agents. The module's `createInvitation` API can be used to create connection invitations. This API takes a configuration, which has variants depending on the format of the invitation which should be created.

After calling the `createInvitation` API, a `CreatedInvitation` data structure will be returned, which contains the created invitation URL encoding, and the newly created `ConnectionExchange` in the `INVITATION` state. The ID of this `ConnectionExchange` object can be used to track updates in it's state (e.g. an incoming connection request from a peer who receives the invitation).

A `ConnectionExchange` created as a result of creating an invitation will have a role of `INVITER`.

### Creating an Out of Band Invitation

To create a pairwise single-use Aries Out of Band Connection Invitation ([RFC 0434](https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband)), the `createInvitation` API should be used with the "pairwise" variant of the `CreateInvitationConfiguration` input configuration. This configuration takes the following input:

* A `Routing` object, which is an item that the agent will use to inform the peer about how this agent should be contacted. This object notably includes a `serviceEndpoint` (a HTTP endpoint where the peer should send DIDComm messages to), and an optional list of `routingVerkeys`, which (if using a mediator) describes what layers of encryption the peer should wrap their messages in. If the agent is being used with the [Sudo DI Relay](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#relay-message-source), then a [relay postbox](/guides/decentralized-identity/decentralized-identity/relay-sdk/manage-relay-postboxes) can be used to create a `Routing` for that `Postbox` ([see below](#creating-a-connection-route-from-the-sudo-relay-sdk)). If a custom message source is being used, `Routing` will need to be manually constructed.
* `overrideLabel` - a connection label which the inviter should present to the peer as a nickname for themself. If this input is provided, it will override the globally configured label.
* `overrideBaseUrl` - the base URL for the encoded invitation URL (see[ RFC 0160](https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband)). If this input is provided, it will override the globally configured invitation base URL.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let routing: Routing
let label = "Alice"
let baseUrl = "https://example.com"
    
do {
    let createdInvitation = try await agent.connections.exchange.createInvitation(
        configuration: .pairwise(
            routing: routing,
            overrideLabel: label,
            overrideBaseUrl: baseUrl
        )
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val routing: Routing
val label = "Alice"
val baseUrl = "https://example.com"

try {
    val createdInvitation = agent.connections.exchange.createInvitation(
        CreateInvitationConfiguration.Pairwise(
            routing = routing,
            overrideLabel = label,
            overrideBaseUrl = baseUrl
        )
    )
} catch (e: ConnectionExchangeModule.CreateInvitationException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

{% hint style="danger" %}
Aries AIP1 Connection Invitations ([RFC 0160](https://github.com/hyperledger/aries-rfcs/tree/main/features/0160-connection-protocol)) have been superseded by AIP2 invitations and are now deprecated. However they can still be created using the `.LegacyPairwise` variant of the configuration used above. Deprecated APIs will be removed eventually, so please migrate to use AIP2 invitations as seen above.
{% endhint %}

## Accepting a Connection

Once a connection invitation is received (`INVITATION` state in the `INVITEE` role), or a connection request is received in response to a connection created by the Edge Agent (`REQUEST` state in the `INVITER` role) the agent has different options for how they can "accept" that connection.

The agent may choose to create a new connection ([#establish-a-new-connection](#establish-a-new-connection "mention")) or, in certain situations as an Invitee, the connection may be *reused* to avoid creating a duplicate connection with a peer ([#reusing-an-existing-connection](#reusing-an-existing-connection "mention")).

### Establish a New Connection

The agent can "accept" an exchange by creating a new connection. This will begin the next steps of the Aries protocol to establish an end-to-end encrypted channel with that peer.

{% hint style="info" %}
Note that after calling the `acceptConnection` API as an invitee, the connection is not fully established immediately, further messages between the peer and agent are sent in the background. The status updates of the establishment process can be tracked via [agent events](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#subscribe-to-events).
{% endhint %}

To accept a new connection, the `acceptConnection` API is used to with a `AcceptConnectionConfiguration.NewConnection` configuration variant. The configuration takes the following:

* A `Routing` object, which is an item that the agent will use to inform the peer about how this agent should be contacted. This object notably includes a `serviceEndpoint` (a HTTP endpoint where the peer should send DIDComm messages to), and an optional list of `routingVerkeys`, which (if using a mediator) describes what layers of encryption the peer should wrap their messages in. If the agent is being used with the [Sudo DI Relay](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#relay-message-source), then a [relay postbox](/guides/decentralized-identity/decentralized-identity/relay-sdk/manage-relay-postboxes) can be used to create a `Routing` for that `Postbox` ([see below](#creating-a-connection-route-from-the-sudo-relay-sdk)). If a custom message source is being used, `Routing` will need to be manually constructed.
* Optionally, a `PeerConnectionConfiguration` object. Which includes configuration for how the agent should present itself to the peer. Any provided configuration here will override the global setting for this specific connection.

{% tabs %}
{% tab title="Swift" %}

```swift
let connectionExchangeId: String // The identifier of the [ConnectionExchange]
let routingVerkeys: [String] // any additional routing verkeys that 
// the peer should encrypt their messages with before sending.
// In the case of the sudo relay, this is an emptyList(), whereas mediator 
// services may specify routing keys.

let routing = Routing(serviceEndpoint: "https://sample.endpoint.com/abc", routingVerkeys: routingVerkeys)

do {
    try await agent.connections.exchange.acceptConnection(
        connectionExchangeId: connectionExchangeId,
        configuration: .newConnection(
            routing: routing,
            peerConfiguration: PeerConnectionConfiguration()
        )
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val connectionExchangeId: String // The identifier of the [ConnectionExchange]
val routingVerkeys: List<String> // any additional routing verkeys that 
// the peer should encrypt their messages with before sending.
// In the case of the sudo relay, this is an emptyList(), whereas mediator 
// services may specify routing keys.

val routing = Routing("https://sample.endpoint.com/abc", routingVerkeys)

try {
    agent.connections.exchange.acceptConnection(
        connectionExchangeId,
        AcceptConnectionConfiguration.NewConnection(
            routing, 
            PeerConnectionConfiguration()
        )
    )
} catch (e: ConnectionExchangeModule.AcceptConnectionException) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

### Reusing an Existing Connection

If a `ConnectionExchange` is in the invitation state as an invitee (recipient of an invitation), the agent SDK will determine if there are already established connections with the inviter. If there are, then the `reusableConnectionIds` field of the `ConnectionExchange` will be populated with a list of established `Connection` IDs (see [Manage Connections](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-connections)) that were detected to originate from the same Inviter.

When a `ConnectionExchange` is populated with `reusableConnectionIds`, then the desired ID from this list may be used to attempt a "connection reuse". Doing so will request that the inviter use the desired original connection instead of trying to establish a new connection, the exchange now enters the "reuse request" state. Once/if the peer responds, the exchange will complete; successfully re-using the connection. If the peer does not respond, a new connection can be established with this exchange instead ([#establish-a-new-connection](#establish-a-new-connection "mention")).

To accept a connection via reuse, the `acceptConnection` API is used to with a `AcceptConnectionConfiguration.ReuseExistingConnection` configuration variant. The configuration takes the following:

* `connectionId`, the ID of the `Connection` to request reuse from

{% tabs %}
{% tab title="Swift" %}

```swift
let connectionExchangeId: String // The identifier of the [ConnectionExchange]
let connectionId: String // the identifier of the [Connection] to request reuse

do {
    try await agent.connections.exchange.acceptConnection(
        connectionExchangeId: connectionExchangeId,
        configuration: .reuseExistingConnection(connectionId: connectionId)
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val connectionExchangeId: String // The identifier of the [ConnectionExchange]
val connectionId: String // the identifier of the [Connection] to request reuse

try {
    agent.connections.exchange.acceptConnection(
        connectionExchangeId,
        AcceptConnectionConfiguration.ReuseExistingConnection(connectionId)
    )
} catch (e: ConnectionExchangeModule.AcceptConnectionException) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

### Creating a Connection Route from the Sudo Relay SDK

When integrating with the [Sudo Relay SDK](/guides/decentralized-identity/decentralized-identity/relay-sdk) (i.e. via the [relay message source](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#relay-message-source)), the `Routing` for the `acceptConnection` API can be easily created with the `routingFromPostbox` helper API on the relay message source.

{% tabs %}
{% tab title="Swift" %}

```swift
let connectionExchangeId: String // The identifier of the [ConnectionExchange]
let postbox: Postbox // A new postbox created by a sudo relay client.

let relayRouting = SudoDIRelayMessageSource.routingFromPostbox(postbox: postbox)

do {
    try await agent.connections.exchange.acceptConnection(
        connectionExchangeId: connectionExchangeId,
        configuration: .newConnection(routing: relayRouting)
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val connectionExchangeId: String // The identifier of the [ConnectionExchange]
val postbox: Postbox // A new postbox created by a sudo relay client.

val relayRouting = SudoDIRelayMessageSource.routingFromPostbox(postbox)

launch {
    try {
        agent.connections.exchange.acceptConnection(
            connectionExchangeId,
            AcceptConnectionConfiguration.NewConnection(relayRouting)
        )
    } catch (e: ConnectionExchangeModule.AcceptConnectionException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Please refer to the [Sudo Relay SDK](/guides/decentralized-identity/decentralized-identity/relay-sdk) documentation for more information about creating Postboxes and more.
{% endhint %}

{% hint style="warning" %}
To avoid any chance of peers correlating different connections to a specific relay endpoint, it is recommended that a new `Postbox` (and therefore new `Postbox.serviceEndpoint` and corresponding `Routing`) is created per connection.
{% endhint %}

## Get a Pending Connection by ID

As mentioned above, a pending connection is represented by `ConnectionExchange` objects. To retrieve the current state of a specific `ConnectionExchange` in the agent's wallet, the `getById` API can be used. If a connection exchange cannot be found by the given ID, then `null` is returned:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // id of the ConnectionExchange

do {
    let connectionExchange = try await agent.connections.exchange.getById(connectionExchangeId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the connection exchange to get ([ConnectionExchange.connectionExchangeId])

launch {
    try {
        val connectionExchange = agent.connections.exchange.getById(id)
    } catch (e: ConnectionExchangeModule.GetException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Delete a Pending Connection by ID

Similarly, a `ConnectionExchange` in the wallet can be easily deleted via the `deleteById` API:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // id of the ConnectionExchange

do {
    try await agent.connections.exchange.deleteById(connectionExchangeId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the connection exchange to delete ([ConnectionExchange.connectionExchangeId])

launch {
    try {
        agent.connections.exchange.deleteById(id)
    } catch (e: ConnectionExchangeModule.DeleteException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Updating the Metadata of a Pending Connection

`ConnectionExchange` objects contain some metadata that can be controlled by SDK consumers, allowing custom information to be attached to each `ConnectionExchange`, and allowing custom [listing functionality](#listing-pending-connections) to be leveraged.

Each `ConnectionExchange` contains a list of `RecordTag` (`ConnectionExchange.tags`) attached to it, where a `RecordTag` is simply a name-value pair stored with the record. By default, some tags are attached to a new `ConnectionExchange`, this includes:

* tag-name: `~started_timestamp`
* tag-value: *The UNIX epoch seconds which this connection began establishment*

The tags on a `ConnectionExchange` can be replaced or updated by using the `updateConnectionExchange` API, and providing a new set to update. This will replace whatever the current set of tags is:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the connection exchange to update

// add a 'category' of 'work' to this connection, and a 'priority' of '1'
let update = ConnectionExchangeUpdate(tags: [
    RecordTag(name: "category", value: "work"),
    RecordTag(name: "~priority", value: "1")
])

do {
    try await agent.connections.exchange.updateConnectionExchange(connectionExchangeId: id, connectionExchangeUpdate: update)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the connection exchange to update ([ConnectionExchange.connectionExchangeId])

// add a 'category' of 'work' to this connection, and a 'priority' of '1'
val update = ConnectionExchangeUpdate(
    tags = listOf(
         RecordTag("category", "work"),
         RecordTag("~priority", "1")   
    )
)

launch {
    try {
        agent.connections.exchange.updateConnectionExchange(id, update)
    } catch (e: ConnectionExchangeModule.UpdateException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Like most data in the wallet, `RecordTag` will be stored encrypted. **Unless**, the tag name is prefixed with `~`, then the tag value will be stored unencrypted. Storing a tag value as unencrypted will allow some additional listing queries to be performed ([see below](#listing-pending-connections)).
{% endhint %}

## Listing Pending Connections

To list all pending connections in the agent's wallet, the `listAll` API can be used:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let conns = try await agent.connections.exchange.listAll(options: nil)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val conns: List<ConnectionExchange> = agent.connections.exchange.listAll(null)
    } catch (e: ConnectionExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

### Filtered Listing

More complicated `ConnectionExchange` list queries can also be achieved by utilizing the `ListConnectionExchangeFilters`.

These filters allow for the list of `ConnectionExchange` to be filtered by their `state`, `tags`, or both together.

Filtering by `state` can be achieved as follows:

{% tabs %}
{% tab title="Swift" %}

```swift
// get all the pending connections which are waiting for their invitation to be accepted
let filters = ListConnectionExchangeFilters(state: ConnectionExchangeState.invitation)
let options = ListConnectionExchangeOptions(filters: filters)
do {
    let conns = try await agent.connections.exchange.listAll(options: options)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// get all the pending connections which are waiting for their invitation to be accepted
val filters = ListConnectionExchangeFilters(state = ConnectionExchangeState.Invitation)
val options = ListConnectionExchangeOptions(filters)
        
launch {
    try {
        val connectionsPendingInvitationAcceptance = agent.connections.exchange.listAll(options)
    } catch (e: ConnectionExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

To filter by `tags` applied to the `ConnectionExchange` (i.e. applied [via the update API](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/establishing-connections#updating-the-metadata-of-a-pending-connection)), the `tagFilter` field of `ListConnectionExchangeFilters` should be used. This field takes a `String` in compliance with a [Wallet Query Language (WQL)](https://hyperledger-indy.readthedocs.io/projects/sdk/en/latest/docs/design/011-wallet-query-language/README.html) Query.

Continuing from the example in the [Update section](#updating-the-metadata-of-a-pending-connection):&#x20;

{% tabs %}
{% tab title="Swift" %}

```swift
// WQL Query, filter for 'category' == 'work'
let wqlQuery = "{ \"category\": \"work\" }"
let filters = ListConnectionExchangeFilters(tagFilter: wqlQuery)
let options = ListConnectionExchangeOptions(filters: filters)
do {
    let pendingWorkConnections = try await agent.connections.exchange.listAll(options: options)
} catch {
    // handle error
}
```

```swift
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
let wqlQuery = "{ \"~priority\": { \"$lt\": \"2\" } }"
let filters = ListConnectionExchangeFilters(tagFilter: wqlQuery)
let options = ListConnectionExchangeOptions(filters: filters)

do {
    let highPriorityPendingConnections = try await agent.connections.exchange.listAll(options: options)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// WQL Query, filter for 'category' == 'work'
val wqlQuery = """{ "category": "work" }"""
val filters = ListConnectionExchangeFilters(tagFilter = wqlQuery)
val options = ListConnectionExchangeOptions(filters)

launch {
    try {
        val pendingWorkConnections = agent.connections.exchange.listAll(options)
    } catch (e: ConnectionExchangeModule.ListException) {
        // Handle exception
    }
}
```

```kotlin
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
val wqlQuery = """{ "~priority": { "${"$"}lt": "2" } }"""
val filters = ListConnectionExchangeFilters(tagFilter = wqlQuery)
val options = ListConnectionExchangeOptions(filters)

launch {
    try {
        val highPriorityPendingConnections = agent.connections.exchange.listAll(options)
    } catch (e: ConnectionExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}


# Manage Connections

Manage peer connections that the agent has established

Connections are the gateway to DIDComm interactions with peers, whether that be receiving a credential, sending a credential proof, or more. After a connection has been [established with a peer](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/establishing-connections), the established connection will appear under the agent's `ConnectionsModule` where they are ready to be used and managed.

The functionality of the `ConnectionsModule` is accessed via the `agent`'s fields: `agent.connections`. The functionality provided is described below.

## Get a Connection by ID

Established connections are represented by `Connection` objects. To retrieve the current state of a specific `Connection` in the agent's wallet, the `getById` API can be used. If a connection cannot be found by the given identifier, then `null` is returned:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the connection to get ([Connection.connectionId])

do {
    let connection = try await agent.connections.getById(connectionId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the connection to get ([Connection.connectionId])

launch {
    try {
        val connection = agent.connections.getById(id)
    } catch (e: ConnectionsModule.GetException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Delete a Connection by ID

Similarly, a `Connection` in the wallet can be easily deleted via the `deleteById` API:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the connection to delete ([Connection.connectionId])

do {
    try await agent.connections.deleteById(connectionId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the connection to delete ([Connection.connectionId])

launch {
    try {
        agent.connections.deleteById(id)
    } catch (e: ConnectionsModule.DeleteException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Updating the Metadata of a Connection

`Connection` objects contain metadata that can be controlled by SDK consumers, allowing custom information to be attached to each `Connection` . It also allows custom [listing functionality](#listing-connections) to be leveraged.

Each `Connection` contains a list of `RecordTag` (`Connection.tags`) attached to it, where a `RecordTag` is simply a name-value pair stored with the record. By default, some tags are attached to a new `Connection`. These include:

* tag-name: `~created_timestamp` tag-value: *The UNIX epoch seconds which this connection was established*

The tags on a `Connection` can be replaced or updated by using the `updateConnection` API, and providing a new set to update. This will replace the current set of tags. An example follows:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the connection to update

// add a 'category' of 'work' to this connection, and a 'priority' of '1'
let update = ConnectionUpdate(tags: [
    RecordTag(name: "category", value: "work"),
    RecordTag(name: "~priority", value: "1")
])

do {
    try await agent.connections.updateConnection(connectionId: id, connectionUpdate: update)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the connection to update ([Connection.connectionId])

// add a 'category' of 'work' to this connection, and a 'priority' of '1'
val update = ConnectionUpdate(
    tags = listOf(
         RecordTag("category", "work"),
         RecordTag("~priority", "1")   
    )
)

launch {
    try {
        agent.connections.updateConnection(id, update)
    } catch (e: ConnectionsModule.UpdateException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Like most data in the wallet, `RecordTag` will be stored encrypted. **Unless**, the tag name is prefixed with `~`, then the tag value will be stored unencrypted. Storing a tag value as unencrypted will allow some additional listing queries to be performed ([see below](#listing-connections)).
{% endhint %}

## Listing Connections

To list all connections in the agent's wallet, the `listAll` API can be used:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let conns = try await agent.connections.listAll(options: nil)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val conns: List<Connection> = agent.connections.listAll(null)
    } catch (e: ConnectionsModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

### Filtered Listing

More complicated `Connection` list queries can also be achieved by utilizing the `ListConnectionsFilters`.

To filter by `tags` applied to the `Connection` (i.e. applied [via the update API](#updating-the-metadata-of-a-connection)), the `tagFilter` field of `ListConnectionsFilters` should be used. This field takes a `String` in compliance with a [Wallet Query Langauge (WQL)](https://hyperledger-indy.readthedocs.io/projects/sdk/en/latest/docs/design/011-wallet-query-language/README.html) Query.

Continuing from the example in the [Update section](#updating-the-metadata-of-a-connection):&#x20;

{% tabs %}
{% tab title="Swift" %}

```swift
// WQL Query, filter for 'category' == 'work'
let wqlQuery = "{ \"category\": \"work\" }"
let filters = ListConnectionsFilters(tagFilter: wqlQuery)
let options = ListConnectionsOptions(filters: filters)
do {
    let workConnections = try await agent.connections.listAll(options: options)
} catch {
    // handle error
}
```

```swift
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
let wqlQuery = "{ \"~priority\": { \"$lt\": \"2\" } }"
let filters = ListConnectionsFilters(tagFilter: wqlQuery)
let options = ListConnectionsOptions(filters: filters)

do {
    let highPriorityConnections = try await agent.connections.listAll(options: options)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// WQL Query, filter for 'category' == 'work'
val wqlQuery = """{ "category": "work" }"""
val filters = ListConnectionsFilters(tagFilter = wqlQuery)
val options = ListConnectionsOptions(filters)

launch {
    try {
        val workConnections = agent.connections.listAll(options)
    } catch (e: ConnectionsModule.ListException) {
        // Handle exception
    }
}
```

```kotlin
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
val wqlQuery = """{ "~priority": { "${"$"}lt": "2" } }"""
val filters = ListConnectionsFilters(tagFilter = wqlQuery)
val options = ListConnectionsOptions(filters)

launch {
    try {
        val highPriorityConnections = agent.connections.listAll(options)
    } catch (e: ConnectionsModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}


# Messaging

Receive, send, and manage text messages exchanged between the Edge Agent connections.

After [establishing a DIDComm connection](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/establishing-connections) with a peer, one of many supported interactions is the ability to exchange text messages back and forth along the E2EE channel. The Edge Agent supports Aries Basic Message Protocol ([RFC 0095](https://github.com/hyperledger/aries-rfcs/blob/main/features/0095-basic-message/README.md)) to accomplish this.&#x20;

The `MessagingModule` contains the methods to receive, send and manage these messages. It is accessed via the `agent`'s fields: `agent.connections.messaging`.

## Subscribe to Inbound Messages

As described in the [Agent Management](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#subscribe-to-events) section, subscription to new incoming Basic Messages is supported via the `subscribeToAgentEvents` API. The subscriber will be invoked once for each new message, and will include the inbound message data, such as the `content`, the `receivedTime` and the ID of the `Connection` who sent it.

{% tabs %}
{% tab title="Swift" %}

```swift
class MessageSubscriber : AgentEventSubscriber {
    func inboundBasicMessage(basicMessage: BasicMessage.Inbound) {
        // the following will be invoked whenever a new basic message is received.
        print("new message: \(basicMessage)")
    }
    
    func connectionExchangeStateChanged(connectionExchange: ConnectionExchange) {}
    func credentialExchangeStateChanged(credentialExchange: CredentialExchange) {}
    func proofExchangeStateChanged(proofExchange: ProofExchange) {}
    func messageProcessed(messageId: String) {}
}

let customSubscriber = MessageSubscriber()
let subscriberId = agent.subscribeToAgentEvents(subscriber: customSubscriber)
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val subscriber = object : AgentEventSubscriber {
    override fun inboundBasicMessage(basicMessage: BasicMessage.Inbound) {
        // the following will be invoked whenever a new basic message is received.
        println("new message: $basicMessage")
    }
}

val subscriberId = agent.subscribeToAgentEvents(subscriber)
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
See [Agent Management](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#subscribe-to-events) for more subscription management details.
{% endhint %}

## Send Messages

To send a basic message to a connection, the `sendBasicMessage` API can simply be used. On success, the outbound basic message that was sent to the peer will be returned.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let connectionId: String // the identifier of the [Connection] to message

do {
    let sentMessage = try await agent.connections.messaging.sendBasicMessage(
        connectionId: connectionId,
        content: "hello world"
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val connectionId: String // the identifier of the [Connection] to message

try {
    val sentMessage = agent.connections.messaging.sendBasicMessage(
        connectionId = connectionId,
        content = "hello world"
    )
} catch (e: MessagingModule.SendBasicMessageException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

## Query Messages

The SDK features one API for listing basic messages in a variety of ways: `listBasicMessages`.  The API is designed to support common peer to peer messaging use cases.&#x20;

The method takes a `ListBasicMessagesOptions` object as input as a way to configure pagination, sorting and filtering. It has the following fields within it:

* `filters` - A `ListBasicMessagesFilter` object with configurations for how results should be filtered
  * `filters.connectionId` - if provided, the resulting list of messages will only contain messages from the given connection
  * `filters.limitMessagesPerConnection` - if provided, the resulting list of messages will limit the number of messages from a single connection. Useful for "conversation list views": "list recent messages, limited to 1 per connection".
* `paging` - A common `Paging` object, configuring the limit of  items to fetch, and the current cursor position (if any)
* `sorting` - A `ListBasicMessagesSorting` object specifying how the results should be sorted (pre filtering and paging). Currently chronological sorting (ascending and descending) is supported

The value returned is a `PageResult<BasicMessage>`, which contains a list of `items` and a string cursor, `nextToken`, if there is more items to fetch. This `nextToken` can be passed into the `paging` item when performing the next `listBasicMessages` operation to fetch the next page of items from where the cursor was up to.

### List All Recent Messages

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let pageSize: UInt = 10

do {
    let page1 = try await agent.connections.messaging.listBasicMessages(
        options: .init(
            paging: .init(limit: pageSize)
        )
    )
    let pageItems1 = page1.items
    
    // fetch next page if nextToken is not nil
    let page2 = try await agent.connections.messaging.listBasicMessages(
        options: .init(
            paging: .init(limit: pageSize, nextToken: page1.nextToken)
        )
    )
    let pageItems2 = page2.items
    
    // continue..
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val pageSize = 10u

try {
    val page1 = agent.connections.messaging.listBasicMessages(
        options = ListBasicMessagesOptions(
            paging = Paging(pageSize)
        )
    )
    val pageItems1 = page1.items

    // fetch next page if nextToken is not null
    val page2 = agent.connections.messaging.listBasicMessages(
        options = ListBasicMessagesOptions(
            paging = Paging(pageSize, page1.nextToken)
        )
    )
    val pageItems2 = page2.items
    
    // continue..
} catch (e: MessagingModule.ListBasicMessagesException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

### List Oldest Messages

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let pageSize: UInt = 10

do {
    let page = try await agent.connections.messaging.listBasicMessages(
        options: .init(
            paging: .init(limit: pageSize),
            sorting: .chronological(direction: .ascending)
        )
    )
    let pageItems = page.items
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val pageSize = 10u

try {
    val page = agent.connections.messaging.listBasicMessages(
        options = ListBasicMessagesOptions(
            paging = Paging(pageSize),
            sorting = ListBasicMessagesSorting.Chronological(SortDirection.ASCENDING)
        )
    )
    val pageItems = page.items
} catch (e: MessagingModule.ListBasicMessagesException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

### List Recent Messages with a Connection

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let connectionId: String
let pageSize: UInt = 10

do {
    let page = try await agent.connections.messaging.listBasicMessages(
        options: .init(
            filters: .init(connectionId: connectionId),
            paging: .init(limit: pageSize)
        )
    )
    let pageItems = page.items
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val connectionId: String
val pageSize = 10u

try {
    val page = agent.connections.messaging.listBasicMessages(
        options = ListBasicMessagesOptions(
            filters = ListBasicMessagesFilter(connectionId = connectionId),
            paging = Paging(pageSize),
        )
    )
    val pageItems = page.items
} catch (e: MessagingModule.ListBasicMessagesException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

### List Most Recent Connection Conversations

To support use cases like a "recent conversations" list view, the list API may be used like so:

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let pageSize: UInt = 10

do {
    let page = try await agent.connections.messaging.listBasicMessages(
        options: .init(
            filters: .init(limitMessagesPerConnection: 1),
            paging: .init(limit: pageSize)
        )
    )
    let pageItems = page.items
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val pageSize = 10u

try {
    val page = agent.connections.messaging.listBasicMessages(
        options = ListBasicMessagesOptions(
            filters = ListBasicMessagesFilter(limitMessagesPerConnection = 1u),
            paging = Paging(pageSize),
        )
    )
    val pageItems = page.items
} catch (e: MessagingModule.ListBasicMessagesException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

## Delete Messages

### Delete All Messages with a Connection

All basic messages to/from a connection can be deleted from storage with the `deleteBasicMessagesForConnection` API.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let connectionId: String

do {
    try await agent.connections.messaging.deleteBasicMessagesForConnection(
        connectionId: connectionId
    )
} catch {
// handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val connectionId: String

try {
    agent.connections.messaging.deleteBasicMessagesForConnection(
        connectionId = connectionId
    )
} catch (e: MessagingModule.DeleteBasicMessagesException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

### Delete Basic Messages by IDs

Alternatively, a known set of message IDs (`BasicMessage.id`) can be passed into the `deleteBasicMessages` API to delete them.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let messageIds: [String]

do {
    try await agent.connections.messaging.deleteBasicMessages(
        ids: messageIds
    )
} catch {
// handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val messageIds: List<String>

try {
    agent.connections.messaging.deleteBasicMessages(
        ids = messageIds
    )
} catch (e: MessagingModule.DeleteBasicMessagesException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

## Opt Out of Message Storage

Some Edge Agent SDK consumers may wish to handle Basic Message storage themselves, for instance, to add their own indexes and data schema. Or even to just avoid message storage all together. If that is the case, the SDKs managed storage of Basic Messages can be opt out of with the `AgentConfiguration`'s `messagingConfiguration.storeBasicMessages` field. By default this is `true`, setting the configuration field to `false` will disable storage of Basic Messages.&#x20;

See [Agent Management](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management) for details on managing the agent's configuration.

{% hint style="info" %}
With storage disabled, Basic Messages will still be processed and will still invoke the subscription (see [#subscribe-to-inbound-messages](#subscribe-to-inbound-messages "mention")), but nothing will be put into the agent's database. Meaning the above APIs (list, delete) will have limited functionality.
{% endhint %}


# Manage DIDs

Manage the DIDs which are owned by the Edge Agent for usage in DI protocols

Many DI protocols require the agent to provide the DID which they want to use for the interaction. Some Edge Agent interactions automatically handle this usage of DIDs in protocols. However in some protocols it is more suitable for the SDK consumer to be explicit about the DID/s they want to use. When this is the case, the SDK's `DidsModule` can be used to manage the DIDs owned by the agent.

The functionality of the `DidsModule` is accessed via the `agent`'s fields: `agent.dids`.

## Create DIDs

To create and store DIDs with the Edge Agent SDK, the `createDid` API can be used. This API takes a `CreateDidOptions` type, which has different data variants for the different DID methods supported. On success, this API returns a `DidInformation` data object with the details of the DID that was created.

For instance, to create a did:key DID:

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

do {
    let createdDid = try await agent.dids.createDid(
        options: .didKey(keyType: .ed25519)
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

try {
    val createdDid = agent.dids.createDid(
        options = CreateDidOptions.DidKey(
            keyType = DidKeyType.ED25519
        )
    )
} catch (e: DidsModule.CreateDidException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
In the above example, the DID's cryptographic key will be generated using the agent's internal software-based key management. For hardware-based key management and other custom options, see [Utilize Alternative Cryptography Providers](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/utilize-alternative-cryptography-providers#create-a-did-backed-by-a-custom-cryptography-provider).
{% endhint %}

{% hint style="info" %}
See the [Standards and Protocols](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/standards-and-protocols) section for a list of DID methods supported for creation by the Edge Agent SDK.
{% endhint %}

## Get full DID Information by ID

To retrieve the current full `DidInformation` of a specific DID owned by the agent, the `getById` API can be used. If a DID cannot be found by the given identifier, then `null` is returned:

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

do {
    let did = try await agent.dids.getById(did: "did:example:123")
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

try {
    val did = agent.dids.getById("did:example:123")
} catch (e: DidsModule.GetException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

## Delete a DID

Similarly, a DID in the wallet can be easily deleted via the `deleteById` API:

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

do {
    try await agent.dids.deleteById(did: "did:example:123")
} catch {
// handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

try {
    agent.dids.deleteById("did:example:123")
} catch (e: DidsModule.DeleteException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

## Updating the Storage Metadata of a DID

`DidInformation` objects contain storage metadata (local only) that can be controlled by SDK consumers, allowing custom information to be attached to each `DidInformation` . It also allows custom [listing functionality](#listing-dids) to be leveraged.

Each `DidInformation` contains a list of `RecordTag` (`DidInformation.tags`) attached to it, where a `RecordTag` is simply a name-value pair stored with the record.&#x20;

The tags on a `DidInformation` can be replaced or updated by using the `updateDid` API, and providing a new set to update. This will replace the current set of tags. An example follows:

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

// add a 'category' of 'work' to this DID, and a 'priority' of '1'
let update = DidUpdate(tags: [
    RecordTag(name: "category", value: "work"),
    RecordTag(name: "~priority", value: "1")
])

do {
    try await agent.dids.updateDid(
        did: "did:example:123",
        didUpdate: update
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

// add a 'category' of 'work' to this DID, and a 'priority' of '1'
val update = DidUpdate(
    tags = listOf(
        RecordTag("category", "work"),
        RecordTag("~priority", "1")
    )
)

try {
    agent.dids.updateDid("did:example:123", update)
} catch (e: DidsModule.UpdateException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Like most data in the wallet, `RecordTag` will be stored encrypted. **Unless**, the tag name is prefixed with `~`, then the tag value will be stored unencrypted. Storing a tag value as unencrypted will allow some additional listing queries to be performed ([see below](#listing-dids)).
{% endhint %}

## Listing DIDs

To list all DIDs owned by the agent, the `listAll` API can be used:

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

do {
    let dids: [DidInformation] = try await agent.dids.listAll(
        options: nil
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

try {
    val dids: List<DidInformation> = agent.dids.listAll()
} catch (e: DidsModule.ListException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

### Filtered Listing

More complicated `DidInformation` list queries can also be achieved by utilizing the `ListDidsFilters` filter. These filters allow for filtering by DID Method, Key Types and record tags.

To filter by `tags` applied to the `DidInformation` (i.e. applied [via the update API](#updating-the-storage-metadata-of-a-did)), the `tagFilter` field of `ListDidsFilters` should be used. This field takes a `String` in compliance with a [Wallet Query Language (WQL)](https://hyperledger-indy.readthedocs.io/projects/sdk/en/latest/docs/design/011-wallet-query-language/README.html) Query.

Continuing from the example in the [Update section](#updating-the-storage-metadata-of-a-did):&#x20;

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

// WQL Query, filter for 'category' == 'work'
let wqlQuery = "{ \"category\": \"work\" }"
let filters = ListDidsFilters(tagFilter: wqlQuery)
let options = ListDidsOptions(filters: filters)

do {
    let workDids = try await agent.dids.listAll(options: options)
} catch {
    // handle error
}
```

```swift
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
let wqlQuery = "{ \"~priority\": { \"$lt\": \"2\" } }"
let filters = ListDidsFilters(tagFilter: wqlQuery)
let options = ListDidsOptions(filters: filters)

do {
    let highPriorityDids = try await agent.dids.listAll(options: options)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

// WQL Query, filter for 'category' == 'work'
val wqlQuery = """{ "category": "work" }"""
val filters = ListDidsFilters(tagFilter = wqlQuery)
val options = ListDidsOptions(filters)

try {
    val workDids = agent.dids.listAll(options)
} catch (e: DidsModule.ListException) {
    // Handle exception
}
```

```kotlin
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
val wqlQuery = """{ "~priority": { "${"$"}lt": "2" } }"""
val filters = ListDidsFilters(tagFilter = wqlQuery)
val options = ListDidsOptions(filters)

try {
    val highPriorityDids = agent.dids.listAll(options)
} catch (e: DidsModule.ListException) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}


# Accepting New Credentials

Accept and store incoming verifiable credentials from Issuers, and manage credentials mid-issuance.

The `CredentialExchangeModule` provides the functionality needed to easily walk through the Decentralized Identity protocol flow for accepting and storing W3C, IETF SD-JWT and Anoncreds credentials. This module contains support for both **Aries/DIDComm**-based credential exchanges (e.g., after [establishing an E2EE DIDComm connection with an issuer](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/establishing-connections)), and for **OpenID4VC**-based credential exchanges.

The functionality of the `CredentialExchangeModule` is accessed via the `agent`'s field  `agent.credentials.exchange`. The functionality provided is described below.

## Receive a Credential Offer

A `CredentialExchange` (a pending credential) data object is created in the agent's wallet once a *credential offer* is received. A `CredentialExchange` can represent either an Aries or OpenID4VC protocol, this distinction is made via the *variant* of the `CredentialExchange` (`CredentialExchange.Aries` or `CredentialExchange.OpenId4Vc`). Each of these variants contain further data specific to the protocol being used.

How a `CredentialExchange` is received by the Edge Agent depends on the protocol being used (see below).

However, regardless of the protocol, after an offer is processed by the agent, a `CredentialExchange` is created in the agent's wallet and can be accessed and used by the APIs mentioned below. If the agent is subscribed to agent events ([see here](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#subscribe-to-events)), a new `CredentialExchange` will invoke a credential update event.

### Receiving an Aries Credential Offer

The agent receives [Aries credential offers](https://github.com/hyperledger/aries-rfcs/tree/main/features/0036-issue-credential) as encrypted DIDComm messages, either through the `receiveMessage` [API](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#receiving-a-message), or via the the `MessageSource` of the agent's [run loop](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#message-sources). The credential offer must come from an [established connection](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-connections), and the `CredentialExchange.Aries` will contain information about which connection the offer came from.

Additionally, `CredentialExchange.Aries` will show the metadata of what credential is being exchanged, and what the attributes/values of that credential are. These attributes/values can be reviewed by consumers before [accepting the offer](#accepting-a-credential-offer).

#### Aries Credential Exchange Formats

The Edge Agent supports multiple formats of credential exchanges (see [Standards and Protocols](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/standards-and-protocols)). To represent this, the `CredentialExchange.Aries` contains a `formatData` field. This field is a `AriesCredentialExchangeFormatData` data structure which has variants for the different types of supported exchange formats. Currently these format variants include:

* `.Anoncred`: contains the details of a credential exchange being performed with Aries [Anoncreds](https://github.com/hyperledger/aries-rfcs/tree/main/features/0771-anoncreds-attachments) or[ Indy Attachments](https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments). The data format is used to exchange Anoncreds credentials.
* `.AriesLdProof`: contains the details of a credential exchange being performed with [Aries LD Proof Attachments](https://github.com/hyperledger/aries-rfcs/tree/main/features/0593-json-ld-cred-attach). The data format is used to exchange W3C VCs (LD Proof) credentials.

### Receiving an OpenID4VC Credential Offer

The agent is capable of receiving [OpenID4VC credential offers](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-endpoint) via the `openid-credential-offer://` URL scheme through the `receiveUrl` [API](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#receiving-a-url-request). The `CredentialExchange.OpenId4Vc` will contain details about:

* the URL of the OpenID4VC Issuer
* the credential configurations that are being offered by the issuer (i.e. what credential types and formats can be accepted)
* the required authorization that the agent must fulfil before accepting the offer and receiving a credential
* any specified display details for the credential configurations and the issuer (e.g., logo, colours).

These details can be reviewed by consumers before [authorizing](#authorize-a-credential-exchange-openid4vc-only) and [accepting](#accepting-a-credential-offer) the exchange.

#### OpenID4VC Credential Configuration Formats

Like [Aries exchanges](#aries-credential-exchange-formats), OpenID4VC exchanges support different formats of credentials to be exchanged. This is done via the offered configurations: `OpenId4VcCredentialConfiguration`. Each configuration is returned as a data structure with variants for the different types of supported formats it may represent. Currently these format variants include:

* `.SdJwtVc`: (dc+sd-jwt) contains the details of an IETF SD-JWT VC being offered.
* `.VcSdJwt`: (vc+sd-jwt) contains the details of a W3C VCDM 2.0 signed by SD-JWT being offered.
* `LdpVc`: (ldp\_vc) contains the details of a W3C VCDM 1.1/2.0 signed with a Linked Data Proof being offered.

## Authorize a Credential Exchange (OpenID4VC Only)

Unique to OpenID4VC, after the credential offer is [received](#receiving-an-openid4vc-credential-offer), the `CredentialExchange.OpenId4Vc` is initially in the `UNAUTHORIZED` state, and it must be advanced into the `AUTHORIZED` state before the offer is ready to [accept](#accepting-a-credential-offer). The  `authorizeExchange` API (via `.openId4Vc`) is used for this purpose.&#x20;

This API takes the identifier of the `CredentialExchange.OpenId4Vc` to authorize, and a `configuration` for how the agent should authorize with the issuer.

The `OpenId4VcAuthorizeConfiguration` configuration has variants for the different kinds of authorization methods. The chosen method should match the permitted authorization variants specified by the issuer in `CredentialExchange.OpenId4Vc.requiredAuthorization`.

&#x20;The `OpenId4VcAuthorizeConfiguration` variants include:

* `.WithPreAuthorization` - authorization using [pre-authorization flow](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-pre-authorized-code-flow). In this flow, the authorization is already baked-in to the offer that was originally received, the only extra data that may be required for submission in this configuration is `txCode` - a PIN-like string which the issuer may require to be provided. This input is required if the `txCodeRequired` field of `RequiredAuthorization.PreAuthorized` is non-null.
* `.WithAuthorizationCode` — authorization using the authorization code flow. This flow requires the user to complete an OAuth login in a browser. See Authorization Code Flow below.

### Pre-Authorization

Used when `requiredAuthorization` is `RequiredAuthorization.PreAuthorized`.

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to authorize ([CredentialExchange.credentialExchangeId])
let txCode: String? // collected TX-Code (e.g. from InputField) if a txCode was required

do {
    try await agent.credentials.exchange.openId4Vc.authorizeExchange(
        credentialExchangeId: id,
        configuration: .withPreAuthorization(txCode: txCode)
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to authorize ([CredentialExchange.credentialExchangeId])
val txCode: String? // collected TX-Code (e.g. from InputField) if a txCode was required

try {
    val config = OpenId4VcAuthorizeConfiguration.WithPreAuthorization(txCode)
    agent.credentials.exchange.openId4Vc.authorizeExchange(id, config)
} catch (e: CredentialExchangeModule.OpenId4Vc.AuthorizeExchangeException) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

After successfully calling this API, the `CredentialExchange.OpenId4Vc` will enter the `AUTHORIZED` state, ready to accept the offer.

#### Authorization Code Flow

Used when `requiredAuthorization` is `RequiredAuthorization.AuthorizationCode`. This is a two-step process:

1. **Initiate authorization** — Call `initiateAuthorization` to obtain an authorization URL that the user must visit in a browser to complete OAuth login.
2. **Complete authorization** — After the user completes login, the browser redirects back to your app with the authorization response. Pass the full redirect URI to `authorizeExchange` with `.WithAuthorizationCode`.

**Step 1: Initiate Authorization**

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to authorize
let clientId: String // Your OAuth client_id known with the issuer
let redirectUri: String // Your app's redirect URI (e.g. "myapp://auth-callback")

do {
    let result = try await agent.credentials.exchange.openId4Vc.initiateAuthorization(
        credentialExchangeId: id,
        configuration: .init(clientId: clientId, redirectUri: redirectUri)
    )
    // result.authorizationUrl — open this in a browser (e.g. ASWebAuthenticationSession)
    // result.expiresIn — optional deadline (seconds) for the user to complete auth
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to authorize
val clientId: String // Your OAuth client_id known with the issuer
val redirectUri: String // Your app's redirect URI (e.g. "myapp://auth-callback")

try {
    val config = InitiateAuthorizationConfiguration(
        clientId = clientId,
        redirectUri = redirectUri,
    )
    val result = agent.credentials.exchange.openId4Vc.initiateAuthorization(id, config)
    // result.authorizationUrl — open this in a browser (e.g. Auth Tab / Custom Tab)
    // result.expiresIn — optional deadline (seconds) for the user to complete auth
} catch (e: CredentialExchangeModule.OpenId4Vc.InitiateAuthorizationException) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

The consumer is responsible for opening `authorizationUrl` in a browser and capturing the redirect callback. Refer to our sample apps for an example of this flow.

**Step 2: Complete Authorization**

Once the browser redirects back to your app (e.g. `myapp://auth-callback?code=xyz&state=abc`), pass the full redirect URI response to `authorizeExchange`:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange
let redirectUriResponse: String // The full callback URI from the browser redirect

do {
    try await agent.credentials.exchange.openId4Vc.authorizeExchange(
        credentialExchangeId: id,
        configuration: .withAuthorizationCode(redirectUriResponse: redirectUriResponse)
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange
val redirectUriResponse: String // The full callback URI from the browser redirect

try {
    val config = OpenId4VcAuthorizeConfiguration.WithAuthorizationCode(redirectUriResponse)
    agent.credentials.exchange.openId4Vc.authorizeExchange(id, config)
} catch (e: CredentialExchangeModule.OpenId4Vc.AuthorizeExchangeException) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

After successfully calling this API, the `CredentialExchange.OpenId4Vc` will enter the `AUTHORIZED` state, ready to accept the offer.

## Accepting a Credential Offer

With a `CredentialExchange.Aries` in the `OFFER` state or a `CredentialExchange.OpenId4Vc` in the `AUTHORIZED` state, the agent can "accept" that offer, which will request a credential from the issuer. Both Aries and OpenID4VC exchanges use the same `acceptOffer` API for this action, however the behaviour and provided configuration is different depending on the protocol.

The `acceptOffer` API takes the identifier of the `CredentialExchange` to accept, and a `configuration` for how the agent should behave in accepting the exchange. This configuration has variants for Aries and OpenID4VC. The difference in behaviour and configuration is described below.

### Accepting an Aries Credential Offer

To accept an Aries exchange,  the `AcceptCredentialOfferConfiguration.Aries` configuration should be used. It allows the following behaviour to be adjusted via it's parameters:

* `autoStoreCredential` - a flag for whether the credential should be automatically stored in the wallet once the Issuer responds with the issued credential. If false, then the consumer is responsible for calling the [Store Credential API](#store-a-credential) once the `CredentialExchange` receives the "issued" state.
* `storeCredentialConfiguration` - a configuration for how the credential should be auto-stored (if `autoStoreCredential` is configured as true). Allows for custom behaviour, such as automatically applying certain `tags` to the stored `Credential`.
* `formatSpecificConfiguration` - configuration which is specific to the Aries credential exchange format. This configuration has variants for the different formats which maps to the `CredentialExchange.Aries.formatData` field. These variants include:
  * `.AriesLdProofVc` - configuration which can be applied when the credential exchange format is an Aries LD Proof

For Aries exchanges, after calling `acceptOffer` the credential has not yet been issued nor stored. The Issuer will still have to approve the request and respond again with the issued credential data. The status updates of the issuance process can be tracked via [agent events](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#subscribe-to-events).

#### Accepting an Aries Anoncred Credential

To accept an Anoncred credential offer using the `.Anoncred` credential exchange format, no additional `formatSpecificConfiguration` is required. The following can be done:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to accept ([CredentialExchange.credentialExchangeId])

do {
    let configuration = AcceptCredentialOfferConfiguration.aries(.init())
    try await agent.credentials.exchange.acceptOffer(
        credentialExchangeId: id,
        configuration: configuration
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to accept ([CredentialExchange.credentialExchangeId])

try {
    val configuration = AcceptCredentialOfferConfiguration.Aries()
    agent.credentials.exchange.acceptOffer(id, configuration)
} catch (e: CredentialExchangeModule.AcceptOfferException) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

#### Accept an Aries W3C Credential

To accept a W3C credential offer using the `.AriesLdProof` credential exchange format, the `formatSpecificConfiguration` field can be provided with additional configuration. This additional configuration includes:

* `overrideCredentialSubjectId` - if provided, request that the issuer uses this string as the Credential Subject ID of the W3C VC. This mechanism can be used to inform the issuer of the holder DID the Edge Agent wants to VC to be bound to. See [Manage DIDs](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-dids).

To accept a W3C and request that the Edge Agent's DID be bound to the credential, the following can be done:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to accept ([CredentialExchange.credentialExchangeId])

do {
    // alternatively, an existing DID may be reused.
    let newDid = try await agent.dids.createDid(
        options: .didKey(keyType: .ed25519)
    )
    
    let configuration = AcceptCredentialOfferConfiguration.aries(.init(
        formatSpecificConfiguration: .ariesLdProofVc(
            overrideCredentialSubjectId: newDid.did
        )
    ))
    try await agent.credentials.exchange.acceptOffer(
        credentialExchangeId: id,
        configuration: configuration
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to accept ([CredentialExchange.credentialExchangeId])

try {
    // alternatively, an existing DID may be reused.
    val newDid = agent.dids.createDid(
        CreateDidOptions.DidKey(DidKeyType.ED25519)
    )
    
    val configuration = AcceptCredentialOfferConfiguration.Aries(
        formatSpecificConfiguration = AcceptCredentialOfferFormatSpecificConfiguration.AriesLdProofVc(
            overrideCredentialSubjectId = newDid.did
        )
    )
    agent.credentials.exchange.acceptOffer(id, configuration)
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
In order to prove holder ownership of a W3C credential (i.e. when presenting proof), it must be bound to one of the Edge Agent's DIDs. Hence the importance of this configuration.
{% endhint %}

### Accepting an OpenID4VC Credential Offer

To accept an OpenID4VC exchange,  the `AcceptCredentialOfferConfiguration.OpenId4Vc` configuration should be used. It allows the following behaviour to be adjusted via it's parameters:

* `autoStoreCredential` - see [#accepting-an-aries-credential-offer](#accepting-an-aries-credential-offer "mention")
* `storeCredentialConfiguration` - see [#accepting-an-aries-credential-offer](#accepting-an-aries-credential-offer "mention")
* `credentialConfigurationId` - the key of the credential configuration to request be issued. This must be one of the offered keys from `.offeredCredentialConfigurations`
* `holderBinding` - `OpenId4VcBindingMethod` configuration which specifies how the issuer should bind the requested credential to the agent. The selected binding method should match what was permitted by the issuer in the `allowedBindingMethods` of the configuration being requested. Examples with supported binding methods are given below.

For OpenID4VC exchanges, the credential is issued and ready to store and calling `acceptOffer` (or stored if auto-store is configured). The issued credentials can be previewed via the `issuedCredentialPreviews` field of the exchange prior to storage.

#### Accepting an OpenID4VC Exchange with DID Binding

The `.WithDid` `OpenId4VcBindingMethod` configuration can be used to request that the issued credential be bound to a particular DID owned by the Edge Agent (See [Manage DIDs](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-dids)). This method should be used with a DID that has a DID method and key type that satisfies what is specified in the credential configuration's `allowedBindingMethods` sub-fields.

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to accept ([CredentialExchange.credentialExchangeId])
let credentialConfigurationId: String // The credential configuration ID/key to request

do {
    // alternatively, an existing DID may be reused.
    // assumes did:key DID method and P256 key type supported
    let newDid = try await agent.dids.createDid(
        options: .didKey(keyType: .p256)
    )
    
    let configuration = AcceptCredentialOfferConfiguration.openId4Vc(.init(
        credentialConfigurationId: credentialConfigurationId,
        holderBinding: .withDid(did: newDid.did)
    ))
    try await agent.credentials.exchange.acceptOffer(
        credentialExchangeId: id,
        configuration: configuration
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to accept ([CredentialExchange.credentialExchangeId])
val credentialConfigurationId: String // The credential configuration ID/key to request

try {
    // alternatively, an existing DID may be reused.
    // assumes did:key DID method and P256 key type supported
    val newDid = agent.dids.createDid(
        CreateDidOptions.DidKey(DidKeyType.P256)
    )
    
    val configuration = AcceptCredentialOfferConfiguration.OpenId4Vc(
        credentialConfigurationId = configurationId,
        holderBinding = OpenId4VcBindingMethod.WithDid(holderDid)
    )
    agent.credentials.exchange.acceptOffer(id, configuration)
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

## Store a Credential

When a `CredentialExchange` reaches the `ISSUED` state, and has not been configured to [auto-store](#accepting-a-credential-offer), then the agent must call the `storeCredential` API before the `Credential` is stored in the wallet, completing the protocol.&#x20;

The API takes the identifier of the `CredentialExchange` to store the credential of, and a `configuration` for how the agent should behave when storing the credential.

The `StoreCredentialConfiguration` data object can be used to overwrite `tags` that the `Credential` will be stored with. It could also be left as `null` to leave the `tags` as their default.

The API will also return the new `Credential` that was stored as a result of the API call. This new credential will also now be available in the agent's [Credential management module](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-credentials).

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to store ([CredentialExchange.credentialExchangeId])

do {
    let configuration = StoreCredentialConfiguration()
    let newCredential: Credential = try await agent.credentials.exchange.storeCredential(
        credentialExchangeId: id,
        configuration: configuration
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to store ([CredentialExchange.credentialExchangeId])

launch {
    try {
        val configuration = StoreCredentialConfiguration()
        val newCredential: Credential = agent.credentials.exchange.storeCredential(id, configuration)
    } catch (e: CredentialExchangeModule.StoreCredentialException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
There is no need to call this API if the agent is configured to auto-store credentials.
{% endhint %}

## Get a Pending Credential by ID

As mentioned above, a credential pending issuance is represented by `CredentialExchange` objects. To retrieve the current state of a specific `CredentialExchange` in the agent's wallet, the `getById` API can be used. If a credential exchange cannot be found by the given identifier, then `null` is returned:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to get ([CredentialExchange.credentialExchangeId])

do {
    let credentialExchange = try await agent.credentials.exchange.getById(credentialExchangeId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to get ([CredentialExchange.credentialExchangeId])

launch {
    try {
        val credentialExchange: CredentialExchange? = agent.credentials.exchange.getById(id)
    } catch (e: CredentialExchangeModule.GetException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Delete a Pending Credential by ID

Similarly, a `CredentialExchange` in the wallet can be easily deleted via the `deleteById` API:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the cred exchange to delete ([CredentialExchange.credentialExchangeId])

do {
    try await agent.credentials.exchange.deleteById(credentialExchangeId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to delete ([CredentialExchange.credentialExchangeId])

launch {
    try {
        agent.credentials.exchange.deleteById(id)
    } catch (e: CredentialExchangeModule.DeleteException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Updating the Metadata of a Pending Credential

`CredentialExchange` objects contain some metadata that can be controlled by SDK consumers, allowing custom information to be attached to each `CredentialExchange`, and allowing custom [listing functionality](#listing-pending-credentials) to be leveraged.

Each `CredentialExchange` contains a list of `RecordTag` (`CredentialExchange.tags`) attached to it, where a `RecordTag` is simply a name-value pair stored with the record. By default, some tags are attached to a new `CredentialExchange`, this includes:

* tag-name: `~started_timestamp`, tag-value: *The UNIX epoch seconds which this credential began exchange*

The tags on a `CredentialExchange` can be replaced or updated by using the `updateCredentialExchange` API, and providing a new set to update. This will replace whatever the current set of tags is:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the credential exchange to update

// add a 'category' of 'work' to this credential, and a 'priority' of '1'
let update = CredentialExchangeUpdate(tags: [
    RecordTag(name: "category", value: "work"),
    RecordTag(name: "~priority", value: "1")
])

do {
    try await agent.credentials.exchange.updateCredentialExchange(credentialExchangeId: id, credentialExchangeUpdate: update)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the cred exchange to update ([CredentialExchange.credentialExchangeId])

// add a 'category' of 'work' to this credential, and a 'priority' of '1'
val update = CredentialExchangeUpdate(
    tags = listOf(
         RecordTag("category", "work"),
         RecordTag("~priority", "1")   
    )
)

launch {
    try {
        agent.credentials.exchange.updateCredentialExchange(id, update)
    } catch (e: CredentialExchangeModule.UpdateException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Like most data in the wallet, `RecordTag` will be stored encrypted. **Unless**, the tag name is prefixed with `~`, then the tag value will be stored unencrypted. Storing a tag value as unencrypted will allow some additional listing queries to be performed ([see below](#listing-pending-credentials)).
{% endhint %}

## Listing Pending Credentials

To list all pending credentials in the agent's wallet, the `listAll` API can be used:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let credentialExchanges = try await agent.credentials.exchange.listAll(options: nil)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val credentialExchanges: List<CredentialExchange> = agent.credentials.exchange.listAll(null)
    } catch (e: CredentialExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

### Filtered Listing

More complicated `CredentialExchange` list queries can also be achieved by utilizing the `ListCredentialExchangeFilters`.

These filters allow for a list of `CredentialExchange` to be filtered by their `state`, `tags`, or both together.

Filtering by `state` can be achieved as follows.

{% tabs %}
{% tab title="Swift" %}

```swift
// get all the pending credentials which are waiting for their offer to be accepted
let filters = ListCredentialExchangeFilters(state: CredentialExchangeState.offer)
let options = ListCredentialExchangeOptions(filters: filters)
do {
    let credentialExchanges = try await agent.credentials.exchange.listAll(options: options)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// get all the pending credentials which are waiting for their offer to be accepted
val filters = ListCredentialExchangeFilters(state = CredentialExchangeState.Offer)
val options = ListCredentialExchangeOptions(filters)
        
launch {
    try {
        val credentialsPendingOfferAcceptance = agent.credentials.exchange.listAll(options)
    } catch (e: CredentialExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

To filter by `tags` applied to the `CredentialExchange` (i.e. applied via[ the update API](#updating-the-metadata-of-a-pending-credential)), the `tagFilter` field of `ListCredentialExchangeFilters` should be used. This field takes a `String` in compliance with a [Wallet Query Language (WQL) Query](https://hyperledger-indy.readthedocs.io/projects/sdk/en/latest/docs/design/011-wallet-query-language/README.html).

Continuing from the example in the [Update API section](#updating-the-metadata-of-a-pending-credential):&#x20;

{% tabs %}
{% tab title="Swift" %}

```swift
// WQL Query, filter for 'category' == 'work'
let wqlQuery = "{ \"category\": \"work\" }"
let filters = ListCredentialExchangeFilters(tagFilter: wqlQuery)
let options = ListCredentialExchangeOptions(filters: filters)
do {
    let pendingWorkCredentials = try await agent.credentials.exchange.listAll(options: options)
} catch {
    // handle error
}
```

```swift
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
let wqlQuery = "{ \"~priority\": { \"$lt\": \"2\" } }"
let filters = ListCredentialExchangeFilters(tagFilter: wqlQuery)
let options = ListCredentialExchangeOptions(filters: filters)

do {
    let highPriorityPendingCredentials = try await agent.credentials.exchange.listAll(options: options)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// WQL Query, filter for 'category' == 'work'
val wqlQuery = """{ "category": "work" }"""
val filters = ListCredentialExchangeFilters(tagFilter = wqlQuery)
val options = ListCredentialExchangeOptions(filters)

launch {
    try {
        val pendingWorkCredentials = agent.credentials.exchange.listAll(options)
    } catch (e: CredentialExchangeModule.ListException) {
        // Handle exception
    }
}
```

```kotlin
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
val wqlQuery = """{ "~priority": { "${"$"}lt": "2" } }"""
val filters = ListCredentialExchangeFilters(tagFilter = wqlQuery)
val options = ListCredentialExchangeOptions(filters)

launch {
    try {
        val highPriorityPendingCredentials = agent.credentials.exchange.listAll(options)
    } catch (e: CredentialExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}


# Manage Credentials

Manage the set of verifiable credentials received and owned by the agent

After [accepting and storing a credential](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/accepting-new-credentials), credentials will be available for management under the agent's `CredentialsModule`. Credentials available in this module are ready to be used in Decentralized Identity interactions, such as [when presenting credential proofs](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/present-credentials-for-verification). The `CredentialsModule` provides users with an easy way to fetch, edit and delete credentials in the agent's wallet.

The functionality of the `CredentialsModule` is accessed via the `agent`'s fields: `agent.credentials`. The functionality provided is described below.

## Get a Credential by ID

Received verifiable credentials are represented by `Credential` objects. To retrieve the current state of a specific `Credential` in the agent's wallet, the `getById` API can be used. If a credential cannot be found by the given identifier, then `null` is returned:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the credential to get ([Credential.credentialId])

do {
    let credential = try await agent.credentials.getById(credentialId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the credential to get ([Credential.credentialId])

launch {
    try {
        val credential = agent.credentials.getById(id)
    } catch (e: CredentialsModule.GetException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

### Credential Formats

Similar to [Credential Exchanges](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/accepting-new-credentials#credential-formats), the `Credential` object can represent different credential formats. `Credential` contains a `formatData` field, which is a `CredentialFormatData` data structure with variants for the different supported formats. Currently these format variants include:

* `AnoncredV1`: Contains details of the [Anoncreds Credential](https://hyperledger.github.io/anoncreds-spec/) this `Credential` represents. Includes the credential attributes and Anoncreds specific metadata.
* `W3C`: Contains details of the [W3C Verifiable Credential](https://www.w3.org/TR/vc-data-model/) this `Credential` represents. Includes the complete `W3cCredential` data structure as defined by the [W3C specification](https://www.w3.org/TR/vc-data-model/).
* `SdJwtVc`: Contains details of the [IETF SD-JWT VC](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/) this `Credential` represents. Includes the complete raw SD-JWT and processed claims and metadata of the SD-JWT.

## Delete a Credential by ID

Similarly, a `Credential` in the wallet can be easily deleted via the `deleteById` API:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the credential to delete ([Credential.credentialId])

do {
    try await agent.credentials.deleteById(credentialId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the credential to delete ([Credential.credentialId])

launch {
    try {
        agent.credentials.deleteById(id)
    } catch (e: CredentialsModule.DeleteException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Updating the Metadata of a Credential

`Credential`s contain metadata that can be controlled by SDK consumers, allowing custom information to be attached to each `Credential`, and allowing custom [listing functionality](#listing-pending-credentials) to be leveraged.

Each `Credential` contains a list of `RecordTag` (`Credential.tags`) attached to it, where a `RecordTag` is simply a name-value pair stored with the record. By default, some tags are attached to a new `Credential`, this includes:

* tag-name: `~created_timestamp`, tag-value: *The UNIX epoch seconds which this credential was stored*

The tags on a `Credential` can be replaced or updated by using the `updateCredential` API, and providing a new set to update. This will replace whatever the current set of tags is:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the credential to update ([Credential.credentialId])

// add a 'category' of 'work' to this credential, and a 'priority' of '1'
let update = CredentialUpdate(tags: [
    RecordTag(name: "category", value: "work"),
    RecordTag(name: "~priority", value: "1")
])

do {
    try await agent.credentials.updateCredential(credentialId: id, credentialUpdate: update)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the credential to update ([Credential.credentialId])

// add a 'category' of 'work' to this credential, and a 'priority' of '1'
val update = CredentialUpdate(
    tags = listOf(
         RecordTag("category", "work"),
         RecordTag("~priority", "1")   
    )
)

launch {
    try {
        agent.credentials.updateCredential(id, update)
    } catch (e: CredentialsModule.UpdateException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Like most data in the wallet, `RecordTag` will be stored encrypted. **Unless**, the tag name is prefixed with `~`, then the tag value will be stored unencrypted. Storing a tag value as unencrypted will allow some additional listing queries to be performed ([see below](#listing-pending-credentials)).
{% endhint %}

## Listing Pending Credentials

To list all credentials in the agent's wallet, the `listAll` API can be used:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let creds = try await agent.credentials.listAll(options: nil)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val creds: List<Credential> = agent.credentials.listAll(null)
    } catch (e: CredentialsModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

### Filtered Listing

More complicated `Credential` list queries can also be achieved by utilizing the `ListCredentialsFilters`.

To filter by `tags` applied to the `Credential` (i.e. applied [via the update API](#updating-the-metadata-of-a-credential)), the `tagFilter` field of `ListCredentialsFilters` should be used. This field takes a `String` in compliance with a [Wallet Query Langauge (WQL)](https://hyperledger-indy.readthedocs.io/projects/sdk/en/latest/docs/design/011-wallet-query-language/README.html) Query.

Continuing from the example in the [Update section](#updating-the-metadata-of-a-credential):&#x20;

{% tabs %}
{% tab title="Swift" %}

```swift
// WQL Query, filter for 'category' == 'work'
let wqlQuery = "{ \"category\": \"work\" }"
let filters = ListCredentialsFilters(tagFilter: wqlQuery)
let options = ListCredentialsOptions(filters: filters)
do {
    let workCredentials = try await agent.credentials.listAll(options: options)
} catch {
    // handle error
}
```

```swift
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
let wqlQuery = "{ \"~priority\": { \"$lt\": \"2\" } }"
let filters = ListCredentialsFilters(tagFilter: wqlQuery)
let options = ListCredentialsOptions(filters: filters)

do {
    let highPriorityCredentials = try await agent.credentials.listAll(options: options)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// WQL Query, filter for 'category' == 'work'
val wqlQuery = """{ "category": "work" }"""
val filters = ListCredentialsFilters(tagFilter = wqlQuery)
val options = ListCredentialsOptions(filters)

launch {
    try {
        val workCredentials = agent.credentials.listAll(options)
    } catch (e: CredentialsModule.ListException) {
        // Handle exception
    }
}
```

```kotlin
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
val wqlQuery = """{ "~priority": { "${"$"}lt": "2" } }"""
val filters = ListCredentialsFilters(tagFilter = wqlQuery)
val options = ListCredentialsOptions(filters)

launch {
    try {
        val highPriorityCredentials = agent.credentials.listAll(options)
    } catch (e: CredentialsModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}


# Present Credentials for Verification

Use the agent's stored credentials to present a cryptographic proof to a Verifier

The `ProofExchangeModule` provides the functionality needed to easily walk through the Decentralized Identity protocol flow for handling credential presentation requests and responding with the desired credentials & proofs. This module contains support for both **Aries/DIDComm**-based proof exchanges (e.g., after [establishing an E2EE DIDComm connection with a verifier](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/establishing-connections)), and for **OpenID4VC**-based proof exchanges.

The functionality of the `ProofExchangeModule` is accessed via the `agent`'s fields: `agent.proofs.exchange`. The functionality provided is described below.

## Receive a Presentation Request

A `ProofExchange` (a pending presentation / proof in exchange) is created in the agent's wallet once a *presentation request* is received. Like `CredentialExchange` (see [Accepting New Credentials](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/accepting-new-credentials)), a `ProofExchange` can represent either an Aries or OpenID4VC protocol, this distinction is made via the *variant* of the `ProofExchange` (`ProofExchange.Aries` or `ProofExchange.OpenId4Vc`). Each of these variants contain further data specific to the protocol being used.

How a `ProofExchange` is initially received by the Edge Agent depends on the protocol being used (see below).

However regardless of the protocol, after a request is processed by the agent, a `ProofExchange` is created in the agent's wallet and can be accessed and used by the APIs mentioned below. If the agent is subscribed to agent events ([see here](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#subscribe-to-events)), a new `ProofExchange` will invoke a proof update event.

### Receiving an Aries Presentation Request

The agent receives [Aries presentation requests](https://github.com/hyperledger/aries-rfcs/blob/main/features/0454-present-proof-v2/README.md#request-presentation) as encrypted DIDComm messages, either through the `receiveMessage` [API](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#receiving-a-message), or via the `MessageSource` of the agent's [run loop](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#message-sources). The presentation request must come from an [established connection](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-connections), and the `ProofExchange.Aries` will contain information about which connection the request came from.

#### Aries Proof Exchange Formats

Similar to [Credential Exchanges](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/accepting-new-credentials#credential-formats), the `ProofExchange.Aries` object can represent different Aries proof exchange formats (see [Standards and Protocols](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/standards-and-protocols)). `ProofExchange.Aries` contains a `formatData` field, which is a `AriesProofExchangeFormatData` data structure with variants for the different supported formats. Currently these format variants include:

* `.Anoncred`: contains the details of an Aries presentation exchange being performed with Aries [Anoncreds](https://github.com/hyperledger/aries-rfcs/tree/main/features/0771-anoncreds-attachments) or[ Indy Attachments](https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments). This format is used to exchange Anoncreds presentation requests & presentations. When the Edge Agent receives a ProofExchange request of this format, the SDK can select and present it's AnoncredV1 `Credential`s in response.
* `.Dif`: contains the details of an Aries presentation exchange being performed with a [DIF Presentation Exchange](https://github.com/hyperledger/aries-rfcs/tree/main/features/0510-dif-pres-exch-attach). When the Edge Agent receives a ProofExchange request of this format, the SDK can select and present it's suitable `Credential`s in response.

The inner contents of these formatData objects particularly contains important details about what presentation items (requirements) which are being requested. In the case of Anoncreds, this may include groups of attributes and/or predicates that the verifier wishes to be presented (each with their own requirements). Or in the case of DIF, it will be a [Presentation Definition](https://identity.foundation/presentation-exchange/spec/v1.0.0/#presentation-definition), including several Input Descriptors that the verifier wishes to be presented (each with their own requirements).&#x20;

In all cases, the inner data is structured in line with the relevant specifications. SDK consumers should make themselves familiar with these standard formats to understand how the presentation request data should be processed and/or displayed to the end application user.

{% hint style="success" %}
Check out our [sample apps](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/sdk-releases) for a reference on how these data formats can be processed in an application.
{% endhint %}

### Receiving an OpenID4VC Presentation Request

The agent is capable of receiving [OpenID4VP requests](https://openid.net/specs/openid-4-verifiable-presentations-1_0-21.html#name-authorization-request) via the `openid://` and `openid4vp://` URL scheme through the `receiveUrl` [API](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#receiving-a-url-request). The `ProofExchange.OpenId4Vc` will importantly contain details of; the verifier's ID (e.g. their DID), and the `presentationRequest`.&#x20;

The `presentationRequest` contains the full [OID4VP DCQL Query](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l) describing the details of what the verifier wishes to be presented.

## Finding Credentials for a Presentation

After receiving a `ProofExchange` in the `REQUEST` state, the consumer should query the agent to discover which credentials the agent has that will satisfy each item.&#x20;

The `retrieveCredentialsForProofRequest` API, can be used for this. This API returns a `RetrievedPresentationCredentials` data structure to represent this.

`RetrievedPresentationCredentials` has variants depending on the `ProofExchange` presentation format that is being requested (DCQL, DIF or Anoncred/Indy). Generally, the inner data of each variant maps the identifiers of the requested items to the IDs of `Credential`s that are suitable for presenting the requested item.

<table><thead><tr><th width="142">Protocol</th><th width="106">Format</th><th>RetrievedPresentationCredentials</th></tr></thead><tbody><tr><td>Aries</td><td>Anoncred</td><td>Anoncred</td></tr><tr><td>Aries</td><td>DIF (v1)</td><td>DIF</td></tr><tr><td>OpenID4VC</td><td>DCQL</td><td>DCQL</td></tr></tbody></table>

### Credentials for an Anoncreds Presentation

When the `retrieveCredentialsForProofRequest` API is used on an `.Anoncred` formatted `ProofExchange.Aries`, it will return a `RetrievedPresentationCredentials.Anoncred` data object. This object contains:

* `credentialsForRequestedAttributes`: a map of requested attribute group referents/IDs, to the list of `Credential` IDs which may be used for presenting the attribute group. Attribute group IDs correspond to the referents in the `AnoncredProofRequestInfo` object of `formatData`.
* `credentialsForRequestedPredicates`: similar to above, this contains a map of requested predicate referents/IDs, to the list of `Credential` IDs which may be used for presenting the predicate.
* `selfAttestableAttributes`: a list of attribute referents/IDs which were requested for self-attestation. Unlike `credentialsForRequestedAttributes`, these requested attributes are to be self-attested by the agent; meaning any string value can be presented for these attributes. Attribute IDs correspond to the referents in the `AnoncredProofRequestInfo` object of `formatData`.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let proofExchange: ProofExchange.Aries

do {
    guard case .anoncred(let anoncredsProofRequest) = proofExchange.formatData else {
        return
    }
    
    let suitableCreds = try await agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchangeId: proofExchange.proofExchangeId
    )
    guard case .anoncred(
        let credentialsForRequestedAttributes,
        let credentialsForRequestedPredicates,
        let selfAttestableAttributes
    ) = suitableCreds else {
        return
    }

    credentialsForRequestedAttributes.forEach { groupIdentifier, creds in
        let groupInfo = anoncredsProofRequest.requestedAttributes[groupIdentifier]!
        print("Attribute group \(groupInfo) can be presented with the following creds: \(creds)")
    }
    
    credentialsForRequestedPredicates.forEach { predicateId, creds in
        let predicateInfo = anoncredsProofRequest.requestedPredicates[predicateId]!
        print("predicate \(predicateInfo) can be presented with the following creds: \(creds)")
    }
    
    selfAttestableAttributes.forEach { attributeIdentifier in 
        let info = anoncredsProofRequest.requestedAttributes[attributeIdentifier]!
        print("Attribute \(info) should be self attested")
    }
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val proofExchange: ProofExchange.Aries

try {
    val anoncredData = proofExchange.formatData as? ProofExchangeFormatData.Anoncred ?: return
    val anoncredsProofRequest = anoncredData.proofRequest

    val suitableCreds = agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchange.proofExchangeId
    ) as RetrievedPresentationCredentials.Anoncred

    suitableCreds.credentialsForRequestedAttributes.forEach { (groupIdentifier, creds) ->
        val groupInfo = anoncredsProofRequest.requestedAttributes[groupIdentifier]!!
        println(
            "Attribute group '$groupInfo' can be presented with the following creds: $creds"
        )
    }
    suitableCreds.credentialsForRequestedPredicates.forEach { (predicateId, creds) ->
        val predicateInfo = anoncredsProofRequest.requestedPredicates[predicateId]!!
        println(
            "Predicate '$predicateInfo' can be presented with the following creds: $creds"
        )
    }
    suitableCreds.selfAttestableAttributes.forEach { (attributeIdentifier) ->
        val info = anoncredsProofRequest.requestedAttributes[attributeIdentifier]!!
        println(
            "Attribute '$info' should be self attested"
        )
    }
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
In the case where an agent holds no `Credential`s that are appropriate for a requested attribute group or predicate, a mapping to the referent will still be present, but will be mapped to an empty list.
{% endhint %}

### Credentials for a DIF Presentation

When the `retrieveCredentialsForProofRequest` API is used on a `.Dif` formatted `ProofExchange.Aries`, it will return a `RetrievedPresentationCredentials.Dif` data object. This object contains:

* `credentialsForRequestedDescriptors`: a map of requested [DIF Input Descriptor](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor) IDs, to the list of `Credential` IDs which may be used for presenting the input descriptor. Descriptor IDs correspond to the `InputDescriptor`s within `formatData`.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let proofExchange: ProofExchange.Aries

do {
    guard case .dif(let presentationDefinition) = proofExchange.formatData else {
        return
    }
    
    let suitableCreds = try await agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchangeId: proofExchange.proofExchangeId
    )
    guard case .dif(let credentialsForRequestedDescriptors) = suitableCreds else {
        return
    }

    credentialsForRequestedDescriptors.forEach { descriptorId, creds in
        let inputDescriptor = presentationDefinition.inputDescriptors.first { $0.id == descriptorId }!
        print("Input Descriptor \(inputDescriptor) can be presented with the following creds: \(creds)")
    }
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val proofExchange: ProofExchange.Aries

try {
    val difData = proofExchange.formatData as? ProofExchangeFormatData.Dif ?: return
    val presentationDefinition = difData.requestedPresentationDefinition

    val suitableCreds = agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchange.proofExchangeId
    ) as RetrievedPresentationCredentials.Dif

    suitableCreds.credentialsForRequestedDescriptors.forEach { (descriptorId, creds) ->
        val inputDescriptor = presentationDefinition.inputDescriptors.first { it.id == descriptorId }
        println(
            "Input Descriptor '$inputDescriptor' can be presented with the following creds: $creds"
        )
    }
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
In the case where an agent holds no `Credential`s that are appropriate for a requested Input Descriptor, a mapping to the descriptor will still be present, but will be mapped to an empty list.
{% endhint %}

### Credentials for a DCQL Presentation

When the `retrieveCredentialsForProofRequest` API is used on any `ProofExchange.OpenId4Vc`, it will return a `RetrievedPresentationCredentials.Dcql` data object. This object contains:

* `credentialIdsForQueryId`: a map of requested [DCQL Credential Query IDs](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l), to the list of `Credential` IDs which may be used for presenting the credential query. Query IDs correspond to the `CredentialQuery`s within `formatData`.

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let proofExchange: ProofExchange.OpenId4Vc

do {
    let dcqlQuery = proofExchange.presentationRequest
    
    let suitableCreds = try await agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchangeId: proofExchange.proofExchangeId
    )
    guard case .dcql(let credentialIdsForQueryId) = suitableCreds else {
        return
    }

    credentialIdsForQueryId.forEach { queryId, creds in
        let query = dcqlQuery.credentials.first { $0.id == queryId }!
        print("CredentialQuery \(query) can be presented with the following creds: \(creds)")
    }
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val proofExchange: ProofExchange.OpenId4Vc

try {
    val dcqlQuery = proofExchange.presentationRequest

    val suitableCreds = agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchange.proofExchangeId
    ) as RetrievedPresentationCredentials.Dcql

    suitableCreds.credentialIdsForQueryId.forEach { (queryId, creds) ->
        val query = presentationDefinition.credentials.first { it.id == queryId }
        println(
            "CredentialQuery '$query' can be presented with the following creds: $creds"
        )
    }
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
In the case where an agent holds no `Credential`s that are appropriate for a requested Credential Query, a mapping to the query will still be present, but will be mapped to an empty list.
{% endhint %}

## Presenting Credentials

After identifying credentials that are appropriate for presenting a proof exchange ([see above section](#finding-credentials-for-a-presentation)), the consumer must select a `Credential` to use for each requested presentation item. This selection is done by constructing a `PresentationCredentials` data object before passing it to the `presentProof` API where credential proofs will then be created and sent to the Verifier.

However, as before, the `PresentationCredentials` object has variants depending on the presentation exchange format being presented. The chosen variant should match the `RetrievedPresentationCredentials` variant.

### Presenting for an Anoncreds Presentation

Like other APIs, if presenting an Anoncreds presentation, then the `.Anoncred` variant should be used. In this case, a `PresentationCredentials.Anoncred` data object should be constructed with the following data:

* `credentialsForRequestedAttributes`:  a map of requested attribute group referents/IDs, to an `AnoncredPresentationAttributeGroup` object. This object contains the `credentialId` of the credential to present for the attribute group, and a `revealed` boolean flag. "revealed" is an Anoncreds zero-knowledge-proof feature: if `revealed` is set to false, the Verifier will receive proof of the Credential, without seeing any of the credential's attribute values.
* `credentialsForRequestedPredicates`: a map of requested predicate referents/IDs, to an `AnoncredPresentationPredicate` object. This object simply contains the `credentialId` of the credential to present for this predicate.
* `selfAttestedAttributes`: a map of self-attestable attribute referents/IDs, to a chosen string value (self attested value).

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let id: String // the id of the [ProofExchange]

do {
    let suitableCreds = try await agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchangeId: id
    )
    guard case .anoncred(
        let credentialsForRequestedAttributes,
        let credentialsForRequestedPredicates,
        let selfAttestableAttributes
    ) = suitableCreds else {
        return
    }
    
    let selectedCredsForRequestedAttributes = credentialsForRequestedAttributes.mapValues { creds in
        // auto-select the first credential for presentation
        AnoncredPresentationAttributeGroup(credentialId: creds[0], revealed: true)
    }
    
    let selectedCredsForRequestedPredicates = credentialsForRequestedPredicates.mapValues { creds in
        // auto-select the first credential for presentation
        AnoncredPresentationPredicate(credentialId: creds[0])
    }
    
    let selfAttestedAttributes = selfAttestableAttributes.reduce(into: [String: String]()) { map, referent in
        // auto-select a value
        map[referent] = "my attested value"
    }
    
    try await agent.proofs.exchange.presentProof(
        proofExchangeId: id,
        presentationCredentials: .anoncred(
            credentialsForRequestedAttributes: selectedCredsForRequestedAttributes,
            credentialsForRequestedPredicates: selectedCredsForRequestedPredicates,
            selfAttestedAttributes: selfAttestedAttributes
        )
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val id: String // the id of the [ProofExchange]

try {
    val suitableCreds = agent.proofs.exchange.retrieveCredentialsForProofRequest(
        id
    ) as RetrievedPresentationCredentials.Anoncred

    val selectedCredsForRequestedAttributes =
        suitableCreds.credentialsForRequestedAttributes.mapValues { (_, creds) ->
            // auto-select the first credential for presentation
            AnoncredPresentationAttributeGroup(creds[0], revealed = true)
        }

    val selectedCredsForRequestedPredicates =
        suitableCreds.credentialsForRequestedPredicates.mapValues { (_, creds) ->
            // auto-select the first credential for presentation
            AnoncredPresentationPredicate(creds[0])
        }
    
    val selfAttestedAttributes =
        suitableCreds.selfAttestableAttributes.associateWith { referent -> "my attested value" }

    agent.proofs.exchange.presentProof(
        proofExchangeId = id,
        presentationCredentials = PresentationCredentials.Anoncred(
            credentialsForRequestedAttributes = selectedCredsForRequestedAttributes,
            credentialsForRequestedPredicates = selectedCredsForRequestedPredicates,
            selfAttestedAttributes = selfAttestedAttributes
        )
    )
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For simplicity, this example assumes that there is at least 1 suitable credential for each attribute group and predicate. Additionally this example implements "auto-select", apps may wish to let their users manually select credentials. See our [sample apps](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/sdk-releases) for a reference.
{% endhint %}

### Presenting for a DIF Presentation

Like other APIs, if presenting a DIF presentation, then the `.Dif` variant should be used. In this case, a `PresentationCredentials.Dif` data object should be constructed with the following data:

* `credentialsForDescriptors`:  a map of requested input descriptor IDs, to the ID of the `Credential` that should present the descriptor.

{% hint style="success" %}
If possible, the Edge Agent SDK will automatically attempt to derive a selectively disclosed credential if the credential format and data allows (using BBS+ or SD-JWT). Meaning the agent will not present more data then necessary to the verifier; similar to Anoncreds.&#x20;

See [Aries RFC 0646](https://github.com/hyperledger/aries-rfcs/tree/main/features/0646-bbs-credentials) and [Standards and Protocols](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/standards-and-protocols).
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let id: String // the id of the [ProofExchange]

do {
    let suitableCreds = try await agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchangeId: id
    )
    guard case .dif(let credentialsForRequestedDescriptors) = suitableCreds else {
        return
    }
    
    let selectedCredsForDescriptors = credentialsForRequestedDescriptors.mapValues { creds in
        // auto-select the first credential for presentation
        creds[0]
    }
    
    try await agent.proofs.exchange.presentProof(
        proofExchangeId: id,
        presentationCredentials: .dif(
            credentialsForDescriptors: selectedCredsForDescriptors
        )
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val id: String // the id of the [ProofExchange]

try {
    val suitableCreds = agent.proofs.exchange.retrieveCredentialsForProofRequest(
        id
    ) as RetrievedPresentationCredentials.Dif

    val selectedCredsForDescriptors =
        suitableCreds.credentialsForRequestedDescriptors.mapValues { (_, creds) ->
            // auto-select the first credential for presentation
            creds[0]
        }
    
    agent.proofs.exchange.presentProof(
        proofExchangeId = id,
        presentationCredentials = PresentationCredentials.Dif(
            credentialsForDescriptors = selectedCredsForDescriptors
        )
    )
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For simplicity, this example assumes that there is at least 1 suitable credential for each input descriptor. Additionally this example implements "auto-select", apps may wish to let their users manually select credentials. See our [sample apps](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/sdk-releases) for a reference.
{% endhint %}

### Presenting for a DCQL Presentation

Like other APIs, if presenting a DCQL presentation, then the `.Dcql` variant should be used. In this case, a `PresentationCredentials.Dcql` data object should be constructed with the following data:

* `credentialIdForQueryId`:  a map of requested query IDs, to the ID of the `Credential` that should present the query.

{% hint style="success" %}
If possible, the Edge Agent SDK will automatically attempt to derive a selectively disclosed credential if the credential format and data allows (using BBS+ or SD-JWT). Meaning the agent will not present more data then necessary to the verifier; similar to Anoncreds.&#x20;

See [Aries RFC 0646](https://github.com/hyperledger/aries-rfcs/tree/main/features/0646-bbs-credentials) and [Standards and Protocols](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/standards-and-protocols).
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent
let id: String // the id of the [ProofExchange]

do {
    let suitableCreds = try await agent.proofs.exchange.retrieveCredentialsForProofRequest(
        proofExchangeId: id
    )
    guard case .dcql(let credentialIdsForQueryId) = suitableCreds else {
        return
    }
    
    let selectedCredsForQueries = credentialIdsForQueryId.mapValues { creds in
        // auto-select the first credential for presentation
        creds[0]
    }
    
    try await agent.proofs.exchange.presentProof(
        proofExchangeId: id,
        presentationCredentials: .dcql(
            credentialIdForQueryId: selectedCredsForQueries
        )
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent
val id: String // the id of the [ProofExchange]

try {
    val suitableCreds = agent.proofs.exchange.retrieveCredentialsForProofRequest(
        id
    ) as RetrievedPresentationCredentials.Dcql

    val selectedCredsForQueries =
        suitableCreds.credentialIdsForQueryId.mapValues { (_, creds) ->
            // auto-select the first credential for presentation
            creds[0]
        }
    
    agent.proofs.exchange.presentProof(
        proofExchangeId = id,
        presentationCredentials = PresentationCredentials.Dcql(
            credentialIdForQueryId = selectedCredsForQueries
        )
    )
} catch (e: Exception) {
    // Handle exception
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For simplicity, this example assumes that there is at least 1 suitable credential for each query. Additionally this example implements "auto-select", apps may wish to let their users manually select credentials. See our [sample apps](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/sdk-releases) for a reference.
{% endhint %}

## Get a Pending Presentation by ID

As mentioned above, a pending presentation is represented by `ProofExchange` objects. To retrieve the current state of a specific `ProofExchange` in the agent's wallet, the `getById` API can be used. If a proof exchange cannot be found by the given ID, then `null` is returned:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the proof exchange to get ([ProofExchange.proofExchangeId])

do {
    let proofExchange = try await agent.proofs.exchange.getById(proofExchangeId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the proof exchange to get ([ProofExchange.proofExchangeId])

launch {
    try {
        val proofExchange: ProofExchange? = agent.proofs.exchange.getById(id)
    } catch (e: ProofExchangeModule.GetException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Delete a Pending Presentation by ID

Similarly, a `ProofExchange` in the wallet can be easily deleted via the `deleteById` API:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the proof exchange to delete ([ProofExchange.proofExchangeId])

do {
    try await agent.proofs.exchange.deleteById(proofExchangeId: id)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the proof exchange to delete ([ProofExchange.proofExchangeId])

launch {
    try {
        agent.proofs.exchange.deleteById(id)
    } catch (e: ProofExchangeModule.DeleteException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

## Updating the Metadata of a Pending Presentation

`ProofExchange` objects contain some metadata that can be controlled by SDK consumers, allowing custom information to be attached to each `ProofExchange`, and allowing custom [listing functionality](#listing-pending-presentations) to be leveraged.

Each `ProofExchange` contains a list of `RecordTag` (`ProofExchange.tags`) attached to it, where a `RecordTag` is simply a name-value pair stored with the record. By default, some tags are attached to new `ProofExchange`s, this includes:

* tag-name: `~started_timestamp`, tag-value: *The UNIX epoch seconds which this presentation began exchange*

The tags on a `ProofExchange` can be replaced or updated by using the `updateProofExchange` API, and providing a new set to update. This will replace whatever the current set of tags is:

{% tabs %}
{% tab title="Swift" %}

```swift
let id: String // ID of the proof exchange to update ([ProofExchange.proofExchangeId])

// add a 'category' of 'work' to this proof, and a 'priority' of '1'
let update = ProofExchangeUpdate(tags: [
    RecordTag(name: "category", value: "work"),
    RecordTag(name: "~priority", value: "1")
])

do {
    try await agent.proofs.exchange.updateProofExchange(proofExchangeId: id, proofExchangeUpdate: update)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val id: String // ID of the proof exchange to update ([ProofExchange.proofExchangeId])

// add a 'category' of 'work' to this proof, and a 'priority' of '1'
val update = ProofExchangeUpdate(
    tags = listOf(
         RecordTag("category", "work"),
         RecordTag("~priority", "1")   
    )
)

launch {
    try {
        agent.proofs.exchange.updateProofExchange(id, update)
    } catch (e: ProofExchangeModule.UpdateException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Like most data in the wallet, `RecordTag` will be stored encrypted. **Unless**, the tag name is prefixed with `~`, then the tag value will be stored unencrypted. Storing a tag value as unencrypted will allow some additional listing queries to be performed ([see below](#listing-pending-presentations)).
{% endhint %}

## Listing Pending Presentations

To list all pending presentations in the agent's wallet, the `listAll` API can be used:

{% tabs %}
{% tab title="Swift" %}

```swift
do {
    let proofs = try await agent.proofs.exchange.listAll(options: nil)
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        val proofs: List<ProofExchange> = agent.proofs.exchange.listAll(null)
    } catch (e: ProofExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

### Filtered Listing

More complicated `ProofExchange` list queries can also be achieved by utilizing the `ListProofExchangeFilters`.

These filters allow for lists of `ProofExchange` to be filtered by; their `state`, by their `tags`, or filtered by both together.

Filtering by `state` can be achieved like so:

{% tabs %}
{% tab title="Swift" %}

```swift
// get all the pending proofs which are waiting to be presented
let filters = ListProofExchangeFilters(state: ProofExchangeState.request)
let options = ListProofExchangeOptions(filters: filters)
    
do {
    let proofsPendingPresentation = try await agent.proofs.exchange.listAll(options: options)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// get all the pending proofs which are waiting to be presented
val filters = ListProofExchangeFilters(state = ProofExchangeState.Request)
val options = ListProofExchangeOptions(filters)
        
launch {
    try {
        val proofsPendingPresentation = agent.proofs.exchange.listAll(options)
    } catch (e: ProofExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}

To filter by `tags` applied to the `ProofExchange` (i.e. applied via the [update API](#updating-the-metadata-of-a-pending-presentation)), the `tagFilter` field of `ListProofExchangeFilters` should be used. This field takes a `String` in compliance with a [Wallet Query Language (WQL)](https://hyperledger-indy.readthedocs.io/projects/sdk/en/latest/docs/design/011-wallet-query-language/README.html) Query.

Continuing from the example in the [Update API section](#updating-the-metadata-of-a-pending-presentation):&#x20;

{% tabs %}
{% tab title="Swift" %}

```swift
// WQL Query, filter for 'category' == 'work'
let wqlQuery = "{ \"category\": \"work\" }"
let filters = ListProofExchangeFilters(tagFilter: wqlQuery)
let options = ListProofExchangeOptions(filters: filters)
do {
    let pendingWorkProofs = try await agent.proofs.exchange.listAll(options: options)
} catch {
    // handle error
}
```

```swift
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
let wqlQuery = "{ \"~priority\": { \"$lt\": \"2\" } }"
let filters = ListProofExchangeFilters(tagFilter: wqlQuery)
let options = ListProofExchangeOptions(filters: filters)

do {
    let highPriorityPendingProofs = try await agent.proofs.exchange.listAll(options: options)
} catch {
    // Handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
// WQL Query, filter for 'category' == 'work'
val wqlQuery = """{ "category": "work" }"""
val filters = ListProofExchangeFilters(tagFilter = wqlQuery)
val options = ListProofExchangeOptions(filters)

launch {
    try {
        val pendingWorkProofs = agent.proofs.exchange.listAll(options)
    } catch (e: ProofExchangeModule.ListException) {
        // Handle exception
    }
}
```

```kotlin
// WQL Query, filter for priority < 2 (e.g. 'high' priority items)
val wqlQuery = """{ "~priority": { "${"$"}lt": "2" } }"""
val filters = ListProofExchangeFilters(tagFilter = wqlQuery)
val options = ListProofExchangeOptions(filters)

launch {
    try {
        val highPriorityPendingProofs = agent.proofs.exchange.listAll(options)
    } catch (e: ProofExchangeModule.ListException) {
        // Handle exception
    }
}
```

{% endtab %}
{% endtabs %}


# Utilize Alternative Cryptography Providers

Manage where cryptography operations are handled by the agent, for custom and high assurance use cases.

By default, the **Edge Agent SDK** utilizes software for cryptographic key management. These keys are managed by [Askar](https://github.com/openwallet-foundation/askar), and encrypted at rest. However, in some use cases, such as those where higher assurance is required, alternative cryptography providers may be injected into the Edge Agent, allowing key management to occur outside of the agent's software.&#x20;

These custom cryptography providers may utilize whatever provider is desired, such as: device embedded hardware security modules (HSMs), external HSMs (e.g. hardware tokens), cloud HSMs, etc.

&#x20;In addition to allowing custom cryptography providers, we provide default cryptography provider implementations for **iOS** and **Android** which utilize the **device's secure hardware** for key management.

## Implement a Custom Provider

Custom cryptography providers must conform to the `ExternalCryptoProvider` interface/protocol. To implement `ExternalCryptoProvider`, essential key usage and management functionality must be fulfilled:

* `createKeyPair` - method to create and store a key pair with the provider
* `deleteKeyPair`- method to delete a key pair from the provider
* `sign`- method to sign over some block of data with the given key pair within the provider

Notably, `ExternalCryptoProvider`s must also implement a unique `enclaveId` identifier. This identifier should be kept unique and constant.

{% hint style="info" %}
The iOS and Android SDK provides extensive documentation about how each method and variable of the interface must be implemented. Additionally, the `AndroidHardwareCryptoProvider` and `iOSHardwareCryptoProvider` default implementations can be used as a reference.
{% endhint %}

## Register the Provider

After implementing a custom provider, or if simply using the default `AndroidHardwareCryptoProvider`/`iOSHardwareCryptoProvider` implementations, an instance of that provider must be registered when building the agent instance.

To do this, the additional `registerExternalCryptoProvider` method should be used with the agent builder (see [Agent Management](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/agent-management#builder)). Multiple crypto providers can be registered, so long as they are unique.

{% tabs %}
{% tab title="Swift" %}

```swift
let myCustomCryptoProvider: any ExternalCryptoProvider // some implementation of the protocol

let networkConfiguration = NetworkConfiguration(
    sovConfiguration: nil,
    cheqdConfiguration: NetworkConfiguration.Cheqd()
)
let agentConfiguration = AgentConfiguration(networkConfiguration: networkConfiguration)
let agent = try SudoDIEdgeAgentBuilder()
    .setAgentConfiguration(agentConfiguration: agentConfiguration)
    .registerExternalCryptoProvider(provider: myCustomCryptoProvider)
    .registerExternalCryptoProvider(provider: IOSHardwareCryptoProvider())
    .build()
```

{% endtab %}

{% tab title="Kotlin" %}

<pre class="language-kotlin"><code class="lang-kotlin"><strong>val appContext: Context // Android application context
</strong>val myCustomCryptoProvider: ExternalCryptoProvider&#x3C;*,*> // some implementation of the interface

val networkConfiguration = NetworkConfiguration(
    sovConfiguration = null,
    cheqdConfiguration = NetworkConfiguration.Cheqd()
)

val agentConfiguration = AgentConfiguration(
    networkConfiguration = networkConfiguration
)

val agent: SudoDIEdgeAgent = SudoDIEdgeAgent.builder()
    .setContext(appContext)
    .setAgentConfiguration(agentConfiguration)
    .registerExternalCryptoProvider(myCustomCryptoProvider)
    .registerExternalCryptoProvider(AndroidHardwareCryptoProvider())
    .build()
</code></pre>

{% endtab %}
{% endtabs %}

## Utilizing the Provider

Once a provider instance is registered with the agent, the agent can be instructed to utilize that specific provider class via the configuration options for some operations.

### Create a DID backed by a Custom Cryptography Provider

One such operation where custom cryptography providers can be utilized, is during DID creation when the underlying DID key is generated. Utilizing a custom provider here means that the created DID will have a cryptographic key which is backed by that provider. Then in subsequent usages of that DID, such as binding it to an incoming credential or presenting a credential, the custom provider will be invoked whenever the DID's signature is required.

By default, the key pair for a new DID will be generated using the agent's internal software (see [Manage DIDs](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/manage-dids#create-dids)). However this can be changed with the `CreateKeyPairEnclaveOptions` options in the `CreateDidOptions` for did:key and did:jwk DIDs. For instance, to create a did:key using the device's secure hardware key management:

{% tabs %}
{% tab title="Swift" %}

```swift
let agent: SudoDIEdgeAgent

do {
    let createdDid = try await agent.dids.createDid(
        options: .didKey(
            keyType: .ed25519,
            enclaveOptions: .external(.init(
                providerClass: IOSHardwareCryptoProvider.self,
                providerOptions: .init()
            ))
        )
    )
} catch {
    // handle error
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val agent: SudoDIEdgeAgent

val hardwareEnclaveOptions = CreateKeyPairEnclaveOptions.External(
    providerClass = AndroidHardwareCryptoProvider::class,
    providerOptions = AndroidHardwareCryptoProvider.CreateKeyPairOptions(
        hardwarePreference = AndroidHardwareCryptoProvider.HardwarePreference.STRONGBOX_TEE_FALLBACK
    )
)

try {
    val createdDid = agent.dids.createDid(
        options = CreateDidOptions.DidKey(
            keyType = DidKeyType.ED25519,
            enclaveOptions = hardwareEnclaveOptions
        )
    )
} catch (e: DidsModule.CreateDidException) {
    // handle error
}
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
Check out our [sample apps](/guides/decentralized-identity/decentralized-identity/edge-agent-sdk/sdk-releases) for a reference on how these secure hardware cryptography providers can be utilized.
{% endhint %}


# SDK Releases

Additional resources for the Edge Agent SDKs

## Decentralized Identity Edge Agent SDK for iOS​ <a href="#decentralized-identity-relay-sdk-for-ios" id="decentralized-identity-relay-sdk-for-ios"></a>

* [​Sample app source code](https://github.com/sudoplatform/sudo-di-edge-agent-sample-ios)​

## Decentralized Identity Edge Agent SDK for Android <a href="#decentralized-identity-relay-sdk-for-android" id="decentralized-identity-relay-sdk-for-android"></a>

* [​Sample app source code](https://github.com/sudoplatform/sudo-di-edge-agent-sample-android)

{% hint style="success" %}
To gain access to the iOS or Android SDK please [contact us](https://sudoplatform.com/#lets-chat).
{% endhint %}


# Standards and Protocols

Technical summary of the standards and protocols supported by the Edge Agent SDK

## Secure Channel

<table><thead><tr><th width="156"></th><th>Description</th></tr></thead><tbody><tr><td><strong>DIDComm V1</strong></td><td>DIDComm V1 over HTTP/s is used by the Edge Agent to transmit and receive E2EE messages (credential exchanges, proof presentations, chat messages, etc).</td></tr></tbody></table>

## Credential & Presentation Formats

<table><thead><tr><th width="176"></th><th>Description</th></tr></thead><tbody><tr><td><strong>Anoncreds V1</strong></td><td><a href="https://hyperledger.github.io/anoncreds-spec/">Hyperledger Anoncreds V1</a> credentials and presentation can be used by the Edge Agent. Allowing the full zero-knowledge proof capabilities of Anoncreds to be taken advantage of by applications.</td></tr><tr><td><strong>W3C VCs/VPs</strong></td><td><a href="https://www.w3.org/TR/vc-data-model/">W3C Verifiable Credentials and Verifiable Presentations</a> (VCDM 1.1 &#x26; 2.0) are supported by the Edge Agent. Including Linked Data Proofs (LDP) signatures and SD-JWT signatures (vc+sd-jwt).</td></tr><tr><td><strong>SD-JWT VC</strong></td><td><a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/">IETF SD-JWT VCs</a> are supported by the Edge Agent.</td></tr></tbody></table>

## Credential & Proof Exchange

<table data-full-width="true"><thead><tr><th width="116.33333333333331"></th><th>Description</th><th>VC/VP Formats</th><th>Other</th></tr></thead><tbody><tr><td><strong>AIP1</strong></td><td>Hyperledger Aries protocols from the AIP1 suite can be used to receive credentials and present proofs. This includes the protocols <a href="https://github.com/hyperledger/aries-rfcs/blob/main/features/0036-issue-credential/README.md">issue-credential-v1</a> and <a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0037-present-proof">present-proof-v1</a> respectively.</td><td><ul class="contains-task-list"><li><input type="checkbox" checked><a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments">Indy</a></li></ul></td><td></td></tr><tr><td><strong>AIP2</strong></td><td>Hyperledger Aries protocols from the AIP2 suite can also be used to receive credentials and present proofs. This includes the protocols <a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2">issue-credential-v2</a> and <a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0454-present-proof-v2">present-proof-v2</a> respectively.</td><td><ul class="contains-task-list"><li><input type="checkbox" checked><a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments">Indy</a></li><li><input type="checkbox" checked><a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0771-anoncreds-attachments">Anoncreds</a></li><li><input type="checkbox" checked><a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0593-json-ld-cred-attach">Aries LD Proof (W3C)</a></li><li><input type="checkbox" checked><a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0510-dif-pres-exch-attach">DIF Presentation Exchange (W3C)</a></li></ul></td><td></td></tr><tr><td><strong>OID4VCI</strong></td><td><a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html">OpenID4VCI 1.0</a> protocol can be used to receive credentials from OpenID4VC issuers.</td><td><ul class="contains-task-list"><li><input type="checkbox" checked><a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-ietf-sd-jwt-vc">dc+sd-jwt (IETF SD-JWT VC)</a></li><li><input type="checkbox" checked><a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-vc-secured-using-data-integ">ldp_vc (W3C VCDM 1.1/2.0)</a></li><li><input type="checkbox" checked><a href="https://www.w3.org/TR/vc-jose-cose/#with-sd-jwt">vc+sd-jwt (W3C VCDM 2.0)</a></li></ul></td><td><ul class="contains-task-list"><li><input type="checkbox" checked>JWK binding</li><li><input type="checkbox" checked>Pre-authorization flow</li><li><input type="checkbox" checked>Authorization flow</li></ul></td></tr><tr><td><strong>OID4VP</strong></td><td><a href="https://openid.net/specs/openid-4-verifiable-presentations-1_0.html">OpenID4VP 1.0</a> protocol can be used to present credentials to OpenID4VP verifiers. </td><td><p></p><ul class="contains-task-list"><li><input type="checkbox" checked><a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-ietf-sd-jwt-vc">dc+sd-jwt (IETF SD-JWT VC)</a></li><li><input type="checkbox" checked><a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-vc-secured-using-data-integ">ldp_vc (W3C VCDM 1.1/2.0)</a></li><li><input type="checkbox" checked><a href="https://www.w3.org/TR/vc-jose-cose/#with-sd-jwt">vc+sd-jwt (W3C VCDM 2.0)</a></li></ul></td><td><ul class="contains-task-list"><li><input type="checkbox" checked>DID-based verifiers</li><li><input type="checkbox" checked>direct_post flow</li><li><input type="checkbox">🚧 fragment/ redirect flow</li></ul></td></tr></tbody></table>

{% hint style="info" %}
[vc+sd-jwt](https://www.w3.org/TR/vc-jose-cose/#with-sd-jwt) (SD-JWT signed W3C VCDM 2.0) over OID4VCI/OID4VP is not a registered format yet. In the absence of a registered specification, we target interop with key libraries such as **credo-ts**.
{% endhint %}

## Supported DID Methods

<table data-full-width="true"><thead><tr><th width="128"></th><th width="566">Description</th><th width="96" data-type="checkbox">Resolve</th><th data-type="checkbox">Create</th></tr></thead><tbody><tr><td><a href="https://w3c-ccg.github.io/did-method-key/"><strong>did:key</strong></a></td><td>DID Keys can be used by the Edge Agent. They are particularly useful for creating a holder binding with some entity (e.g. W3C VC)</td><td>true</td><td>true</td></tr><tr><td><a href="https://github.com/quartzjer/did-jwk/blob/main/spec.md"><strong>did:jwk</strong></a></td><td>DID JWKs can be used by the Edge Agent. Like did:key, they are ideal for holder bindings, and have seen adoption in the OpenID4VC ecosystem.</td><td>true</td><td>true</td></tr><tr><td><a href="https://identity.foundation/peer-did-method-spec/index.html"><strong>did:peer</strong></a></td><td>did:peer V4 is used by the Edge Agent in the process of establishing peer to peer connections.</td><td>true</td><td>true</td></tr><tr><td><a href="https://docs.cheqd.io/product/architecture/adr-list/adr-001-cheqd-did-method"><strong>did:cheqd</strong></a></td><td>DID Cheqd is supported by the Edge Agent for DID and Anoncreds resolution. This method is a highly attractive public DID type, as it bridges modern DID features with an open decentralized ledger.</td><td>true</td><td>false</td></tr><tr><td><a href="https://w3c-ccg.github.io/did-method-web/"><strong>did:web</strong></a></td><td>Resolution of DID Web is supported by the Edge Agent. This method is one of the simplest public DID methods, but has gained adoption for it's simplicity in many OpenID4VC implementations.</td><td>true</td><td>false</td></tr><tr><td><a href="https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html"><strong>did:sov</strong></a></td><td>Sovrin DIDs and Anoncreds can be resolved by the Edge Agent against the configured sovrin network (any indy network). Supporting the public DID type preferred by many Aries Issuers.</td><td>true</td><td>false</td></tr><tr><td><a href="https://identity.foundation/didwebvh/next/"><strong>did:webvh</strong></a></td><td>🚧 <em>Coming soon..</em></td><td>false</td><td>false</td></tr><tr><td><a href="https://hyperledger.github.io/indy-did-method/"><strong>did:indy</strong></a></td><td>🚧 <em>Coming soon.. (replacing vague did:sov usage)</em></td><td>false</td><td>false</td></tr></tbody></table>

## Other Protocols

Some other major protocols supported by Edge Agent include (but not limited to):

<table><thead><tr><th width="180"></th><th>Description</th></tr></thead><tbody><tr><td><strong>AIP2 Connection Protocols</strong></td><td>Out of Band protocol (<a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0434-outofband">RFC 0434</a>) &#x26; DIDExchange protocol (<a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0023-did-exchange">RFC 0023</a>) is supported by the Edge Agent as a means for bootstrapping DIDComm connections.</td></tr><tr><td><strong>AIP1 Connection Protocol</strong></td><td>The AIP1 connection protocol (<a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0160-connection-protocol">RFC 0160</a>) is supported by the Edge Agent as a means for bootstrapping DIDComm connections.</td></tr><tr><td><strong>Aries Basic Messages</strong></td><td>The AIP2 protocol for exchanging text messages along a DIDComm connection (<a href="https://github.com/hyperledger/aries-rfcs/tree/c3b0e2120ad24810598375663b6922b980f85d00/features/0095-basic-message">RFC 0095</a>) is supported by the Edge Agent (see <a href="/pages/eXN4pAzhxLEOZ9z8iIed">Messaging</a>).</td></tr><tr><td><strong>Aries BBS+ Derived W3C VCs</strong></td><td>As an extension to supporting W3C credentials, the Edge Agent can receive and derive selectively disclosed variants of BBS+ signed credentials (as described in <a href="https://github.com/hyperledger/aries-rfcs/tree/main/features/0646-bbs-credentials">RFC 0646</a>). This means the agent can selectively disclose a subset of their BBS+ W3C credential attributes when performing a presentation.</td></tr><tr><td><strong>OpenID SIOPv2</strong></td><td>🚧 <em>Coming soon..</em></td></tr></tbody></table>

## Data Encryption

{% hint style="success" %}
All Agent wallet data (Connections, Credentials, Keys, Exchange states, Messages, etc) is encrypted at rest, using a combination of [Aries Askar](https://github.com/hyperledger/aries-askar) and SQLite encryption.
{% endhint %}


# Cloud Agent

Providing Decentralized Identity cloud agent solutions to your organization.

Sudo Platform provides a decentralized identity Cloud Agent. Once your dedicated cloud agent instance is deployed, you can interact with it via its admin API.

The Sudo Platform Cloud Agent automates as much as possible, handling differences between various protocols and credential specifications for you via its unified admin API, allowing your organization to focus on the shortest path to success.

<table><thead><tr><th width="212.4102564102564">Component</th><th>Description</th></tr></thead><tbody><tr><td><a href="/pages/dmP85gTzVAgnFMX1TWm5">Cloud Agent Admin API</a></td><td>A flexible and powerful GraphQL API for the Sudo Platform Cloud Agent which can be called using any HTTP client, with authentication methods supporting both server-side and client-side web applications.</td></tr></tbody></table>


# Cloud Agent Admin API

Sudo Platform Decentralized Identity Cloud Agent admin API supports exchange of credentials and presentations over various Aries protocols, with support for both Anoncreds and W3C VC specifications

The cloud agent admin API allows you to easily perform decentralized identity (DI) operations using your dedicated cloud agent instance. This API is pure GraphQL and can be interacted with by any HTTP client, empowering both server-side and client-side web applications with decentralized identity capabilities.

The cloud agent supports Aries protocols and the Anoncreds and W3C Verifiable Credential Data Model specifications, with support for more protocols and digital credential specifications coming soon.

## Key Features

|                                                                                                                                                                                                                                                                                                             |                                                                                                                                                                                                                                                                                                                           |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Establish end-to-end encrypted DIDComm connections](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/aries-interop-profile-aip/connection-exchanges)                                                                                                                                       | Send connection invitations, and reply to connection invitations                                                                                                                                                                                                                                                          |
| [Manage established connections](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/connections)                                                                                                                                                                                              | Browse connections and close connections                                                                                                                                                                                                                                                                                  |
| [Securely message connections](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/basic-messages)                                                                                                                                                                                             | Securely send and receive textual messages with established connections                                                                                                                                                                                                                                                   |
| Write Anoncreds [schemas](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials/schemas) and [credential definitions](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials/credential-definitions) to the ledger | Prepare your own Anoncreds credential templates and apply them when issuing Anoncreds credentials                                                                                                                                                                                                                         |
| [Issue and receive credentials](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/aries-interop-profile-aip/credential-exchanges)                                                                                                                                                            | Issue both Anoncreds and W3C credentials with a single unified API, supporting all Aries protocol versions for maximum interoperability. The cloud agent can also hold its own credentials when necessary. Your organization has full control over whether to keep or erase the cloud agent's credential exchange history |
| [Verify and present credentials](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/aries-interop-profile-aip/proof-exchanges)                                                                                                                                                                | Verify both Anoncreds and W3C credentials with a single unified API, supporting all Aries protocol versions for maximum interoperability. The cloud agent can also present its own credentials when necessary. Your organization has full control over whether to keep or erase the cloud agent's verification history    |
| [Manage held credentials](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials)                                                                                                                                                                                                     | Manage both [Anoncreds credentials](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials) and [W3C credentials](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/w3c-credentials) which are held by the cloud agent under a single unified API  |
| [Audit trail for the cloud agent](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/audit-logs)                                                                                                                                                                                              | The cloud agent admin API automatically logs all user operations, giving you internal visibility on your organizational operations                                                                                                                                                                                        |

## Next steps

* [Integrate your server-side or client-side application with the cloud agent admin API](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/integrate-the-cloud-agent-admin-api)


# Integrate the Cloud Agent Admin API

Integrate your application with the Cloud Agent admin API

The Sudo Platform Cloud Agent admin API is a pure GraphQL API. [Click here](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/api-schema) to view the full schema.

To integrate, simply make HTTP requests to the admin API of your deployed Cloud Agent instance. Server-side applications can use an API key for authentication, meanwhile client-side applications can use a user's login credentials and resultant bearer token for authentication. Keep your API key secure. Never give a client-side application access to your API key.

## Integrate server-side application using API Key

If calling the admin API from a server-side application, set the `x-api-key` header to the admin API key for your Cloud Agent instance. GraphQL queries and mutations always use the `POST` HTTP method. Insert your GraphQL query or mutation as a string into the request `body`. Following is an example of using `fetch` to make HTTP requests in TypeScript.

```typescript
async function callWithApiKey(gql: string) {
  const adminApiUrl: string = '' // Set the URL to your Cloud Agent instance's admin API.
  const adminApiKey: string = '' // Set your admin API key.

  const response = await fetch(adminApiUrl, {
    method: 'POST',
    body: gql,
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      'x-api-key': adminApiKey,
    }
  })

  if (response.status !== 200) {
    throw `HTTP response status ${response.status}`
  }

  const json = JSON.parse(await response.text())

  if (json.errors) {
    throw `GraphQL error when resolving ${json.errors[0].path}: ${json.errors[0].message}`
  }

  return json.data
}

// The following example GraphQL query fetches recent basic messages for a connection.
const exampleGqlQuery: string = `{
  connection(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
  ) {
    id
    alias
    basicMessages(
      page: {}
    ) {
      items {
        id
        content
        timestamp
        direction
      }
      nextToken
    }
  }
}`
const queryResponse = await callWithApiKey(exampleGqlQuery)
const connectionAlias: string = queryResponse.connection.alias
const messages: BasicMessage[] = queryResponse.connection.basicMessages.items

// The following example GraphQL mutation sends a basic message to a connection.
const exampleGqlMutation: string = `mutation {
  sendBasicMessage(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
    content: "Hello Bob"
  ) {
    id
    content
    timestamp
    direction
  }
}`
const mutationResponse = await callWithApiKey(exampleGqlMutation)
const sentMessageId = mutationResponse.sendBasicMessage.id
```

## Integrate client-side application using Bearer Token

If calling the Cloud Agent admin API directly from a client-side application, the user of the client-side application would first need to be authenticated using their user login credentials, which would return a bearer token. This bearer token can then be used to authenticate HTTP requests to the admin API. The example code found above still applies - simply replace the `fetch` call's auth method.

```typescript
const adminApiUrl: string = '' // Set the URL to your Cloud Agent instance's admin API.
const bearerToken: string = '' // Set the bearer token for the active authenticated user.

await fetch(adminApiUrl, {
  method: 'POST',
  body: gql,
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json',
    Authorization: `Bearer ${bearerToken}`,
  }
})
```


# Aries Interop Profile (AIP)

Use Aries protocols to establish connections, exchange credentials and exchange presentations

The cloud agent is [Aries](https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0302-aries-interop-profile/README.md) compatible. The cloud agent supports all current versions of the Aries protocols (v1 and v2) but uses the latest version (v2) by default.


# Connection Exchanges

Manage connection exchanges using Aries protocols

A connection exchange, represented by `AIPConnectionExchange`, is an encapsulation of the cloud agent's progress in establishing a new DIDComm connection. A connection exchange starts with a connection invitation, then follows a relevant Aries protocol to communicate with the connecting party (a foreign agent) until either the connection is established or the protocol is aborted.

## Connection Exchange Roles & States

An `AIPConnectionExchange` has a role (known as `myRole`) and a `state` associated with it.

### Role

The role indicates which role the cloud agent plays in the connection exchange protocol. The role is fixed for the lifetime of the connection exchange.

Role is either:

`INVITER` or `INVITEE`

### State

The state indicates the current state of the cloud agent in the connection exchange protocol. The state transitions over time in a forward direction and cannot transition to previous states.

State transitions to success:

`INVITATION` -> `REQUEST` -> `RESPONSE` -> `DONE`

### Summary

The following table provides a description of each role and each role+state combination.

| Role      | State        | Description                                                                                   |
| --------- | ------------ | --------------------------------------------------------------------------------------------- |
| `INVITER` |              | The connection exchange was initiated by the cloud agent.                                     |
|           | `INVITATION` | The cloud agent sent a connection invitation.                                                 |
|           | `REQUEST`    | The cloud agent received a connection request.                                                |
|           | `RESPONSE`   | The cloud agent sent a response to a connection request.                                      |
|           | `DONE`       | Terminal state for a successful connection exchange. A `Connection` was created.              |
|           | `ABANDONED`  | Terminal state for an unsuccessful connection exchange. A `Connection` was ***not*** created. |
| `INVITEE` |              | The connection exchange was initiated by a foreign agent.                                     |
|           | `INVITATION` | The cloud agent received a connection invitation.                                             |
|           | `REQUEST`    | The cloud agent sent a connection request.                                                    |
|           | `RESPONSE`   | The cloud agent received a response to its connection request.                                |
|           | `DONE`       | Terminal state for a successful connection exchange. A `Connection` was created.              |
|           | `ABANDONED`  | Terminal state for an unsuccessful connection exchange. A `Connection` was ***not*** created. |

## Create connection exchange

The cloud agent creates a new `AIPConnectionExchange` with a single-use invitation embedded within. The generated `invitationUrl` must be sent to its intended recipient out-of-band, for instance, via a QR code for the recipient to scan with their DI wallet. The connection exchange will be visible to the cloud agent under the `alias` "Bob". Any foreign agent who views the invitation will see that it was created by "Alice" (this is an unverifiable, self-attested label). Once a foreign agent has responded to the invitation, the invitation will be invalidated. In this scenario, the cloud agent has role `INVITER`.

```graphql
mutation MyMutation {
  createAipConnectionInvitation(
    invitation: {
      alias: "Bob"
      myLabel: "Alice"
    }
  ) {
    id
    myRole
    state
    alias
    theirLabel
    invitation {
      invitationUrl
    }
  }
}
```

## Accept connection invitation

The cloud agent receives the connection invitation, creates a new `AIPConnectionExchange` and then accepts the invitation in a single operation. The received `invitationUrl` is stored for future reference. The connection exchange will be visible to the cloud agent under the `alias` "Alice". In communications with the foreign agent, the cloud agent will present itself as "Bob" (this is an unverifiable, self-attested label). In this scenario, the cloud agent has role `INVITEE`.

```graphql
mutation MyMutation {
  acceptAipConnectionInvitation(
    invitation: {
      invitationUrl: "https://example.com?c_i=<base64_encoded_invitation>"
      alias: "Alice"
      myLabel: "Bob"
    }
  ) {
    id
    myRole
    state
    alias
    theirLabel
    invitation {
      invitationUrl
    }
  }
}
```

## View connection exchanges

View connection exchanges in any state, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  aipConnectionExchanges(
    page: {
      nextToken: null
    }
    filter: {}
  ) {
    items {
      id
      myRole
      state
      alias
      theirLabel
    }
    nextToken
  }
}
```

Alternatively, view a single connection exchange, referenced by its unique ID.

```graphql
query MyQuery {
  aipConnectionExchange(
    connExId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
  ) {
    id
    myRole
    state
    alias
    theirLabel
  }
}
```

## Delete a connection exchange

Delete a connection exchange along with its embedded invitation. Note that if the connection exchange is in the `DONE` state, the resultant `Connection` is ***not*** deleted by this operation.

```graphql
mutation MyMutation {
  deleteAipConnectionExchange(
    connExId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
  )
}
```


# Credential Exchanges

Manage credential exchanges using Aries protocols

A credential exchange, represented by `AIPCredentialExchange`, is an encapsulation of the cloud agent's progress in either issuing or receiving a credential over an established DIDComm connection.

## Credential Exchange Roles & States

An `AIPCredentialExchange` has a role (known as `myRole`) and a `state` associated with it.

### Role

The role indicates which role the cloud agent plays in the credential exchange protocol. The role is fixed for the lifetime of the credential exchange.

Role is either:

`ISSUER` or `HOLDER`

### State

The state indicates the current state of the cloud agent in the credential exchange protocol. The state transitions over time in a forward direction and cannot transition to previous states.

State transitions to success:

`OFFER` -> `REQUEST` -> `ISSUED` -> `DONE`

`PROPOSAL` -> `OFFER` -> `REQUEST` -> `ISSUED` -> `DONE`

### Summary

The following table provides a description of each role and each role+state combination.

| Role     | State       | Description                                                                                                                                                                                                              |
| -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ISSUER` |             | The cloud agent is acting as the credential issuer.                                                                                                                                                                      |
|          | `PROPOSAL`  | The connection proposed that the cloud agent issue it a credential.                                                                                                                                                      |
|          | `OFFER`     | The cloud agent sent a credential offer. If this state was preceded by `PROPOSAL`, then the offered credential may be based on the connection's proposal.                                                                |
|          | `REQUEST`   | The connection requested that the cloud agent issue the offered credential.                                                                                                                                              |
|          | `ISSUED`    | The cloud agent issued the offered credential to the connection.                                                                                                                                                         |
|          | `DONE`      | Terminal state for a successful credential exchange. The credential was issued.                                                                                                                                          |
|          | `ABANDONED` | Terminal state for an unsuccessful credential exchange. The credential was ***not*** issued.                                                                                                                             |
| `HOLDER` |             | The cloud agent is acting as the credential holder, and the connection is acting as the credential issuer. Technically, the cloud agent only holds the credential once the credential exchange reaches the `DONE` state. |
|          | `PROPOSAL`  | The cloud agent proposed that the connection issue it a credential.                                                                                                                                                      |
|          | `OFFER`     | The cloud agent received a credential offer. If this state was preceded by `PROPOSAL`, then the offered credential may be based on the cloud agent's proposal.                                                           |
|          | `REQUEST`   | The cloud agent requested that the connection issue the offered credential.                                                                                                                                              |
|          | `ISSUED`    | The connection issued the offered credential to the cloud agent.                                                                                                                                                         |
|          | `DONE`      | Terminal state for a successful credential exchange. A `Credential` was created.                                                                                                                                         |
|          | `ABANDONED` | Terminal state for an unsuccessful credential exchange. A `Credential` was ***not*** created.                                                                                                                            |

## Supported credential formats

The cloud agent supports credential exchanges for both Anoncreds Verifiable Credentials and W3C Verifiable Credentials. The credential format for the specific credential exchange is specified using the `format` nested object.

## Offer a credential

The cloud agent creates a new `AIPCredentialExchange` with role `ISSUER` and sends a credential offer to the specified `connectionId`. The `format` input object must be filled with the details of the credential to offer.

```graphql
mutation MyMutation {
  sendAipCredentialOffer(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
    offer: {
      format: {
        # INSERT DETAILS OF THE CREDENTIAL TO OFFER
      }
      autoAcceptRequest: true
    }
  ) {
    id
    myRole
    state
    connectionId
  }
}
```

If instead the cloud agent *receives* a credential offer, a new `AIPCredentialExchange` is created with role `HOLDER`.

{% hint style="info" %}
Anoncreds credentials

In order to offer and issue an [Anoncreds credential](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials), a `credDefId` must be provided. This value must reference a [credential definition](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials/credential-definitions) which is already written to the ledger.
{% endhint %}

## View received credential offers

View credential exchanges in any state, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

As an example, the filter options can be used to find received credential offers, even from a specific connection.

```graphql
query MyQuery {
  aipCredentialExchanges(
    page: {
      nextToken: null
    }
    filter: {
      myCredExRole: HOLDER
      credExStates: [OFFER]
      connectionIds: ["001fe530-3562-41d6-a631-3df9c7ae1f38"]
    }
  ) {
    items {
      id
      myRole
      state
      messages {
        offer {
          comment
          format {
            __typename
            ... on AnoncredsCredOffer {
              credDefId
              schemaId
              attributeValues {
                name
                value
              }
            }
            ... on W3CCredNegotiation {
              credentialJson
            }
          }
        }
      }
      connectionId
    }
    nextToken
  }
}
```

Alternatively, view a single credential exchange, referenced by its unique ID.

```graphql
query MyQuery {
  aipCredentialExchange(
    credExId: "225d12f5-f5fa-43d0-ba83-bf602f729fd6"
  ) {
    id
    myRole
    state
    connectionId
  }
}
```

## Accept a credential offer

The cloud agent accepts the offer by requesting the offered credential from the connection. The input param `credExId` must match the ID of an `AIPCredentialExchange` which has the `HOLDER` role and is in the `OFFER` state. When the cloud agent finally receives the issued credential, the credential will be automatically stored as a new `Credential`.

```graphql
mutation MyMutation {
  acceptAipCredentialOffer(
    credExId: "225d12f5-f5fa-43d0-ba83-bf602f729fd6"
    acceptOffer: {}
  ) {
    id
    myRole
    state
    connectionId
  }
}
```

## Issue a credential

The cloud agent accepts a request for a credential by issuing the credential to the connection. The input param `credExId` must match the ID of an `AIPCredentialExchange` which has the `ISSUER` role and is in the `REQUEST` state.

```graphql
mutation MyMutation {
  acceptAipCredentialRequest(
    credExId: "33c3cfe6-3377-44eb-a357-bce11b2f9c17"
    acceptRequest: {}
  ) {
    id
    myRole
    state
    connectionId
  }
}
```

## Delete a credential exchange

Delete a credential exchange. If the credential exchange is not in the `DONE` state, then this operation will also send a "problem report" to the associated connection to inform them that the cloud agent has abandoned the credential exchange. Note that if the credential exchange has role `HOLDER` and is in the `DONE` state, the exchange will have resulted in a new `Credential`, which is ***not*** deleted by this operation.

```graphql
mutation MyMutation {
  deleteAipCredentialExchange(
    credExId: "225d12f5-f5fa-43d0-ba83-bf602f729fd6"
  )
}
```


# Proof Exchanges

Manage proof exchanges, also known as presentation exchanges, using Aries protocols

A proof exchange, represented by `AIPProofExchange`, is an encapsulation of the cloud agent's progress in either sending or receiving a credential presentation over an established DIDComm connection.

## Proof Exchange Roles & States

An `AIPProofExchange` has a role (known as `myRole`) and a `state` associated with it.

### Role

The role indicates which role the cloud agent plays in the proof exchange protocol. The role is fixed for the lifetime of the proof exchange.

Role is either:

`VERIFIER` or `PROVER`

### State

The state indicates the current state of the cloud agent in the proof exchange protocol. The state transitions over time in a forward direction and cannot transition to previous states.

State transitions to success:

`REQUEST` -> `PRESENTED` -> `DONE`

`PROPOSAL` -> `REQUEST` -> `PRESENTED` -> `DONE`

### Summary

The following table provides a description of each role and each role+state combination.

| Role       | State       | Description                                                                                                                                                                                                                                                                                                                    |
| ---------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `VERIFIER` |             | The cloud agent is acting as the verifier of the credential presentation.                                                                                                                                                                                                                                                      |
|            | `PROPOSAL`  | The connection proposed that it send a presentation to the cloud agent for verification.                                                                                                                                                                                                                                       |
|            | `REQUEST`   | The cloud agent sent a proof request (also known as a presentation request) to the connection. If this state was preceded by `PROPOSAL`, then the request may be based on the connection's proposal.                                                                                                                           |
|            | `PRESENTED` | The cloud agent received the presentation from the connection.                                                                                                                                                                                                                                                                 |
|            | `DONE`      | Terminal state for a successful proof exchange. The received presentation has undergone verification. Note that this state does not indicate the result of the verification. To check the verification result, see `verificationResult` on the `AIPProofPresentation` message which is embedded within the `AIPProofExchange`. |
|            | `ABANDONED` | Terminal state for an unsuccessful proof exchange. The cloud agent did ***not*** receive a presentation from the connection.                                                                                                                                                                                                   |
| `PROVER`   |             | The cloud agent is acting as the prover (also known as the holder or the presenter). The cloud agent will generate and send a verifiable presentation using one or more of its held credentials as a base.                                                                                                                     |
|            | `PROPOSAL`  | The cloud agent proposed that it send a presentation to a connection for verification.                                                                                                                                                                                                                                         |
|            | `REQUEST`   | The cloud agent received a proof request (also known as a presentation request) from the connection. If this state was preceded by `PROPOSAL`, then the request may be based on the cloud agent's proposal.                                                                                                                    |
|            | `PRESENTED` | The cloud agent sent the presentation to the connection.                                                                                                                                                                                                                                                                       |
|            | `DONE`      | Terminal state for a successful proof exchange. Note that the cloud agent with the role of `PROVER` does not receive any indication of the verification result.                                                                                                                                                                |
|            | `ABANDONED` | Terminal state for an unsuccessful proof exchange. The cloud agent did ***not*** send a presentation to the connection.                                                                                                                                                                                                        |

## Supported credential presentation formats

The cloud agent supports proof exchanges for both Anoncreds Verifiable Presentations and W3C Verifiable Presentations. The credential presentation format for the specific proof exchange is specified using the `format` nested object.

## Request proof

The cloud agent creates a new `AIPProofExchange` with role `VERIFIER` and send a proof request to the specified `connectionId`. The `format` input object must be filled with the details of the presentation to request.

A `referent` must be included for each set of attributes requested. The referent acts as an identifier for a set of attributes, and the prover must reference it in their subsequent presentation. If more than one referent appears in the same proof request, the prover will be able to present a different credential per referent.

```graphql
mutation MyMutation {
  sendAipProofRequest(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
    request: {
      format: {
        # INSERT DETAILS OF THE PRESENTATION TO REQUEST
      }
    }
  ) {
    id
    myRole
    state
    connectionId
  }
}
```

If instead the cloud agent *receives* a proof request, a new `AIPProofExchange` is created with role `PROVER`.

## View received proof requests

View proof exchanges in any state, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

As an example, the filter options can be used to find received proof requests, even from a specific connection.

```graphql
query MyQuery {
  aipProofExchanges(
    page: {
      nextToken: null
    }
    filter: {
      myProofExRole: PROVER
      proofExStates: [REQUEST]
      connectionIds: ["001fe530-3562-41d6-a631-3df9c7ae1f38"]
    }
  ) {
    items {
      id
      myRole
      state
      messages {
        request {
          comment
          format {
            __typename
            ... on AnoncredsProofRequest {
              attributeGroups {
                referent
                attributeNames
                restrictions {
                  schemaId
                  schemaIssuerId
                  schemaName
                  schemaVersion
                  credDefId
                  issuerId
                  attributeNames
                  attributeValues {
                    name
                    value
                  }
                  nonRevoked {
                    from
                    to
                  }
                  predicate {
                    operator
                    threshold
                  }
                }
              }
            }
            ... on W3CProofNegotiation {
              attributeGroups {
                referent
                attributes {
                  attributePaths
                  valueRestrictions {
                    dataFormat
                    pattern
                    inSet
                    notInSet
                    equal
                    notEqual
                    minLength
                    maxLength
                    lessThan
                    lessThanOrEqual
                    greaterThan
                    greaterThanOrEqual
                  }
                  isHolderSubject
                  presentationType
                }
              }
              submissionRequirementJsons
            }
          }
        }
      }
      connectionId
    }
    nextToken
  }
}
```

Alternatively, view a single proof exchange, referenced by its unique ID.

```graphql
query MyQuery {
  aipProofExchange(
    proofExId: "44269eb7-0288-46ad-ab59-e646ad010202"
  ) {
    id
    myRole
    state
    connectionId
  }
}
```

## Accept a proof request

Before accepting the proof request, the cloud agent must select which credentials to use in the presentation. The following call can be used to quickly identify held [credentials](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials) which are valid for selection. The returned `referent` maps each set of credential IDs to a `referent` in the proof request. The array of returned `credentialIds` will be empty if the cloud agent does not possess any valid credential for that referent.

```graphql
mutation MyMutation {
  credentialsForAipProofExchange(
    proofExId: "44269eb7-0288-46ad-ab59-e646ad010202"
  ) {
    credentialsForReferents {
      referent
      credentialIds
    }
  }
}
```

The cloud agent accepts the request by generating the presentation and sending it to the connection. The input param `proofExId` must match the ID of an `AIPProofExchange` which has the `PROVER` role and is in the `REQUEST` state. Once the cloud agent with `PROVER` role has accepted a proof request, there is nothing left for it to do in the exchange.

```graphql
mutation MyMutation {
  acceptAipProofRequest(
    proofExId: "44269eb7-0288-46ad-ab59-e646ad010202"
    presentation: {
      format: {
        # INSERT DETAILS OF THE PRESENTATION TO SEND
      }
    }
  ) {
    id
    myRole
    state
    connectionId
  }
}
```

## Delete a proof exchange

Delete a proof exchange. This will send a "problem report" to the associated connection to inform them that the cloud agent has abandoned the proof exchange.

```graphql
mutation MyMutation {
  deleteAipProofExchange(
    credExId: "44269eb7-0288-46ad-ab59-e646ad010202"
  )
}
```


# Connections

Manage established connections

A `Connection` represents an established, active communications pathway with a foreign agent whom the cloud agent has connected to via successful completion of a [connection exchange](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/aries-interop-profile-aip/connection-exchanges).

## View connections

View established connections, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  connections(
    page: {
      nextToken: null
    }
    filter: {}
  ) {
    items {
      id
      alias
    }
    nextToken
  }
}
```

Alternatively, view a single connection, referenced by its unique ID.

```graphql
query MyQuery {
  connection(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
  ) {
    id
    alias
  }
}
```

## Delete a connection

Delete an established connection. Any basic messages associated with the connection are also deleted.

```graphql
mutation MyMutation {
  deleteConnection(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
  )
}
```


# Basic Messages

Send and receive messages over DIDComm connection

A `BasicMessage` is an arbitrary textual message sent over DIDComm connection. It allows for basic communications between the cloud agent and an established [connection](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/connections).

## Send a basic message to a connection

The cloud agent creates a new `BasicMessage` and sends the message to the specified connection.

```graphql
mutation MyMutation {
  sendBasicMessage(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
    content: "Hi Bob. I've sent you your updated credential offer. If you don't accept it within the next week, I'll have to withdraw the offer. Please let me know if you changed your mind. Thanks"
  ) {
    id
    content
    timestamp
    direction
  }
}
```

## Broadcast a basic message to multiple connections

The cloud agent sends a message to an array of connections. This operation creates multiple `BasicMessage`(s) with identical `content`, one for each specified connection. The operation returns an array of `connectionIds` to whom the message was successfully sent.

```graphql
mutation MyMutation {
  broadcastBasicMessage(
    connectionIds: ["001fe530-3562-41d6-a631-3df9c7ae1f38", "01560d79-712a-42d5-91f6-8ded03c7e549"]
    content: "An upgraded version of your credential is now available. See website for details."
  ) {
    connectionIds
  }
}
```

## View basic messages for a connection

View basic messages, with pagination and a required `connectionId` filter. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  basicMessages(
    page: {
      nextToken: null
    }
    filter: {
      connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
    }
  ) {
    items {
      id
      content
      timestamp
      direction
    }
    nextToken
  }
}
```

Alternatively, when querying a `Connection`, it is possible to fetch basic messages associated with that connection. The following query fetches a page of connections who have basic messages associated with them, and returns the latest basic message for each of those connections.

```graphql
query MyQuery {
  connections(
    page: {
      nextToken: null
    }
    filter: {
      hasBasicMessages: true
    }
  ) {
    items {
      id
      alias
      basicMessages(page: {
        limit: 1
        nextToken: null
      }) {
        items {
          id
          content
          timestamp
          direction
        }
        nextToken
      }
    }
    nextToken
  }
}
```

## Delete basic messages

Delete multiple basic messages. The operation returns an array of `messageIds` which were successfully deleted.

```graphql
mutation MyMutation {
  deleteBasicMessages(
    messageIds: ["ebf7dbd4-38e0-4109-ad44-aa0c06a4a8c7", "f133e282-8817-4959-ac16-39cba8954b4e"]
  ) {
    messageIds
  }
}
```

Alternatively, delete all basic messages for a specified connection.

```graphql
mutation MyMutation {
  deleteBasicMessagesForConnection(
    connectionId: "001fe530-3562-41d6-a631-3df9c7ae1f38"
  )
}
```


# DIDs

Create and manage DIDs owned by the cloud agent

`Did`s are a core component of the Cloud Agent, they are the identifiers that the cloud agent uses when engaging in DI operations (e.g. issuing VCs).

## Create a DID

The Cloud Agent service supports multiple DID types (DID methods) to be created. All DIDs are created with the `createDid` mutation, with different inputs depending on the DID method. A private human readable and searchable `alias` can be assigned to DIDs for record-keeping purposes.

### Cheqd DID

`did:cheqd` DIDs can be created by the cloud agent and written permanently to the cheqd ledger as a production-ready DID. The `network` parameter determines whether the DID is created on testnet or mainnet.

```graphql
mutation MyMutation {
  createDid(
    input: {
      alias: "MyCheqdDID"
      createCheqdDidOptions: {
        network: TESTNET
      }
    }
  ) {
    did
    alias
    methodData {
      ... on CheqdDidMethodData {
        network
      }
    }
    createdTimestamp
    primary
  }
}
```

### Key DID

Alternatively, `did:key` DIDs are a basic no-cost DID that the cloud agent can create, ideal for development. The `keyType` parameter determines which cryptographic key type is used for the DID.

```graphql
mutation MyMutation {
  createDid(
    input: {
      alias: "MyKeyDID"
      createKeyDidOptions: {
        keyType: ED25519
      }
    }
  ) {
    did
    alias
    methodData {
      ... on KeyDidMethodData {
        keyType
      }
    }
    createdTimestamp
    primary
  }
}
```

## Set a primary DID

A Cloud Agent DID can be set the 'primary' DID for the agent, unsetting any existing primary DID. Setting a primary DID is purely for referential and record-keeping purposes when interacting with the Cloud Agent.

```graphql
mutation MyMutation {
  setPrimaryDid(
    did: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
  ) {
    did
    alias
    primary
    modifiedTimestamp
  }
}
```

## Update a DID

A DID's metadata, including its alias and archived status can be updated.

```graphql
mutation MyMutation {
  updateDid(
    did: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
    input: {
      updateAlias: "UpdatedAlias"
      updatedArchived: false
    }
  ) {
    did
    alias
    archived
    modifiedTimestamp
  }
}
```

## View DIDs

DIDs created by the cloud agent can be listed, using pagination and filtering options. This query returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  dids(
    page: {
      nextToken: null
    }
    filter: {
      methods: [KEY, CHEQD]
      archived: false
    }
  ) {
    items {
      did
      alias
      methodData {
        ... on KeyDidMethodData {
          keyType
        }
        ... on CheqdDidMethodData {
          network
        }
      }
      primary
      createdTimestamp
    }
    nextToken
  }
}
```

Alternatively, a single DID can be viewed, referenced by its unique identifier.

```graphql
query MyQuery {
  did(
    did: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
  ) {
    did
    alias
    methodData {
      ... on KeyDidMethodData {
        keyType
      }
    }
    primary
    archived
    createdTimestamp
  }
}
```

The current primary DID for the agent can be viewed with a standalone query.

```graphql
query MyQuery {
  primaryDid {
    did
    alias
    methodData {
      ... on KeyDidMethodData {
        keyType
      }
      ... on CheqdDidMethodData {
        network
      }
    }
    primary
    createdTimestamp
  }
}
```


# Credentials

Manage held credentials

A `Credential` represents a verifiable credential which has been issued to the cloud agent via a [credential exchange](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/aries-interop-profile-aip/credential-exchanges), and is currently held by the cloud agent. A `Credential` is a GraphQL union type, so must be resolved to either `AnoncredsCredential` or `W3CCredential`.

Note that credentials which have been *issued by* this cloud agent to a foreign agent are instead found on `AIPCredentialExchange.messages.issued`.

## How to use credentials

Held credentials are primarily used for presenting cryptographically verifiable claims in proof exchanges. See the section on [proof exchanges](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/aries-interop-profile-aip/proof-exchanges) for more information.

## View credentials

View held credentials, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  credentials(
    page: {
      nextToken: null
    }
    filter: {}
  ) {
    items {
      __typename
      ... on AnoncredsCredential {
        id
        attributes {
          name
          value
        }
        credDefId
        credDef {
          schemaId
        }
      }
      ... on W3CCredential {
        id
        credentialJson
      }
    }
    nextToken
  }
}
```

Alternatively, view a single credential, referenced by its unique ID.

```graphql
query MyQuery {
  credential(
    credentialId: "14eb376d-2b9c-4c30-b40f-ee3ec8250559"
  ) {
    __typename
    ... on AnoncredsCredential {
      id
      attributes {
        name
        value
      }
      credDefId
      credDef {
        schemaId
      }
    }
    ... on W3CCredential {
      id
      credentialJson
    }
  }
}
```

## Delete a credential

Delete a held credential.

```graphql
mutation MyMutation {
  deleteCredential(
    credentialId: "14eb376d-2b9c-4c30-b40f-ee3ec8250559"
  )
}
```


# Anoncreds Credentials

Manage held Anoncreds verifiable credentials

An `AnoncredsCredential` is a member of the `Credential` union type, representing a credential which follows the Anoncreds specification. Anoncreds credentials can be managed using the same queries and mutations as for any credential.

Each Anoncreds credential is associated with a [credential definition](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials/credential-definitions), which in turn is associated with a [schema](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials/schemas).

## View Anoncreds credentials

View held Anoncreds credentials, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

The `AnoncredsCredential` type provides the option to resolve its associated `AnoncredsCredDef` and `AnoncredsSchema` within a single operation.

```graphql
query MyQuery {
  credentials(
    page: {
      nextToken: null
    }
    filter: {
      credentialFormats: [ANONCREDS]
    }
  ) {
    items {
      __typename
      ... on AnoncredsCredential {
        id
        attributes {
          name
          value
        }
        credDefId
        credDef {
          schemaId
          schema {
            name
            version
          }
          tag
        }
      }
    }
    nextToken
  }
}
```

Alternatively, view a single Anoncreds credential, referenced by its unique ID.

```graphql
query MyQuery {
  credential(
    credentialId: "14eb376d-2b9c-4c30-b40f-ee3ec8250559"
  ) {
    __typename
    ... on AnoncredsCredential {
      id
      attributes {
        name
        value
      }
      credDefId
      credDef {
        schemaId
        schema {
          name
          version
        }
        tag
      }
    }
  }
}
```

## Delete an Anoncreds credential

Delete a held Anoncreds credential.

```graphql
mutation MyMutation {
  deleteCredential(
    credentialId: "14eb376d-2b9c-4c30-b40f-ee3ec8250559"
  )
}
```


# Schemas

Manage Anoncreds schemas created by the cloud agent

An Anoncreds schema, represented by `AnoncredsSchema`, defines a set of attributes which form what is essentially a credential template.

Anoncreds schemas are written to the ledger, and therefore cannot be modified or deleted once created. The schema `version` is intended as a mechanism to write an updated version of a schema to the ledger, however this has no effect on versions already written to the ledger, nor on credentials which are already issued using the schema.

## Create an Anoncreds schema

The cloud agent creates a new `AnoncredsSchema` and writes it permanently to the ledger. The `attributeNames` param determines the set of attributes which must appear on credentials issued using this schema.

```graphql
mutation MyMutation {
  createAnoncredsSchema(
    args: {
      name: "MySchema"
      version: "1.0"
      attributeNames: ["Firstname", "Lastname", "DOB"]
    }
  ) {
    id
  }
}
```

## View Anoncreds schemas

View Anoncreds schemas which are created by the cloud agent, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  anoncredsSchemas(
    page: {
      nextToken: null
    }
    filter: {}
  ) {
    items {
      id
      name
      version
      attributeNames
    }
    nextToken
  }
}
```

Alternatively, view a single Anoncreds schema, referenced by its unique ID.

```graphql
query MyQuery {
  anoncredsSchema(
    schemaId: "Hoh1tsuYFJxqsuJ5vVjxuL:2:MySchema:1.0"
  ) {
    id
    name
    version
    attributeNames
  }
}
```


# Credential Definitions

Manage Anoncreds credential definitions created by the cloud agent

An Anoncreds credential definition, represented by `AnoncredsCredDef`, enables the cloud agent to issue credentials from a specific Anoncreds [schema](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/anoncreds-credentials/schemas).

Anoncreds credential definitions are written to the ledger, and therefore cannot be modified or deleted once created. However, it is possible for multiple credential definitions to reference a single schema.

## Create an Anoncreds credential definition

The cloud agent creates a new `AnoncredsCredDef` and writes it permanently to the ledger. The `schemaId` param references an Anoncreds schema which must be already written to the ledger. The `isRevocable` param determines whether credentials issued using this credential definition can be revoked.

```graphql
mutation MyMutation {
  createAnoncredsCredDef(
    args: {
      schemaId: "Hoh1tsuYFJxqsuJ5vVjxuL:2:MySchema:1.0"
      tag: "MyCredDefTag"
      isRevocable: false
    }
  ) {
    id
  }
}
```

## View Anoncreds credential definitions

View Anoncreds credential definitions which are created by the cloud agent, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

The `AnoncredsCredDef` type provides the option to resolve its associated `AnoncredsSchema` within a single operation.

```graphql
query MyQuery {
  anoncredsCredDefs(
    page: {
      nextToken: null
    }
    filter: {}
  ) {
    items {
      id
      schemaId
      schema {
        name
        version
        attributeNames
      }
      tag
      isRevocable
    }
    nextToken
  }
}
```

Alternatively, view a single Anoncreds credential definition, referenced by its unique ID.

```graphql
query MyQuery {
  anoncredsCredDef(
    credDefId: "Hoh1tsuYFJxqsuJ5vVjxuL:3:CL:3050:MyCredDefTag"
  ) {
    id
    schemaId
    schema {
      name
      version
      attributeNames
    }
    tag
    isRevocable
  }
}
```


# W3C Credentials

Manage held W3C verifiable credentials

A `W3CCredential` is a member of the `Credential` union type, representing a credential which follows the W3C Verifiable Credentials Data Model 1.1 specification. W3C credentials can be managed using the same queries and mutations as for any credential.

## View W3C credentials

View held W3C credentials, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  credentials(
    page: {
      nextToken: null
    }
    filter: {
      credentialFormats: [W3C]
    }
  ) {
    items {
      __typename
      ... on W3CCredential {
        id
        credentialJson
      }
    }
    nextToken
  }
}
```

Alternatively, view a single W3C credential, referenced by its unique ID.

```graphql
query MyQuery {
  credential(
    credentialId: "19d8917d9fd44d09b93eb5a1a71282dd"
  ) {
    __typename
    ... on W3CCredential {
      id
      credentialJson
    }
  }
}
```

## Delete a W3C credential

Delete a held W3C credential.

```graphql
mutation MyMutation {
  deleteCredential(
    credentialId: "19d8917d9fd44d09b93eb5a1a71282dd"
  )
}
```


# Credential Status Lists

Manage and update the status (revoked, suspended) of W3C credentials issued by the Cloud Agent Service.

Credential status management allows issuers to track and update the revocation and suspension status of issued W3C credentials. The Cloud Agent uses [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/) to efficiently manage credential statuses.

### Create a Status List Definition

A status list definition must be created before credentials can be assigned to status lists. The definition specifies which purposes (revocation and/or suspension) the status lists will support and which issuer [DID](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/dids) will manage them. A status list should be created with a unique identifier chosen by the API consumer.

```graphql
mutation MyMutation {
  createStatusListDefinition(
    input: {
      identifier: "my-status-list-def"
      purposes: [REVOCATION, SUSPENSION]
      issuerDid: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
    }
  ) {
    identifier
    purposes
    issuerDid
    lists {
      listNumber
      purpose
    }
    createdTimestamp
  }
}
```

### Manually Assign a Credential to Status Lists

Before issuing a credential, assign it to status lists to enable revocation and suspension tracking. This returns [W3C credential status entry objects](https://www.w3.org/TR/vc-bitstring-status-list/#bitstringstatuslistentry) that should be embedded in the credential before issuing.

{% hint style="info" %}
Alternatively this credential assignment to a status list can be handled automatically when issuing credential: [Standalone VC Issue & Verify](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/standalone-vc-issue-and-verify#embed-credential-status).
{% endhint %}

```graphql
mutation MyMutation {
  assignCredentialToStatusList(
    input: {
      newEntry: {
        credentialId: "urn:uuid:12345678-1234-1234-1234-123456789abc"
        statusListDefinitionIdentifier: "my-status-list-def"
      }
    }
  ) {
    w3cCredentialStatusJsons
  }
}
```

### Update Credential Statuses

Update the revocation and/or suspension status of one or more credentials. The `publishImmediately` flag determines whether the status lists are published automatically after the update. The updated status will not be available publicly until it is published.

```graphql
mutation MyMutation {
  updateCredentialStatuses(
    input: {
      updates: [
        {
          entry: {
            credentialId: "urn:uuid:12345678-1234-1234-1234-123456789abc"
            statusListDefinitionIdentifier: "my-status-list-def"
          }
          revoked: true
          suspended: false
        }
      ]
      publishImmediately: true
    }
  ) {
    success {
      credentialId
      statusListDefinitionIdentifier
    }
    failed {
      entry {
        credentialId
        statusListDefinitionIdentifier
      }
      error
    }
  }
}
```

### Publish Status Lists

Publish status lists to make credential status updates publicly available. This operation publishes all status lists associated with a status list definition.

```graphql
mutation MyMutation {
  publishStatusList(
    input: {
      identifier: "my-status-list-def"
    }
  ) {
    success {
      listNumber
      purpose
    }
    failed {
      list {
        listNumber
        purpose
      }
      error
    }
  }
}
```

### View Status List Definitions

View all status list definitions managed by the agent.

```graphql
query MyQuery {
  statusListDefinitions {
    items {
      identifier
      purposes
      issuerDid
      lists {
        listNumber
        purpose
      }
      createdTimestamp
    }
  }
}
```

Alternatively, view a single status list definition by its unique identifier.

```graphql
query MyQuery {
  statusListDefinition(
    identifier: "my-status-list-def"
  ) {
    identifier
    purposes
    issuerDid
    lists {
      listNumber
      purpose
    }
    createdTimestamp
  }
}
```

### Check Credential Statuses

Check the current revocation and suspension status of one or more credentials. This returns the internally recorded status entries, the external public status may not match if the changes have not been published.

```graphql
query MyQuery {
  credentialStatuses(
    input: {
      entries: [
        {
          credentialId: "urn:uuid:12345678-1234-1234-1234-123456789abc"
          statusListDefinitionIdentifier: "my-status-list-def"
        }
      ]
    }
  ) {
    statuses {
      entry {
        credentialId
        statusListDefinitionIdentifier
      }
      revoked
      suspended
    }
  }
}
```


# Standalone VC Issue & Verify

The Cloud Agent Service supports the ability to sign and verify credentials with standalone APIs.

For certain use cases, it can be desirable to issue VCs independent of [DI Credential Exchange protocols](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/aries-interop-profile-aip/credential-exchanges).

## Sign a W3C Verifiable Credential

The Cloud Agent can be used to sign a W3C Verifiable Credential using a specified cryptographic securing mechanism. The `signer` parameter accepts either a DID or a specific verification method.

The mutation accepts a `credentialJson` , which must be a valid JSON stringified [W3C VCDM](https://www.w3.org/TR/vc-data-model-2.0/) (V1.1 or V2.0).

The output includes both the finalised `credentialJson` (stringified JSON W3C VCDM object) that was ultimately signed, and the `securedCredentialJson` that is cryptographically signed by the agent (stringified JSON data using the specified securingMechanism.)

### Linked Data Proof

Sign a credential using the Linked Data Proof securing mechanism, which produces a credential in the `ldp_vc` format - i.e. an embedded JSON-LD `proof` object.

```graphql
mutation MyMutation {
  signCredential(
    input: {
      signer: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
      format: {
        w3c: {
          credentialJson: "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"type\":[\"VerifiableCredential\"],\"issuer\":\"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK\",\"issuanceDate\":\"2024-01-01T00:00:00Z\",\"credentialSubject\":{\"id\":\"did:example:123\",\"name\":\"Alice\"}}"
          securingMechanism: LINKED_DATA_PROOF
        }
      }
    }
  ) {
    ... on SignW3CCredentialResult {
      credentialJson
      securedCredentialJson
    }
  }
}
```

### SD-JWT

Sign a credential using the Selective Disclosure JWT securing mechanism, which produces a credential in the `vc+sd-jwt` format - i.e. an SD-JWT encoded W3C VCDM.

```graphql
mutation MyMutation {
  signCredential(
    input: {
      signer: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
      format: {
        w3c: {
          credentialJson: "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"type\":[\"VerifiableCredential\"],\"issuer\":\"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK\",\"issuanceDate\":\"2024-01-01T00:00:00Z\",\"credentialSubject\":{\"id\":\"did:example:123\",\"name\":\"Alice\"}}"
          securingMechanism: SD_JWT
        }
      }
    }
  ) {
    ... on SignW3CCredentialResult {
      credentialJson
      securedCredentialJson
    }
  }
}
```

### Embed Credential Status

The cloud agent can automatically assign and embed credential status information when signing a credential. The system will generate and insert `credentialStatus` fields into the credential, assigning it to the specified status list definition.

The given status list definition must be created in accordance with [Credential Status Lists](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/credential-status-lists).

The mutation will also return the `createdStatusListEntry` that was created, and can subsequently be managed via [Credential Status Lists](/guides/decentralized-identity/cloud-agent/cloud-agent-admin-api/credentials/credential-status-lists).

```graphql
mutation MyMutation {
  signCredential(
    input: {
      signer: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
      format: {
        w3c: {
          credentialJson: "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"type\":[\"VerifiableCredential\"],\"issuer\":\"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK\",\"issuanceDate\":\"2024-01-01T00:00:00Z\",\"credentialSubject\":{\"id\":\"did:example:123\",\"name\":\"Alice\"}}"
          securingMechanism: LINKED_DATA_PROOF
          embedCredentialStatus: {
            statusListDefinitionIdentifier: "my-status-list-def"
          }
        }
      }
    }
  ) {
    ... on SignW3CCredentialResult {
      credentialJson
      securedCredentialJson
      createdStatusListEntry {
        credentialId
        statusListDefinitionIdentifier
      }
    }
  }
}
```

## Verify a Verifiable Credential

The Cloud Agent can also be used to verify a secured (signed) credential of various formats and signature schemes, including with credential status checks (i.e. for revocation).

To a verify a VC and check its revocation status (if there is one), the `verifyCredential` mutation can be called, providing the JSON stringified secured VC as the `securedCredentialJson` value.&#x20;

The mutation will return the payload that it decoded from the secured payload, whether the issuer's signature was valid, and if applicable, the credential statuses. If these checks fail to execute, an error will be given with the `reason` field.

{% hint style="info" %}
The input `securedCredentialJson` should be the JSON stringified version of the secured credential payload, which can take multiple formats depending on how it was secured.&#x20;

For instance, an \[SD-]JWT secured VC would take a JSON stringified form such as: `"\"eyJ0...\""` , whereas a Linked Data Proof secured W3C VC would take a JSON stringified form such as: `"{ \"@context\": [...], ... }"`.
{% endhint %}

```graphql
mutation MyMutation {
  verifyCredential(input:{
    securedCredentialJson: "\"eyJ0eXAiOiAidmMrc2Qtand0IiwgImFsZyI6ICJFZERTQSIsICJraWQiOiAiZGlkOmtleTp6Nk1rb2pkNFhaQnpmejlpaWFBVGRZSGdZMnptdENQVmJUajdHeHNqdWtZbmtIdzcjejZNa29qZDRYWkJ6Zno5aWlhQVRkWUhnWTJ6bXRDUFZiVGo3R3hzanVrWW5rSHc3In0.eyJfc2QiOiBbIjZUZXljeElvR1dsYzZKdXZEdl9wNmF2U1hWWGE3U1FFUVNzajAtQ3N6alEiLCAiRG9wa1laRWk4R0w4YXlZdGZVOGtqZ2dUTjlqQnFESUlXZDMzYnU2UW1ibyIsICJMNDFuNjZWQmltTENIZmlSSTlNbFV6UkgtSF9IOEpvV0QwUmp3Rms1bTdJIiwgImQ4aWZ4eXUwUjZWcm1ZblN4Rmt1WjB5aktaOTV5OEZ4a1VtYWQwWFQ0WjgiLCAiak9jUk0yekdSdEY0NU9qMWE5U1pSVXNIVHR3eVBrbnEtcHp0Um95WWZvbyJdLCAiaXNzdWFuY2VEYXRlIjogIjIwMjUtMTItMDJUMDA6Mjg6NDIuMDAwWiIsICJpc3N1ZXIiOiAiZGlkOmtleTp6Nk1rb2pkNFhaQnpmejlpaWFBVGRZSGdZMnptdENQVmJUajdHeHNqdWtZbmtIdzciLCAiX3NkX2FsZyI6ICJzaGEtMjU2In0.NR5jlRC8kKIs1mGqzeh1bxR1NDPdI_Zxm46nqyYlCVZFQicgHb7aF0YHbyXaFLSp8K5Uh0WyCNAZWT4uVYy1AQ~WyJIVG52NmpJVjdpSUdhU2JQNHVWZjBRIiwgIlZlcmlmaWFibGVDcmVkZW50aWFsIl0~WyJaRHBpd1IzSWhSR1c3Q1BRMjg4aUx3IiwgIlBlcm1hbmVudFJlc2lkZW50Il0~WyJQaEIzZ0d1aGRHbW5ETGlRMTBNaGVRIiwgInR5cGUiLCBbeyIuLi4iOiAiNWN2ZEQwc1VWeU5DcTNFYV8xZTVHOWdVY0tGcnFaT2M3S2JYYlZ3NXp0TSJ9LCB7Ii4uLiI6ICJKSU9RVEc3S3UwX2ZNYXRuTlVQZkRDdDl0VG90amF1bENvZDVEY3R1YkZrIn1dXQ~WyJoSjFtSmxsSlowZF83QjlTWUlSbG1BIiwgImlkIiwgInVybjp1dWlkOjgzMjkwMjVmLTEwYzMtNGEwMC04YWFiLTk3OGMzOWEyNDIyZSJd~WyJPUlJZdkJZeW9RTnBWSG5GUFAtbzhRIiwgIlBlcm1hbmVudFJlc2lkZW50Il0~WyJOQ3JoSmVILTU4SDF4aE5Qc1VXLUJBIiwgInR5cGUiLCBbeyIuLi4iOiAiT0JpeGxfOG5GWXZXdDNNN0ExaXJoU3ZzcGZKMFZzcHRxdll1NVpZeFZXUSJ9XV0~WyJGSDFaVGZtbFVSVE5wTER4WS1jX3BRIiwgImdpdmVuTmFtZSIsICJKb2huIl0~WyJwX20zTmpDWnJLMFpmc3ZMeXVnQVFRIiwgImZhbWlseU5hbWUiLCAiRG9lIl0~WyJsakZBYTJmclVUUDBieEZLYXdHYW13IiwgImNyZWRlbnRpYWxTdWJqZWN0IiwgeyJfc2QiOiBbIktPZXJ3ck01dVFnaXRNTHlwUzA5MThtS0ozYWhhR1RKLTE0TlYwSTE2ekUiLCAiUXI5bm9JSTFFNkF1eWpqNi14aFdzc1lxSDdDM1M1LXRzZDZpV2ZwaWJRYyIsICJ2djRzcFhxT09WTnVJV2QtYlhDblVMOUlnOTlLTlBkY29mdUFLSVhQUTlNIl19XQ~WyJNQ3JiOVVQMG55LXVCLWF2TEIxbFdnIiwgInR5cGUiLCAiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Il0~WyJDdTBuaHlFaEJqbkQ1UXVvenhNTEJBIiwgInN0YXR1c1B1cnBvc2UiLCAic3VzcGVuc2lvbiJd~WyJWbi1LVXJNZ2l3OXowMUI0MmVvdUhRIiwgInN0YXR1c0xpc3RJbmRleCIsICI4OTM2MyJd~WyJnU25sMk1ubG5EUGY0RFFTakJMVE93IiwgInN0YXR1c0xpc3RDcmVkZW50aWFsIiwgImh0dHBzOi8vc3RhdHVzbGlzdC5kaS1kZXYuZGktdGVhbS5kZXYuc3Vkb3BsYXRmb3JtLmNvbS9zdGF0dXNfbGlzdC80OCJd~WyIwcjJ2VzZtOHBPOUcya2xjYlgzOHNRIiwgImlkIiwgImh0dHBzOi8vc3RhdHVzbGlzdC5kaS1kZXYuZGktdGVhbS5kZXYuc3Vkb3BsYXRmb3JtLmNvbS9zdGF0dXNfbGlzdC80OCM4OTM2MyJd~WyJyYUkxNGhiTFRmRXlBZ0FKck1nTTR3IiwgInR5cGUiLCAiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Il0~WyJuSWFVc19IM01KSXplRlNQbW1HVnNBIiwgInN0YXR1c1B1cnBvc2UiLCAicmV2b2NhdGlvbiJd~WyJiRjE3M0FhTnBjQ0REVnJwZUJpTUZBIiwgInN0YXR1c0xpc3RJbmRleCIsICI4MjExMiJd~WyJXZE05NTBqRm1FWE5zTGpZWXN4b1JRIiwgInN0YXR1c0xpc3RDcmVkZW50aWFsIiwgImh0dHBzOi8vc3RhdHVzbGlzdC5kaS1kZXYuZGktdGVhbS5kZXYuc3Vkb3BsYXRmb3JtLmNvbS9zdGF0dXNfbGlzdC80NiJd~WyIxNlItY0JFVnFkeUItNTdtNHl0RFB3IiwgImlkIiwgImh0dHBzOi8vc3RhdHVzbGlzdC5kaS1kZXYuZGktdGVhbS5kZXYuc3Vkb3BsYXRmb3JtLmNvbS9zdGF0dXNfbGlzdC80NiM4MjExMiJd~WyJhcEkyeERONTZZT2tSczlTSi1FanhnIiwgImNyZWRlbnRpYWxTdGF0dXMiLCBbeyJfc2QiOiBbIjdqZjcxMkdxOW41MHVFOE9YVjZvYkJfN3FIa1NuMGR3ZXBybGlqc2VjeTgiLCAiQXU4dVVPNDlqdE4tM3poU0pseUdWYW9xdHJudlZHR2xIT01ZTjVHZ1IxayIsICJiRGJSbGlfVXBJeWlLZHhjVUNqQTlqUjlqT3NZM0ZoeWRWOHdHWGJTV0dnIiwgImRzWUZHY1JlVE1hTXgzVjZKT1lUTkhXa1RZQnk0bWVYYkFhenZFNzJDU2MiLCAidVBKdm1ZYVZMTDBtcExXVmViTTZLbF9COVVDQ0I1cER1bTRtNWZxYmZhdyJdfSwgeyJfc2QiOiBbIk5PUjEwcTFHVC1OVFFmRXZXcGxKbmhXQ1UxOW9yRVh0TE83WUl4V0VaYVkiLCAiYk5Kc2YtR2dVNmcwVFhubzFrN2VHMFh1azRoaGZmT1VCTzNyemwtQVBXWSIsICJoNjRKSHR0T290U1JDQmwtTHJCdmt3V1NEMndqa1JHX3lPdy05NXBMdWZNIiwgImlJZjVaV05YVlhtVnBqZ1IzcWtEOFctZzFfTjl1SnJpMFlLZ1BzY2RoWHMiLCAieUtiSmROZTFxMmRKanBzVEhtMGwxd1VQUHhfRk9fUWNPeVVhb3FFeHY2YyJdfV1d~WyI4VVJWLUVWOE5MZXcxM0o5Ylhfa3ZnIiwgImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0~WyJDN2hiY0c4TVNlUmkyOWh2MHVPaGxRIiwgImh0dHBzOi8vdzNpZC5vcmcvY2l0aXplbnNoaXAvdjEiXQ~WyJKUHp2SWY5QmphbU1FbmJLT0pOXzlnIiwgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy9zdGF0dXMvdjEiXQ~WyJydEllOU5oOU90VjJOS2Q4d3dLQlZBIiwgIkBjb250ZXh0IiwgW3siLi4uIjogIjRBTjdfVzVadU53VVJTSm1NSzQ1azAwNmFVT3JwaHBSSC1ZVTRlbzBNT3MifSwgeyIuLi4iOiAieTBTZHlBdFVSdi1vUTVwZmhrS1k4aG56Qk84QzJoTnhTVkNueVhUMUxVWSJ9LCB7Ii4uLiI6ICJXVEZBbUZNVVZKeXZmSEx5SlBGN0hBdFNYemNJcDlIQTFrOUxtUWxSSkRRIn1dXQ~\""
  }) {
    format {
      ... on VerifyW3CCredentialFormatResult {
        credentialJson
        securingMechanism
      }
    }
    signatureVerified {
      value
      reason
    }
    statusChecks {
      revoked {
        value
        reason
    	}
    }
  }
}
```


# Audit Logs

View the cloud agent admin API audit trail

Audit logs are created automatically upon each call to the cloud agent admin API. Each audit log is of the `AuditLog` type, recording information about the call such as the authenticated user ID, the operation, arguments, return data and success status, and the user agent and HTTP response status.

## View audit logs

View audit logs, with pagination and filtering options. Returns a `nextToken` which can be passed into the same query in order to fetch subsequent pages.

```graphql
query MyQuery {
  auditLogs(
    page: {
      nextToken: null
    }
    filter: {}
  ) {
    items {
      requestId
      userId
      timestamp
      operationId
      operationArgs
      operationResultData
      isSuccess
      userAgent
      httpStatus
    }
    nextToken
  }
}
```


# API Schema

The GraphQL schema for the Sudo Platform Cloud Agent admin API

```graphql
#
# Data types for Connection Exchanges / Invitations
#

"""
Aries Interop Profile (AIP) Connection Exchange record data type
"""
type AIPConnectionExchange {
  "The unique identifier for the connection exchange."
  id: ID!
  "The role of this agent in the connection exchange."
  myRole: AIPConnExRole!
  "The current state of the connection exchange."
  state: AIPConnExState!
  "The locally assigned alias for the connection."
  alias: String!
  "The label provided by the connection's peer."
  theirLabel: String
  """
  The invitation associated with the connection exchange.
  If myRole=INVITER, then the invitation was created by this agent.
  If myRole=INVITEE, then the invitation was received by this agent.
  """
  invitation: AIPConnectionInvitation
  "The datetime when the connection exchange was created, in ISO 8601 format."
  createdTimestamp: String!
  "The datetime when the connection exchange was last updated, in ISO 8601 format."
  modifiedTimestamp: String!
}

"""
The role of an agent in an AIP connection exchange.
"""
enum AIPConnExRole {
  INVITER
  INVITEE
}

"""
The state of an AIP connection exchange.
"""
enum AIPConnExState {
  INVITATION
  REQUEST
  RESPONSE
  """
  Terminal state for a successful, "complete" connection exchange.
  """
  DONE
  """
  Terminal state for an unsuccessful connection exchange.
  """
  ABANDONED
}

"""
A connection invitation either created by this agent, or received by this agent. If created by this agent,
the invitation must be shared out-of-band.
"""
type AIPConnectionInvitation {
  "An ID for the connection invitation."
  messageId: ID!
  """
  A label provided by the inviter, to help the invitee recognize them. Note that this is not trusted data.
  If the encapsulating AIPConnectionExchange object has myRole=INVITER, then label represents 'myLabel'.
  If the encapsulating AIPConnectionExchange object has myRole=INVITEE, then label represents 'theirLabel'.
  """
  label: String!
  "The endpoint where the invitee can establish contact with the inviter."
  serviceEndpoint: String!
  """
  The raw invitation which contains all the details on how to contact the inviter.
  This must be delivered to the invitee out-of-band.
  """
  invitationUrl: String!
}

"""
Optional filters for the 'aipConnectionExchanges' query. The filters are AND'd together
"""
input AIPConnectionExchangesFilter {
  "Filter connection exchanges by ID."
  connExIds: [ID!]
  "Filter by the alias of the connection associated with the exchange. Returns exact substring matches."
  alias: String
  """
  Filter by the alias of the connection associated with the exchange.
  Returns all connection exchanges in order of similarity to the search value, with the best matches listed first.
  """
  aliasFuzzySearch: String
  "Filter by the role this agent plays in the connection exchange."
  myConnExRole: AIPConnExRole
  "Filter by the current state of the connection exchange."
  connExStates: [AIPConnExState!]
  """
  Sort the results of this query by the 'modifiedTimestamp' of the connection exchanges.
  Default is 'DESCENDING' so that the latest update will appear first.
  """
  sortOrder: SortOrder
}

"""
Paged result container of the 'aipConnectionExchanges' query
"""
type AIPConnectionExchangesResult {
  "The result items"
  items: [AIPConnectionExchange!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

"""
Input for the 'createAipConnectionInvitation' mutation.
"""
input CreateAipConnectionInvitationInput {
  "The locally assigned alias for the connection."
  alias: String!
  """
  The label to put in the created invitation, to help the invitee recognize this agent.
  If not provided, the system default will be used.
  """
  myLabel: String
}

"""
Input for the 'acceptAipConnectionInvitation' mutation.
"""
input AcceptAipConnectionInvitationInput {
  "The raw invitation which contains all the details on how to contact the inviter."
  invitationUrl: String!
  """
  The locally assigned alias for the connection.
  If not provided, the label inside the invitation will be used as the connection alias.
  """
  alias: String
  """
  The label to pass back to the inviter, to help the inviter recognize this agent.
  If not provided, the system default will be used.
  """
  myLabel: String
}

#
# Data types for Connections
#

"""
Connection record data type
"""
type Connection {
  "Unique identifier of the connection"
  id: ID!
  "The locally assigned alias for the connection"
  alias: String!
  "The label provided by the connection's peer"
  theirLabel: String!
  "The datetime when this connection was created, in ISO 8601 format"
  createdTimestamp: String!
  "The datetime when this connection was last updated, in ISO 8601 format"
  modifiedTimestamp: String!
  "Basic messages associated with the connection"
  basicMessages(page: Page!): BasicMessagesResult!
}

"""
Optional filters for the 'connections' query. The filters are AND'd together
"""
input ConnectionsFilter {
  "Filter connections by ID."
  connectionIds: [ID!]
  "Filter by the alias of the connection. Returns exact substring matches."
  alias: String
  """
  Filter by the alias of the connection.
  Returns all connections in order of similarity to the search value, with the best matches listed first.
  """
  aliasFuzzySearch: String
  """
  Filter connections by their basic messages count
   - If true, only include connections that have >= 1 message
   - If false, only include connections that have 0 messages
   - If null or not supplied (default), include all connections
  """
  hasBasicMessages: Boolean = null
  """
  Sort the results of this query by the 'modifiedTimestamp' of the connections.
  Default is 'DESCENDING' so that the latest update will appear first.
  """
  sortOrder: SortOrder
}

"""
Result type of the 'connections' query
"""
type ConnectionsResult {
  "The result items"
  items: [Connection!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

#
# Data types for Basic Messages
#

"""
Basic message record data type
"""
type BasicMessage {
  "Unique ID of the message"
  id: ID!
  "ID of the connection this message is associated with"
  connectionId: ID!
  "The message content"
  content: String!
  """
  Aggregated time stamp of the message in ISO 8601 format.
  The semantics of this timestamp relate to the message direction.
  - For incoming messages this is the time the service receives the message
  - For outgoing messages this is the time the service sends the message
  """
  timestamp: String
  "Direction of the message"
  direction: BasicMessageDirection!
}

"""
Send direction of a basic message
"""
enum BasicMessageDirection {
  INBOUND
  OUTBOUND
}

"""
Include filter for the basic messages query
"""
input BasicMessagesFilter {
  "Filter on a specific connection ID"
  connectionId: ID!
}

"""
Paged result container of the 'basicMessages' query
"""
type BasicMessagesResult {
  "The result items"
  items: [BasicMessage!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

"""
Paged result container of the 'broadcastBasicMessage' mutation
"""
type BroadcastBasicMessageResult {
  "The connection IDs of all peers that the message was successfully sent to"
  connectionIds: [ID!]!
}

"""
Result from the delete basic message mutation
"""
type DeleteBasicMessagesResult {
  "The message IDs of all items that were deleted"
  messageIds: [ID!]!
}

#
# Data types for Anoncreds Schemas
#

"""
Anoncreds Schema record data type
"""
type AnoncredsSchema {
  "Unique identifier of the schema, as found on the ledger"
  id: ID!
  """
  The name of the schema. An endorser/issuer can only publish one schema
  with this unique 'name' and 'version' combination
  """
  name: String!
  """
  The version of the schema, using the industry-standard 'semantic versioning' format.
  An endorser/issuer can only publish one schema with this unique 'name' and 'version' combination
  """
  version: String!
  """
  Names of the attributes defined by this schema.
  Credentials issued from this schema will be assigned a value for each of these attributes
  """
  attributeNames: [String!]!
}

"""
Optional filters for the 'anoncredsSchemas' query. The filters are AND'd together
"""
input AnoncredsSchemasFilter {
  "Filter schemas by ID. Must be an exact match"
  schemaId: ID
  "Filter schemas by name. Returns exact substring matches."
  schemaName: String
  "Filter schemas by version. Returns exact substring matches."
  schemaVersion: String
  """
  Filter schemas by name and version.
  Returns all schemas in order of similarity to the search value, with the best matches listed first.
  """
  schemaNameAndVersionFuzzySearch: String
}

"""
Paged result container of the 'anoncredsSchemas' query
"""
type AnoncredsSchemasResult {
  "The result items"
  items: [AnoncredsSchema!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

"""
Input parameters for the 'createAnoncredsSchema' mutation.
"""
input CreateAnoncredsSchemaArgs {
  """
  The name of the schema. An endorser/issuer can only publish one schema
  with this unique 'name' and 'version' combination
  """
  name: String!
  """
  The version of the schema, using the industry-standard 'semantic versioning' format.
  An endorser/issuer can only publish one schema with this unique 'name' and 'version' combination
  """
  version: String!
  """
  Names of the attributes defined by this schema.
  Credentials issued from this schema will be assigned a value for each of these attributes
  """
  attributeNames: [String!]!
}

#
# Data types for Anoncreds Credential Definitions (CredDefs)
#

"""
Anoncreds Credential Definition ("credDef") record data type.
A credDef associates a schema with an issuer, allowing the issuer
to create credentials from that schema.
"""
type AnoncredsCredDef {
  "Unique identifier of the credDef, as found on the ledger"
  id: ID!
  "Unique identifier of the schema associated with this credDef"
  schemaId: ID!
  """
  The schema resolved by 'schemaId'.
  Returns null if the schema could not be resolved on the ledger.
  """
  schema: AnoncredsSchema
  """
  An arbitrary string assigned to this credDef, which allows the issuer
  to publish multiple credDefs using the same schema.
  """
  tag: String!
  "If true, then credentials created from this credDef can be revoked"
  isRevocable: Boolean!
}

"""
Optional filters for the 'anoncredsCredDefs' query. The filters are AND'd together
"""
input AnoncredsCredDefsFilter {
  "Filter credDefs by ID. Must be an exact match"
  credDefId: ID
  "Filter credDefs by schema ID. Must be an exact match"
  schemaId: ID
  "Filter credDefs by schema name. Returns exact substring matches."
  schemaName: String
  "Filter credDefs by schema version. Returns exact substring matches."
  schemaVersion: String
  "Filter credDefs by tag. Returns exact substring matches."
  credDefTag: String
  """
  Filter credDefs by tag.
  Returns all credDefs in order of similarity to the search value, with the best matches listed first.
  """
  credDefTagFuzzySearch: String
}

"""
Paged result container of the 'anoncredsCredDefs' query
"""
type AnoncredsCredDefsResult {
  "The result items"
  items: [AnoncredsCredDef!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

"""
Input parameters for the 'createAnoncredsCredDef' mutation.
"""
input CreateAnoncredsCredDefArgs {
  "Unique identifier of the schema associated with the credDef"
  schemaId: ID!
  """
  An arbitrary string assigned to the credDef, which allows the issuer
  to publish multiple credDefs using the same schema.
  """
  tag: String!
  "If true, then credentials created from the credDef can be revoked"
  isRevocable: Boolean!
}

#
# Data types for Wallet Credentials
#

union Credential = AnoncredsCredential | W3CCredential

"""
An Anoncreds Verifiable Credential held by this agent.
"""
type AnoncredsCredential {
  "Unique identifier for the held Anoncreds credential."
  id: ID!
  "Attributes in the credential."
  attributes: [AnoncredsCredAttribute!]!
  "If null, the credential is not revocable."
  revocationInfo: AnoncredsRevocationInfo
  "The ID of the credential definition which defines the credential."
  credDefId: ID!
  """
  For convenience.
  Resolves the credential definition referred to by 'credDefId'.
  The schema for the credential can optionally be resolved within this credential definition.
  Returns null if the credential definition could not be resolved on the ledger.
  """
  credDef: AnoncredsCredDef
  "The datetime when this credential was received, in ISO 8601 format."
  issuanceTimestamp: String!
}

"Details of an attribute in an Anoncreds credential."
type AnoncredsCredAttribute {
  "The name of the credential attribute."
  name: String!
  "The value held by the credential attribute."
  value: String!
}

"Details used to revoke a revocable Anoncreds credential."
type AnoncredsRevocationInfo {
  "The ID of the revocation registry where revocation status of the credential is recorded."
  revRegId: ID!
  "The ID for the credential in the revocation registry."
  credRevId: ID!
}

"""
A W3C Verifiable Credential held by this agent.
"""
type W3CCredential {
  "Unique identifier for the held W3C credential."
  id: ID!
  "The W3C Verifiable Credential, encoded as a stringified JSON."
  credentialJson: String!
  """
  A public identifier for the credential.
  If null, the credential has no public identifier.
  """
  publicId: ID
  "The JSON-LD documents which define the contents of the credential."
  contexts: [String!]!
  "The JSON-LD types found in 'contexts' which define the contents of the credential."
  types: [String!]!
  "The DID of the issuer of the credential."
  issuerId: ID!
  "The datetime from which the credential is valid, in ISO 8601 format."
  issuanceTimestamp: String!
  "The datetime from which the credential is invalid, in ISO 8601 format."
  expirationTimestamp: String
  """
  Each element in this array is a stringified JSON which contains the attributes for a single credential subject.
  A W3C credential may have more than one subject.
  """
  subjectJsons: [String!]!
  "Additional features which the credential supports."
  supportedFeatures: W3CSupportedFeatures!
}

"Specifies which features are supported by a W3C credential."
type W3CSupportedFeatures {
  """
  If true, then it is possible to present to a verifier a subset of the attributes found within the credential,
  without revealing all the attributes.
  """
  selectiveDisclosure: Boolean!
}

"""
Optional filters for the 'credentials' query. The filters are AND'd together
"""
input CredentialsFilter {
  "Filter by the format of the credential."
  credentialFormats: [CredentialFormat!]
  """
  Sort the results of this query by the 'issuanceTimestamp' of the credential.
  Default is 'DESCENDING' so that the latest received credential will appear first.
  """
  sortOrder: SortOrder
}

"""
Paged result container of the 'credentials' query
"""
type CredentialsResult {
  "The result items"
  items: [Credential!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

#
# Data types for Standalone VC Signing
#


"""
Input parameters for the 'signCredential' mutation.
"""
input SignCredentialInput {
  "The format-specific options for signing the credential"
  format: SignCredentialFormatOptions!
  "The signer of the VC, either a DID or Verification Method"
  signer: String!
}

"""
Format-specific options for credential signing.
"""
input SignCredentialFormatOptions {
  "W3C Verifiable Credential signing options"
  w3c: SignW3CCredentialOptions
  # FUTURE - SD-JWT VC
}

"""
Options for automatically embedding credential status information into a W3C Verifiable Credential.
When provided, the system will automatically generate and insert credentialStatus fields into the credential.
"""
input EmbedCredentialStatusOptions {
  "The identifier of the status list definition to use for embedding credential status"
  statusListDefinitionIdentifier: String!
}

"""
Options for signing W3C Verifiable Credentials.
"""
input SignW3CCredentialOptions {
  "The W3C Verifiable Credential to be signed, encoded as a stringified JSON"
  credentialJson: String!
  "The cryptographic securing mechanism to use for signing"
  securingMechanism: W3CSecuringMechanism!
  "If provided, embed the vc.credentialStatus with entries in the given status list"
  embedCredentialStatus: EmbedCredentialStatusOptions
}

"""
Cryptographic securing mechanisms for W3C Verifiable Credentials.
"""
enum W3CSecuringMechanism {
  "Linked Data Proof securing mechanism (ldp_vc / vc format)"
  LINKED_DATA_PROOF
  "Selective Disclosure JWT securing mechanism (vc+sd-jwt format)"
  SD_JWT
}

"""
Result of credential signing operation.
Returns the signed credential in the appropriate format based on the securing mechanism used.
"""
union SignCredentialResult = SignW3CCredentialResult # FUTURE - SD-JWT VC

"""
Result of signing a W3C Verifiable Credential.
Contains both the credential payload and the cryptographically secured payload.
"""
type SignW3CCredentialResult {
  "The W3C VCDM payload as a JSON string"
  credentialJson: String!
  "The cryptographically secured credential as a JSON string. For Linked Data Proof, this is a JSON object. For SD-JWT, this is a JSON string (JWT)"
  securedCredentialJson: String!
  "If any, the created status list entry."
  createdStatusListEntry: StatusEntryIdentifier
}

#
# Data types for Credential Exchanges
#

"""
Aries Interop Profile (AIP) Credential Exchange record data type
"""
type AIPCredentialExchange {
  "The unique identifier for the credential exchange."
  id: ID!
  "The version of AIP used for the credential exchange."
  aipVersion: AIPVersion!
  "The role of this agent in the credential exchange."
  myRole: AIPCredExRole!
  "The current state of the credential exchange."
  state: AIPCredExState!
  "Messages which have been sent or received during the exchange."
  messages: AIPCredExMessages!
  "The datetime when the credential exchange was created, in ISO 8601 format."
  createdTimestamp: String!
  "The datetime when the credential exchange was last updated, in ISO 8601 format."
  modifiedTimestamp: String!
  "The ID of the connection with whom this agent is performing the exchange."
  connectionId: ID!
  """
  The connection resolved by 'connectionId'.
  Returns null if the connection cannot be found, such as when the connection is deleted.
  """
  connection: Connection
}

"""
The role of an agent in an AIP credential exchange.
"""
enum AIPCredExRole {
  ISSUER
  HOLDER
}

"""
The state of an AIP credential exchange.
"""
enum AIPCredExState {
  PROPOSAL
  OFFER
  REQUEST
  """
  If the credential exchange is waiting for a final 'ack', then it will wait in this
  state, otherwise it may transition immediately into the 'DONE' state.
  """
  ISSUED
  """
  Terminal state for a successful, "complete" credential exchange. This means
  that a verifiable credential has been exchanged.
  """
  DONE
  "Terminal state for an unsuccessful credential exchange."
  ABANDONED
}

"Messages sent between the holder and issuer within a credential exchange."
type AIPCredExMessages {
  "The latest 'proposal' message sent by the holder during the credential exchange."
  proposal: AIPCredProposal
  "The latest 'offer' message sent by the issuer during the credential exchange."
  offer: AIPCredOffer
  "The latest 'request' message sent by the holder during the credential exchange."
  request: AIPCredRequest
  "The latest 'issue' message sent by the issuer during the credential exchange."
  issued: AIPCredIssued
}

"""
AIP Credential Proposal record data type.
This is a 'proposal' message within an AIP Credential Exchange.
"""
type AIPCredProposal {
  "A comment associated with the message."
  comment: String
  "Further contents of the message, which depends on the format of the credential being proposed."
  format: AIPCredProposalFormat!
}

union AIPCredProposalFormat = AnoncredsCredProposal | W3CCredNegotiation

"""
Anoncreds Credential Proposal record data type.
"""
type AnoncredsCredProposal {
  "The credential should be based on the specific credential definition referenced by this ID."
  credDefId: ID
  """
  For convenience.
  Returns the credential definition which 'credDefId' resolves to.
  This will return null if 'credDefId' is not set.
  """
  credDef: AnoncredsCredDef
  "The credential should be based on any credential definition which is written by the issuer with this ID."
  credDefIssuerId: ID
  "The credential should be based on the specific schema referenced by this ID."
  schemaId: ID
  """
  For convenience.
  Returns the schema which 'schemaId' resolves to.
  This will return null if 'schemaId' is not set.
  """
  schema: AnoncredsSchema
  "The credential should be based on any schema which has this name."
  schemaName: String
  "The credential should be based on any schema which has this version."
  schemaVersion: String
  "The credential should be based on any schema which is written by the issuer with this ID."
  schemaIssuerId: ID
  """
  The set of attribute names and values the holder wants in the credential.
  If empty array, then the holder may be proposing a credential with no attributes.
  If null, then the holder is leaving the attribute values open-ended for the issuer to decide.
  """
  attributeValues: [AnoncredsCredNegotiationAttribute!]
}

type AnoncredsCredNegotiationAttribute {
  "The name of the attribute."
  name: String!
  """
  The value assigned to the attribute.
  If 'mimeType' is set, then this value will be base64 encoded.
  """
  value: String!
  "The MIME type of 'value'."
  mimeType: String
}

"""
The details of a W3C credential which has not yet been issued.
This data type can represent a W3C Credential Proposal, Offer or Request.
"""
type W3CCredNegotiation {
  "The W3C Verifiable Credential, encoded as a stringified JSON."
  credentialJson: String!
  """
  A public identifier for the credential.
  If null, the credential has no public identifier.
  """
  publicId: ID
  "The JSON-LD documents which define the contents of the credential."
  contexts: [String!]!
  "The JSON-LD types found in 'contexts' which define the contents of the credential."
  types: [String!]!
  "The DID of the issuer who will issue the credential."
  issuerId: ID!
  "The datetime from which the credential is to be valid, in ISO 8601 format."
  issuanceTimestamp: String!
  "The datetime from which the credential is to be invalid, in ISO 8601 format."
  expirationTimestamp: String
  """
  Each element in this array is a stringified JSON which contains the attributes for a single credential subject.
  A W3C credential may have more than one subject.
  """
  subjectJsons: [String!]!
  "Additional features which the credential will support."
  supportedFeatures: W3CSupportedFeatures!
}

"""
AIP Credential Offer record data type.
This is an 'offer' message within an AIP Credential Exchange.
"""
type AIPCredOffer {
  "A comment associated with the message."
  comment: String
  "If not null, the replacement ID used to replace an existing credential."
  replacementId: ID
  "Further contents of the message, which depends on the format of the credential being offered."
  format: AIPCredOfferFormat!
}

union AIPCredOfferFormat = AnoncredsCredOffer | W3CCredNegotiation

"""
Anoncreds Credential Offer record data type.
"""
type AnoncredsCredOffer {
  "The credential will be based on the credential definition with this ID."
  credDefId: ID!
  """
  For convenience.
  Returns the credential definition which 'credDefId' resolves to.
  Returns null if the credential definition could not be resolved on the ledger.
  """
  credDef: AnoncredsCredDef
  "The credential will be based on the schema with this ID."
  schemaId: ID!
  """
  For convenience.
  Returns the schema which 'schemaId' resolves to.
  Returns null if the schema could not be resolved on the ledger.
  """
  schema: AnoncredsSchema
  "The credential will contain these attributes and values."
  attributeValues: [AnoncredsCredNegotiationAttribute!]!
}

"""
AIP Credential Request record data type.
This is a 'request' message within an AIP Credential Exchange.
"""
type AIPCredRequest {
  "A comment associated with the message."
  comment: String
  "Further contents of the message, which depends on the format of the credential being requested."
  format: AIPCredRequestFormat!
}

union AIPCredRequestFormat = AnoncredsCredRequest | W3CCredNegotiation

"""
Anoncreds Credential Request record data type.
"""
type AnoncredsCredRequest {
  """
  Ignore this property - it will always return null.
  This type is intended to be an empty placeholder object.
  This type can potentially be used to determine the format of the credential being requested.
  """
  _: Boolean
}

"""
AIP Credential Issued record data type.
This is an 'issue' message within an AIP Credential Exchange.
"""
type AIPCredIssued {
  "A comment associated with this message."
  comment: String
  "If not null, the replacement ID used to replace an existing credential."
  replacementId: ID
  "Further contents of the message, which depends on the format of the credential which is issued."
  format: AIPCredIssuedFormat!
}

union AIPCredIssuedFormat =
  | AnoncredsCredentialIssuanceRecord
  | W3CCredentialIssuanceRecord

"""
Records the details of an issued Anoncreds verifiable credential.
This credential may have been either issued by this agent or issued to this agent.
"""
type AnoncredsCredentialIssuanceRecord {
  """
  If 'myRole' is 'HOLDER', then this is the internal ID of the held credential.
  If 'myRole' is 'ISSUER', then this will be null because the credential is not held by this agent.
  """
  credentialId: ID
  "Attributes in the credential."
  attributes: [AnoncredsCredAttribute!]!
  "If null, the issued credential is not revocable."
  revocationInfo: AnoncredsRevocationInfo
  "The ID of the credential definition which defines the credential."
  credDefId: ID!
  """
  For convenience.
  Resolves the credential definition referred to by 'credDefId'.
  The schema for the credential can optionally be resolved within this credential definition.
  Returns null if the credential definition could not be resolved on the ledger.
  """
  credDef: AnoncredsCredDef
  "The datetime when the credential was issued, in ISO 8601 format."
  issuanceTimestamp: String!
}

"""
Records the details of an issued W3C Verifiable Credential.
This credential may have been either issued by this agent or issued to this agent.
"""
type W3CCredentialIssuanceRecord {
  """
  If 'myRole' is 'HOLDER', then this is the internal ID of the held credential.
  If 'myRole' is 'ISSUER', then this will be null because the credential is not held by this agent.
  """
  credentialId: ID
  "The W3C Verifiable Credential, encoded as a stringified JSON."
  credentialJson: String!
  """
  A public identifier for the credential.
  If null, the credential has no public identifier.
  """
  publicId: ID
  "The JSON-LD documents which define the contents of the credential."
  contexts: [String!]!
  "The JSON-LD types found in 'contexts' which define the contents of the credential."
  types: [String!]!
  "The DID of the issuer of the credential."
  issuerId: ID!
  "The datetime from which the credential is valid, in ISO 8601 format."
  issuanceTimestamp: String!
  "The datetime from which the credential is invalid, in ISO 8601 format."
  expirationTimestamp: String
  """
  Each element in this array is a stringified JSON which contains the attributes for a single credential subject.
  A W3C credential may have more than one subject.
  """
  subjectJsons: [String!]!
  "Additional features which the credential supports."
  supportedFeatures: W3CSupportedFeatures!
}

"""
Optional filters for the 'aipCredentialExchanges' query. The filters are AND'd together
"""
input AIPCredentialExchangesFilter {
  "Filter by the AIP version of the credential exchange protocol."
  aipVersions: [AIPVersion!]
  "Filter by the role this agent plays in the credential exchange."
  myCredExRole: AIPCredExRole
  "Filter by the current state of the credential exchange."
  credExStates: [AIPCredExState!]
  "Filter by the ID of the connection associated with the credential exchange. Must be an exact match."
  connectionIds: [ID!]
  "Filter by the format of the credential being exchanged."
  credentialFormats: [CredentialFormat!]
  """
  Filter by the ID of the credential which the credential exchange resulted in. Must be an exact match.
  Only credential exchanges where myRole=HOLDER and state=DONE will be associated with a stored credential ID.
  """
  storedCredentialIds: [ID!]
  """
  Sort the results of this query by the 'modifiedTimestamp' of the credential exchanges.
  Default is 'DESCENDING' so that the latest update will appear first.
  """
  sortOrder: SortOrder
}

"""
Paged result container of the 'aipCredentialExchanges' query
"""
type AIPCredentialExchangesResult {
  "The result items"
  items: [AIPCredentialExchange!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

"""
Input parameters for the 'sendAipCredentialOffer' mutation.
"""
input AIPCredOfferInput {
  "A comment associated with the offer."
  comment: String
  "Parameters specific to the format of the credential being offered."
  format: AIPCredOfferFormatInput!
  """
  If true, then the 'acceptAipCredentialRequest' mutation will be executed automatically
  if the receiver of the credential offer accepts the offer.
  If null, defaults to false.
  """
  autoAcceptRequest: Boolean
}

"""
A value must be provided for either the 'anoncreds' param or the 'w3c' param, but not both.
"""
input AIPCredOfferFormatInput {
  "Parameters available when offering an Anoncreds Verifiable Credential."
  anoncreds: AnoncredsCredOfferInput
  "Parameters available when offering a W3C Verifiable Credential."
  w3c: W3CCredOfferInput
}

"""
Parameters for sending an offer of an Anoncreds Verifiable Credential.
"""
input AnoncredsCredOfferInput {
  "The ID of the AnoncredsCredDef to use as a base for the issued credential."
  credDefId: ID!
  "The attributes to place in the issued credential."
  attributeValues: [AnoncredsCredNegotiationAttributeInput!]!
}

input AnoncredsCredNegotiationAttributeInput {
  "Name of the attribute."
  name: String!
  "Value of the attribute."
  value: String!
  "Optional MIME type of the attribute."
  mimeType: String
}

"""
Parameters for sending an offer of a W3C Verifiable Credential.
"""
input W3CCredOfferInput {
  "The W3C Verifiable Credential, encoded as a stringified JSON."
  credentialJson: String!
  """
  Set to true if the issued credential should support selective disclosure presentation features.
  This affects the cryptographic signature used to sign the issued credential.
  If null, defaults to false.
  """
  supportSelectiveDisclosure: Boolean
}

"""
Input parameters for the 'acceptAipCredentialOffer' mutation.
"""
input AcceptAipCredOfferInput {
  "Parameters specific to the format of the credential in the offer."
  format: AcceptAipCredOfferFormatInput
}

"""
If an Anoncreds Verifiable Credential is offered, then omit the 'w3c' param.
"""
input AcceptAipCredOfferFormatInput {
  "Parameters available when accepting an offer for a W3C Verifiable Credential."
  w3c: AcceptW3CCredOfferInput
}

"""
Parameters for accepting an offer for a W3C Verifiable Credential.
"""
input AcceptW3CCredOfferInput {
  """
  Optionally inject a subject DID in the offered credential. Doing so will associate the credential with the subject.
  Usually, the subject DID is expected to reference this agent (when acting as the HOLDER).
  """
  subjectId: String
}

"""
Input parameters for the 'acceptAipCredentialRequest' mutation.
"""
input AcceptAipCredRequestInput {
  "A comment associated with the request."
  comment: String
}

#
# Data types for Proof Exchanges
#

"""
Aries Interop Profile (AIP) Proof Exchange record data type
"""
type AIPProofExchange {
  "The unique identifier for the proof exchange."
  id: ID!
  "The version of AIP used for the proof exchange."
  aipVersion: AIPVersion!
  "The role of this agent in the proof exchange."
  myRole: AIPProofExRole!
  "The current state of the proof exchange."
  state: AIPProofExState!
  "Messages which have been sent or received during the exchange."
  messages: AIPProofExMessages!
  "The datetime when the proof exchange was created, in ISO 8601 format."
  createdTimestamp: String!
  "The datetime when the proof exchange was last updated, in ISO 8601 format."
  modifiedTimestamp: String!
  "The ID of the connection with whom this agent is performing the exchange."
  connectionId: ID!
  """
  The connection resolved by 'connectionId'.
  Returns null if the connection cannot be found, such as when the connection is deleted.
  """
  connection: Connection
}

"""
The role of an agent in an AIP proof exchange.
"""
enum AIPProofExRole {
  VERIFIER
  PROVER
}

"""
The state of an AIP proof exchange.
"""
enum AIPProofExState {
  PROPOSAL
  REQUEST
  """
  If the exchange is waiting for a final 'ack', then it will wait in this
  state, otherwise it may transition immediately into the 'DONE' state.
  """
  PRESENTED
  """
  Terminal state for a successful, "complete" proof exchange. This means that
  a verifiable proof presentation has been exchanged.
  If the exchange is in this state and 'myRole' is 'VERIFIER',
  then the received proof presentation has undergone verification.
  Note that this state does not indicate the result of the verification.
  To check the verification result, see 'verificationResult' on the attached
  AIPProofPresentation.
  """
  DONE
  """
  Terminal state for an unsuccessful proof exchange.
  """
  ABANDONED
}

"Messages sent between the prover and verifier within a proof exchange."
type AIPProofExMessages {
  "The latest 'proposal' message sent by the prover during the proof exchange."
  proposal: AIPProofProposal
  "The latest 'request' message sent by the verifier during the proof exchange."
  request: AIPProofRequest
  "The latest 'presentation' message sent by the prover during the proof exchange."
  presentation: AIPProofPresentation
}

"""
AIP Proof Proposal record data type.
This is a 'proposal' message within an AIP Proof Exchange.
"""
type AIPProofProposal {
  "A comment associated with the message."
  comment: String
  "Further contents of the message, which depends on the format of the credential being proposed for presentation."
  format: AIPProofProposalFormat!
}

union AIPProofProposalFormat = AnoncredsProofProposal | W3CProofNegotiation

"""
Anoncreds Proof Proposal record data type.
"""
type AnoncredsProofProposal {
  """
  A single proof presentation may involve the presentation of data from multiple verifiable credentials.
  Each attribute group requires presentation of data from exactly one verifiable credential, where the attributes
  in the group appear on that verifiable credential.
  This 'attributeGroups' property defines attribute groups which are proposed for presentation.
  """
  attributeGroups: [AnoncredsProofNegotiationAttributeGroup!]!
}

"""
Details of an attribute group within an Anoncreds proof proposal/request.
"""
type AnoncredsProofNegotiationAttributeGroup {
  "A non-unique identifier for the attribute group."
  referent: String!
  "The names of attributes within the group."
  attributeNames: [String!]!
  """
  Restrictions placed on the data and proof which is allowed to be presented for the attribute group.
  If empty, the prover may present self-attested attributes.
  The array elements are OR'd together. The restrictions within each element are AND'd together.
  """
  restrictions: [AnoncredsProofNegotiationAttributeGroupRestrictions!]!
}

"""
Restrictions placed on the data and proof which is allowed to be presented for an attribute group.
These restrictions are AND'd together.
"""
type AnoncredsProofNegotiationAttributeGroupRestrictions {
  """
  If not null, the requested attributes must be sourced from a credential
  which is based on the Anoncreds schema with this exact ID.
  """
  schemaId: ID
  """
  For convenience.
  Returns the schema which 'schemaId' resolves to.
  This will always return null if 'schemaId' is null.
  """
  schema: AnoncredsSchema
  """
  If not null, the requested attributes must be sourced from a credential
  which is based on an Anoncreds schema written to the ledger by this ID.
  """
  schemaIssuerId: ID
  """
  If not null, the requested attributes must be sourced from a credential
  which is based on an Anoncreds schema with this name.
  """
  schemaName: String
  """
  If not null, the requested attributes must be sourced from a credential
  which is based on an Anoncreds schema with this version number.
  """
  schemaVersion: String
  """
  If not null, the requested attributes must be sourced from a credential
  which is based on the Anoncreds credential definition with this exact ID.
  """
  credDefId: ID
  """
  For convenience.
  Returns the credential definition which 'credDefId' resolves to.
  This will always return null if 'credDefId' is null.
  """
  credDef: AnoncredsCredDef
  """
  If not null, the requested attributes must be sourced from a credential
  which was issued by this ID.
  """
  issuerId: ID
  """
  If not empty, the requested attributes must be sourced from a credential
  which also holds these attributes.
  """
  attributeNames: [String!]!
  """
  If not empty, the requested attributes must be sourced from a credential
  which also holds these attributes, with respective values.
  """
  attributeValues: [AnoncredsAttributeValueRestriction!]!
  """
  If not null, the requested attributes must be sourced from a credential
  which is/was not revoked within this time interval.
  """
  nonRevoked: AnoncredsNonRevokedInterval
  """
  If not null, the requested attributes must have values which satisfy
  this predicate.
  """
  predicate: AnoncredsPredicate
}

"""
A value restriction placed on an attribute.
"""
type AnoncredsAttributeValueRestriction {
  "Name of the attribute."
  name: String!
  "Required value of the attribute."
  value: String!
  "If not null, this is the MIME type of 'value'."
  mimeType: String
}

"""
A restriction which specifies a time interval when a credential used to present attributes must not be revoked.
"""
type AnoncredsNonRevokedInterval {
  "If not null, the datetime from which the credential must be valid, in ISO 8601 format."
  from: String
  "If not null, the datetime to which the credential must be valid, in ISO 8601 format."
  to: String
}

"""
A restriction on the range of possible values for an attribute.
"""
type AnoncredsPredicate {
  "The type of value range."
  operator: AnoncredsPredicateOperator!
  "The limit of the value range, working in tandem with the 'operator'."
  threshold: Int!
}

"""
Predicate operators.
"""
enum AnoncredsPredicateOperator {
  LESS_THAN
  LESS_THAN_OR_EQUAL
  GREATER_THAN_OR_EQUAL
  GREATER_THAN
}

"""
W3C Proof Proposal and W3C Proof Request record data type.
"""
type W3CProofNegotiation {
  "A human-friendly name for the proof proposal/request."
  name: String
  "A human-friendly description of the purpose for the proof proposal/request."
  purpose: String
  """
  A single proof presentation may involve the presentation of data from multiple verifiable credentials.
  Each attribute group requires presentation of data from exactly one verifiable credential, where the attributes
  in the group appear on that verifiable credential.
  This 'attributeGroups' property defines attribute groups which are proposed/requested for presentation.
  """
  attributeGroups: [W3CProofNegotiationAttributeGroup!]!
  """
  Each element in this array is a stringified JSON of submission requirements,
  with each JSON structured according to the DIF spec:
  https://identity.foundation/presentation-exchange/spec/v1.0.0/#submission-requirements
  """
  submissionRequirementJsons: [String!]
}

"""
Details of an attribute group within a W3C proof proposal/request.
"""
type W3CProofNegotiationAttributeGroup {
  "A unique identifier for the attribute group."
  referent: String!
  """
  For backward compatibility.
  Restricts the @context and type of a W3CCredential.
  Equivalent to the 'attributes' property containing two W3CProofNegotiationAttributes
  where 'attributePaths' is set to ["$.@context"] and ["$.type"] respectively,
  with 'valueRestrictions' set appropriately for each.
  """
  schemaUris: [String!]
  "A list of attributes within the group, with restrictions applied to each attribute."
  attributes: [W3CProofNegotiationAttribute!]!
}

"""
An attribute in a proof proposal/request, with restrictions applied to it.
"""
type W3CProofNegotiationAttribute {
  """
  JSONPath paths which could possibly locate the attribute within the JSON-encoded W3C credential subject.
  Only one path needs to match.
  """
  attributePaths: [String!]!
  "A human-friendly description of the purpose for proposing/requesting this attribute."
  purpose: String
  "Restrictions on the possible values allowed to be presented for this attribute."
  valueRestrictions: W3CProofNegotiationValueRestrictions!
  "If true, then the holder of the source credential must also be the subject of the source credential."
  isHolderSubject: Boolean!
  """
  If 'RAW_VALUE', then the verifier requires the prover to present the raw value of the attribute.
  If 'PREDICATE', then the verifier requires the prover to present a predicate (boolean value)
  instead of the raw value of the attribute.
  If 'PREDICATE_OR_RAW_VALUE', then the verifier will accept either a predicate (boolean value)
  or the raw value of the attribute, but a predicate is preferred.
  """
  presentationType: W3CProofNegotiationPresentationType!
}

"""
Types of W3C attribute value presentations.
A presentation may contain the raw value of an attribute, or a predicate (boolean value) ZKP.
"""
enum W3CProofNegotiationPresentationType {
  RAW_VALUE
  PREDICATE
  PREDICATE_OR_RAW_VALUE
}

"""
The data format for the inputs to the value restrictions
Built to conform to https://json-schema.org/understanding-json-schema/reference
"""
enum W3CValueRestrictionDataFormat {
  TEXT
  NUMBER
  DATE
}

"""
Restrictions on the possible values which are allowed to be presented for an attribute.
"""
type W3CProofNegotiationValueRestrictions {
  "If not null, the attribute value is required to be in this data format."
  dataFormat: W3CValueRestrictionDataFormat
  "If not null, the attribute value is required to match this regex pattern."
  pattern: String
  """
  If not null, then the attribute value is required to appear in this set.
  If empty array, then the proof presentation will never be valid.
  """
  inSet: [String!]
  "If not null, then the attribute value is required to NOT appear in this set."
  notInSet: [String!]
  "If not null, the attribute value is required to match this exact value."
  equal: String
  "If not null, the attribute value is required to NOT match this exact value."
  notEqual: String
  "If not null, the attribute value is required to be a string of length greater than or equal to this length."
  minLength: Int
  "If not null, the attribute value is required to be a string of length less than or equal to this length."
  maxLength: Int
  "If not null, the attribute value is required to be less than this value, corresponding to the dataFormat provided."
  lessThan: String
  "If not null, the attribute value is required to be less than or equal to this value, corresponding to the dataFormat provided."
  lessThanOrEqual: String
  "If not null, the attribute value is required to be greater than this value, corresponding to the dataFormat provided."
  greaterThan: String
  "If not null, the attribute value is required to be greater than or equal to this value, corresponding to the dataFormat provided."
  greaterThanOrEqual: String
}

"""
AIP Proof Request record data type.
This is a 'request' message within an AIP Proof Exchange.
"""
type AIPProofRequest {
  "A comment associated with the message."
  comment: String
  "Further contents of the message, which depends on the format of the credential being requested for presentation."
  format: AIPProofRequestFormat!
}

union AIPProofRequestFormat = AnoncredsProofRequest | W3CProofNegotiation

"""
Anoncreds Proof Request record data type.
"""
type AnoncredsProofRequest {
  "A human-friendly name for the proof request."
  name: String
  "Version of the proof request."
  version: String
  """
  A single proof presentation may involve the presentation of data from multiple verifiable credentials.
  Each attribute group requires presentation of data from exactly one verifiable credential, where the attributes
  in the group appear on that verifiable credential.
  This 'attributeGroups' property defines attribute groups which are requested for presentation.
  """
  attributeGroups: [AnoncredsProofNegotiationAttributeGroup!]!
}

"""
AIP Proof Presentation record data type.
This is a 'presentation' message within an AIP Proof Exchange.
"""
type AIPProofPresentation {
  "A comment associated with the message."
  comment: String
  "Further contents of the message, which depends on the format of the credential being presented."
  format: AIPProofPresentationFormat!
  """
  If the role on the associated proof exchange is 'Prover', then this will always be null.
  If the role on the associated proof exchange is 'Verifier', then;
  If null, the proof hasn't been verified yet.
  If true, the proof passed verification - this presentation is digitally trusted.
  If false, the proof failed verification - this presentation is not digitally trusted.
  """
  verificationResult: Boolean
}

union AIPProofPresentationFormat =
  | AnoncredsProofPresentation
  | W3CProofPresentation

"""
Anoncreds Proof Presentation record data type.
"""
type AnoncredsProofPresentation {
  "The raw values presented in response to a proof request."
  revealedAttributeGroups: [AnoncredsRevealedAttributeGroup!]!
  "Referents of the attribute groups for which proof was presented, without raw values."
  unrevealedAttributeGroupReferents: [String!]!
  "The self-attested values presented in response to a proof request."
  selfAttestedAttributes: [AnoncredsSelfAttestedAttribute!]!
  "Referents of the attribute groups for which a predicate was presented."
  presentedPredicateReferents: [String!]!
}

"""
The values presented for an attribute group in a proof request.
"""
type AnoncredsRevealedAttributeGroup {
  "The referent of the attribute group."
  referent: String!
  "The attributes in the group, with presented values."
  attributes: [AnoncredsRevealedAttribute!]!
}

"""
An attribute's raw value presented in response to a proof request.
"""
type AnoncredsRevealedAttribute {
  "The name of the attribute."
  name: String!
  "The value presented for the attribute."
  value: String!
}

"""
A self-attested value presented for an attribute in a proof request.
"""
type AnoncredsSelfAttestedAttribute {
  "The referent of the single-element attribute group."
  referent: String!
  "The value presented for the attribute."
  value: String!
}

"""
W3C Proof Presentation record data type.
"""
type W3CProofPresentation {
  "W3C credentials which are presented to satisfy the proof request."
  presentedCredentials: [W3CPresentedCredential!]!
}

"""
A W3C credential presented in response to an attribute group within a proof request.
"""
type W3CPresentedCredential {
  "The referent of the attribute group."
  referent: String!
  """
  The credential presented for the attribute group.
  Note that this credential may or may not be derived from another credential.
  """
  credential: W3CCredentialPresentation!
}

"""
A presentation of a W3C credential.
Note that this credential may or may not be derived from another W3C credential.
"""
type W3CCredentialPresentation {
  "The W3C Verifiable Credential, encoded as a stringified JSON."
  credentialJson: String!
  """
  A public identifier for the credential.
  If null, the credential has no public identifier.
  """
  publicId: ID
  "The JSON-LD documents which define the contents of the credential."
  contexts: [String!]!
  "The JSON-LD types found in 'contexts' which define the contents of the credential."
  types: [String!]!
  "The DID of the issuer of the credential."
  issuerId: ID!
  "The datetime from which the credential is valid, in ISO 8601 format."
  issuanceTimestamp: String!
  "The datetime from which the credential is invalid, in ISO 8601 format."
  expirationTimestamp: String
  """
  Each element in this array is a stringified JSON which contains the attributes for a single credential subject.
  A W3C credential may have more than one subject.
  """
  subjectJsons: [String!]!
}

"""
Optional filters for the 'aipProofExchanges' query. The filters are AND'd together
"""
input AIPProofExchangesFilter {
  "Filter by the AIP version of the proof exchange protocol."
  aipVersions: [AIPVersion!]
  "Filter by the role this agent plays in the proof exchange."
  myProofExRole: AIPProofExRole
  "Filter by the current state of the proof exchange."
  proofExStates: [AIPProofExState!]
  "Filter by the ID of the connection associated with the proof exchange. Must be an exact match."
  connectionIds: [ID!]
  "Filter by the format of the credential being presented in the exchange."
  credentialFormats: [CredentialFormat!]
  """
  Filter by the result of the verification of a received proof presentation. Must be an exact match.
  Only proof exchanges where myRole=VERIFIER and state=DONE will have a value set for 'verificationResult'.
  """
  verificationResult: Boolean
  """
  Sort the results of this query by the 'modifiedTimestamp' of the proof exchanges.
  Default is 'DESCENDING' so that the latest update will appear first.
  """
  sortOrder: SortOrder
}

"""
Input parameters for the 'sendAipProofRequest' mutation.
"""
input AIPProofRequestInput {
  "A comment associated with the request."
  comment: String
  "Parameters specific to the format of the credential presentation being requested."
  format: AIPProofRequestFormatInput!
}

"""
A value must be provided for either the 'anoncreds' param or the 'w3c' param, but not both.
"""
input AIPProofRequestFormatInput {
  "Parameters available when requesting an Anoncreds Verifiable Presentation."
  anoncreds: AnoncredsProofRequestInput
  "Parameters available when requesting a W3C Verifiable Presentation."
  w3c: W3CProofNegotiationInput
}

"""
Parameters for sending a request for an Anoncreds Verifiable Presentation.
"""
input AnoncredsProofRequestInput {
  "Name of the proof request."
  name: String!
  "Version of the proof request."
  version: String!
  "Attributes for which the raw value is expected to be presented."
  requestedAttributes: [AnoncredsProofNegotiationAttributesInput!]!
  "Attributes for which a derived value is expected to be presented."
  requestedPredicates: [AnoncredsProofNegotiationPredicateInput!]!
}

input AnoncredsProofNegotiationAttributesInput {
  "An identifier for this group of attributes and restrictions."
  referent: String!
  "Names of all the attributes grouped under this referent."
  attributeNames: [String!]!
  """
  If not null, the presented attributes must be sourced from a credential
  which is not revoked before this timestamp, inclusive, in ISO 8601 format.
  """
  nonRevokedToTimestamp: String
  """
  Restrictions which apply to every attribute under this referent.
  The array elements are OR'd together. The restrictions within each element are AND'd together.
  """
  restrictions: [AnoncredsProofNegotiationRestrictionsInput!]!
}

input AnoncredsProofNegotiationPredicateInput {
  "An identifier for this attribute, predicate and restrictions."
  referent: String!
  "The name of the attribute to which the predicate and restrictions apply."
  attributeName: String!
  "The operator which determines the range of allowed values for this attribute."
  predicateOperator: AnoncredsPredicateOperator!
  "The limit on the range of values expressed by 'predicateOperator'."
  predicateThreshold: Int!
  """
  If not null, the presented attributes must be sourced from a credential
  which is not revoked before this timestamp, inclusive, in ISO 8601 format.
  """
  nonRevokedToTimestamp: String
  """
  Additional restrictions which apply to this attribute.
  The array elements are OR'd together. The restrictions within each element are AND'd together.
  """
  restrictions: [AnoncredsProofNegotiationRestrictionsInput!]!
}

"These restrictions are AND'd together."
input AnoncredsProofNegotiationRestrictionsInput {
  """
  If not null, the presented attributes must be sourced from a credential
  which is based on the Anoncreds schema with this exact ID.
  """
  schemaId: ID
  """
  If not null, the presented attributes must be sourced from a credential
  which is based on an Anoncreds schema written to the ledger by this ID.
  """
  schemaIssuerId: ID
  """
  If not null, the presented attributes must be sourced from a credential
  which is based on an Anoncreds schema with this name.
  """
  schemaName: String
  """
  If not null, the presented attributes must be sourced from a credential
  which is based on an Anoncreds schema with this version number.
  """
  schemaVersion: String
  """
  If not null, the presented attributes must be sourced from a credential
  which is based on the Anoncreds credential definition with this exact ID.
  """
  credDefId: ID
  """
  If not null, the presented attributes must be sourced from a credential
  which was issued by this ID.
  """
  issuerId: ID
  """
  If not empty, the presented attributes must be sourced from a credential
  which also holds these attributes, with respective values.
  """
  peerAttributeValues: [AnoncredsAttributeValueRestrictionInput!]
}

input AnoncredsAttributeValueRestrictionInput {
  "Name of the attribute."
  name: String!
  "Required value of the attribute."
  value: String!
}

"""
Parameters for sending a proposal/request for a W3C Verifiable Presentation.
"""
input W3CProofNegotiationInput {
  "A human-friendly name for the proof proposal/request."
  name: String
  "A human-friendly description of the purpose for the proof proposal/request."
  purpose: String
  """
  A single proof presentation may involve the presentation of data from multiple verifiable credentials.
  Each attribute group requires presentation of data from exactly one verifiable credential, where the attributes
  in the group appear on that verifiable credential.
  These attribute groups are also known as input descriptors.
  """
  attributeGroups: [W3CProofNegotiationAttributeGroupInput!]!
  """
  Each element in this array is a stringified JSON of submission requirements,
  with each JSON structured according to the DIF spec:
  https://identity.foundation/presentation-exchange/spec/v1.0.0/#submission-requirements
  """
  submissionRequirementJsons: [String!]
}

input W3CProofNegotiationAttributeGroupInput {
  "A unique identifier for the attribute group."
  referent: String!
  """
  For backward compatibility.
  Restricts the @context and type of a W3CCredential.
  Equivalent to the 'attributes' property containing two W3CProofNegotiationAttributeInputs
  where 'attributePaths' is set to ["$.@context"] and ["$.type"] respectively,
  with 'restrictions' set appropriately for each.
  """
  schemaUris: [String!]
  "A list of attributes within the group, with restrictions applied to each attribute."
  attributes: [W3CProofNegotiationAttributeInput!]!
}

input W3CProofNegotiationAttributeInput {
  """
  JSONPath paths which could possibly locate the attribute within the JSON-encoded W3C credential subject.
  Only one path needs to match.
  """
  attributePaths: [String!]!
  "A human-friendly description of the purpose for proposing/requesting this attribute."
  purpose: String
  "Restrictions on the possible values allowed to be presented for this attribute."
  restrictions: W3CProofNegotiationRestrictionsInput!
  "If true, then the holder of the source credential must also be the subject of the source credential."
  isHolderSubject: Boolean!
  """
  If 'RAW_VALUE', then the prover is required to present the raw value of the attribute.
  If 'PREDICATE', then the prover is required to present a predicate (boolean value)
  instead of the raw value of the attribute.
  If 'PREDICATE_OR_RAW_VALUE', then the prover may present either a predicate (boolean value)
  or the raw value of the attribute, but a predicate is preferred.
  """
  presentationType: W3CProofNegotiationPresentationType!
}

"""
Restrictions on the value which may be presented for an attribute.
"""
input W3CProofNegotiationRestrictionsInput {
  "If not null, the attribute value is required to be in this data format. Default is 'TEXT'."
  dataFormat: W3CValueRestrictionDataFormat
  "If not null, the attribute value is required to match this regex pattern."
  pattern: String
  "If not null, then the attribute value is required to appear in this set."
  inSet: [String!]
  "If not null, then the attribute value is required to NOT appear in this set."
  notInSet: [String!]
  "If not null, the attribute value is required to match this exact value."
  equal: String
  "If not null, the attribute value is required to NOT match this exact value."
  notEqual: String
  "If not null, the attribute value is required to be a string of length greater than or equal to this length."
  minLength: Int
  "If not null, the attribute value is required to be a string of length less than or equal to this length."
  maxLength: Int
  "If not null, the attribute value is required to be less than this value, corresponding to the dataFormat provided."
  lessThan: String
  "If not null, the attribute value is required to be less than or equal to this value, corresponding to the dataFormat provided."
  lessThanOrEqual: String
  "If not null, the attribute value is required to be greater than this value, corresponding to the dataFormat provided."
  greaterThan: String
  "If not null, the attribute value is required to be greater than or equal to this value, corresponding to the dataFormat provided."
  greaterThanOrEqual: String
}

"""
Input parameters for the 'acceptAipProofRequest' mutation.
"""
input AIPProofPresentationInput {
  "Parameters specific to the format of the credential presentation being sent."
  format: AIPProofPresentationFormatInput!
}

"""
A value must be provided for either the 'anoncreds' param or the 'w3c' param, but not both.
"""
input AIPProofPresentationFormatInput {
  "Parameters available when sending an Anoncreds Verifiable Presentation."
  anoncreds: AnoncredsProofPresentationInput
  "Parameters available when sending a W3C Verifiable Presentation."
  w3c: W3CProofPresentationInput
}

input AnoncredsProofPresentationInput {
  """
  Present proof of ownership, with raw values, from these credentials
  in response to these proof request attribute group referents.
  """
  revealedAttributeGroups: [AnoncredsPresentedCredentialInput!]!
  """
  Present proof of ownership, but not the raw values, from these credentials
  in response to these proof request attribute group referents.
  """
  unrevealedAttributeGroups: [AnoncredsPresentedCredentialInput!]!
  """
  Present self-attested raw values in response to these proof request attribute referents.
  """
  selfAttestedAttributes: [AnoncredsSelfAttestedAttributeInput!]!
  """
  Present proof of ownership, with ZKP values which are derived from the raw values,
  from these credentials in response to these proof request attribute referents.
  """
  presentedPredicates: [AnoncredsPresentedCredentialInput!]!
}

input AnoncredsPresentedCredentialInput {
  "Referent of an attribute group in the proof request."
  referent: String!
  "The held credential to generate a presentation from in response to this referent."
  credentialId: ID!
}

input AnoncredsSelfAttestedAttributeInput {
  "Referent of an attribute in the proof request."
  referent: String!
  "The value to present in response to this referent."
  value: String!
}

input W3CProofPresentationInput {
  "Present these W3C Verifiable Credentials in response to these proof request referents."
  presentedCredentials: [W3CPresentedCredentialInput!]!
}

input W3CPresentedCredentialInput {
  "Referent of an attribute group (a.k.a. ID of an input descriptor) in the proof request."
  referent: String!
  "The held W3C Verifiable Credential to generate a presentation from in response to this referent."
  credentialId: ID!
}

"""
Paged result container of the 'aipProofExchanges' query
"""
type AIPProofExchangesResult {
  "The result items"
  items: [AIPProofExchange!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

"""
Result container of the 'credentialsForAipProofExchange' query
"""
type CredentialsForAIPProofExchangeResult {
  credentialsForReferents: [CredentialsForReferent!]!
}

type CredentialsForReferent {
  """
  The referent to a subsection in the proof request for which an individual credential can be presented.
  If null, then this object does not map its 'credentialIds' to any particular referent.
  """
  referent: String
  "The IDs of held credentials which are valid for presentation to the referent."
  credentialIds: [ID!]!
}

#
# Data types for DIDs
#

"""
Input parameters for the 'createDid' mutation.

This type provides options for several DID Methods, exactly ONE of these options
should be provided (i.e. for the single desired DID method).
"""
input CreateDidOptions {
  "Optional alias for the DID"
  alias: String
  "did:key method-specific options for creating a DID"
  createKeyDidOptions: CreateKeyDidOptions
  "did:cheqd method-specific options for creating a DID"
  createCheqdDidOptions: CreateCheqdDidOptions
}

"""
Input parameters for creating a KEY method DID.
"""
input CreateKeyDidOptions {
  "The cryptographic key type to use for the DID"
  keyType: KeyDidKeyType!
}

"""
Input parameters for creating a KEY method DID.
"""
input CreateCheqdDidOptions {
  "The cheqd network to create this DID on"
  network: CheqdNetwork!
}

"""
Supported blockchain networks for cheqd DIDs.
"""
enum CheqdNetwork {
  TESTNET
  MAINNET
}

"""
Supported key types for KEY method DIDs.
"""
enum KeyDidKeyType {
  ED25519
  P256
  BLS_12381_G2
}

"""
DID record data type
"""
type Did {
  "The DID identifier"
  did: String!
  "Method-specific data for the DID"
  methodData: DidMethodData!
  "The locally assigned alias for the DID"
  alias: String
  "The datetime when this DID was created, in ISO 8601 format"
  createdTimestamp: String!
  "The datetime when this DID was last updated, in ISO 8601 format"
  modifiedTimestamp: String!
  "Whether this DID is archived"
  archived: Boolean!
  "Whether this DID is set as the primary DID"
  primary: Boolean!
}
union DidMethodData = KeyDidMethodData | CheqdDidMethodData

"""
Method-specific data for KEY method DIDs.
"""
type KeyDidMethodData {
  "The cryptographic key type used by this DID"
  keyType: KeyDidKeyType!
}

"""
Method-specific data for CHEQD method DIDs.
"""
type CheqdDidMethodData {
  "The cheqd network this DID is register to"
  network: CheqdNetwork!
}


"""
DID methods that can be filtered in queries.
"""
enum FilterableDidMethod {
  KEY
  CHEQD
}

"""
Optional filters for the 'dids' query. The filters are AND'd together
"""
input DidsFilter {
  "Filter DIDs by identifier. Must be exact matches"
  dids: [String!]
  "Filter DIDs by method"
  methods: [FilterableDidMethod!]
  "Filter by the alias of the DID. Returns exact substring matches."
  alias: String
  """
  Filter by the alias of the DID.
  Returns all DIDs in order of similarity to the search value, with the best matches listed first.
  """
  aliasFuzzySearch: String
  "Filter DIDs by archived status"
  archived: Boolean
  """
  Sort the results of this query by the 'modifiedTimestamp' of the DIDs.
  Default is 'DESCENDING' so that the latest update will appear first.
  """
  sortOrder: SortOrder
}

"""
Paged result container of the 'dids' query
"""
type DidsResult {
  "The result items"
  items: [Did!]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

"""
Input parameters for the 'updateDid' mutation.
"""
input UpdateDidOptions {
  "Update the alias of the DID"
  updateAlias: String
  "Update the archived status of the DID"
  updatedArchived: Boolean
}

#
# Data types for Status List management
#

"""
The purpose of a status list, indicating what credential status it tracks.
"""
enum StatusListPurpose {
  "Used to track revoked credentials"
  REVOCATION
  "Used to track suspended credentials"
  SUSPENSION
}

"""
Status List Definition record data type
"""
type StatusListDefinition {
  "Unique identifier for the status list definition"
  identifier: ID!
  "The purposes this status list definition supports"
  purposes: [StatusListPurpose!]!
  "The DID of the issuer who creates status lists belonging to this definition"
  issuerDid: String!
  "Information about the individual status lists created for each purpose"
  lists: [StatusListInfo!]!
  "The datetime when this status list definition was created, in ISO 8601 format"
  createdTimestamp: String!
}

"""
Information about an individual status list within a status list definition.
"""
type StatusListInfo {
  "The numeric identifier of this status list"
  listNumber: Int!
  "The purpose of this status list"
  purpose: StatusListPurpose!
}

"""
Input parameters for the 'createStatusListDefinition' mutation.
"""
input CreateStatusListDefinitionInput {
  "Unique identifier for the status list definition"
  identifier: ID!
  "The purposes this status list definition should support"
  purposes: [StatusListPurpose!]!
  "The DID of the issuer creating this status list definition"
  issuerDid: String!
}

"""
Paged result container of the 'statusListDefinitions' query
"""
type StatusListDefinitionsResult {
  "The result items"
  items: [StatusListDefinition!]!
}

"""
Input parameters for the 'publishStatusList' mutation.
"""
input PublishStatusListInput {
  "The identifier of the status list definition to publish"
  identifier: String!
}

"""
Result of the 'publishStatusList' mutation.
"""
type PublishStatusListResult {
  "Status lists that were successfully published"
  success: [StatusListInfo!]!
  "Status lists that failed to publish"
  failed: [PublishStatusListFailure!]!
}

"""
Information about a status list that failed to publish.
"""
type PublishStatusListFailure {
  "The status list that failed to publish"
  list: StatusListInfo!
  "Error message describing why the publish failed"
  error: String
}

"""
Identifies a credential entry within a specific status list definition.
"""
type StatusEntryIdentifier {
  "The unique identifier of the credential"
  credentialId: ID!
  "The identifier of the status list definition containing this entry"
  statusListDefinitionIdentifier: ID!
}

"""
Input parameters for identifying a credential entry within a status list.
"""
input StatusEntryIdentifierInput {
  "The unique identifier of the credential"
  credentialId: ID!
  "The identifier of the status list definition"
  statusListDefinitionIdentifier: ID!
}

"""
Input parameters for assigning a credential to a status list.
"""
input AssignCredentialToStatusListInput {
  "The credential entry to assign to a status list"
  newEntry: StatusEntryIdentifierInput!
}

"""
Result of assigning a credential to status lists.
"""
type StatusListAssignment {
  """
  Array of W3C credential status objects as stringified JSON
  (ready to be JSON parsed and included as credentialStatus objects
  in a W3C VCDM credentialStatus).
  """
  w3cCredentialStatusJsons: [String!]!
}

"""
Input parameters for updating credential statuses.
"""
input UpdateCredentialStatusesInput {
  "List of status updates to apply"
  updates: [UpdateCredentialStatusEntryInput!]!
  "Whether to publish the status lists immediately after updating"
  publishImmediately: Boolean!
}

"""
Input parameters for updating a single credential's status.
"""
input UpdateCredentialStatusEntryInput {
  "The credential entry to update"
  entry: StatusEntryIdentifierInput!
  "Set to true to mark as revoked, false to unrevoke, or null to leave unchanged"
  revoked: Boolean
  "Set to true to mark as suspended, false to unsuspend, or null to leave unchanged"
  suspended: Boolean
}

"""
Result of updating credential statuses.
"""
type UpdateCredentialStatusesResult {
  "Entries that were successfully updated"
  success: [StatusEntryIdentifier!]!
  "Entries that failed to update"
  failed: [UpdateCredentialStatusFailure!]!
}

"""
Information about a credential status update that failed.
"""
type UpdateCredentialStatusFailure {
  "The credential entry that failed to update"
  entry: StatusEntryIdentifier!
  "Error message describing why the update failed"
  error: String
}

"""
Input parameters for retrieving credential statuses.
"""
input GetCredentialStatusesInput {
  "List of credential entries to check status for"
  entries: [StatusEntryIdentifierInput!]!
}

"""
Result of retrieving credential statuses.
"""
type CredentialStatusesResult {
  "Status information for each requested credential"
  statuses: [EntryStatus!]!
}

"""
Status information for a credential entry.
"""
type EntryStatus {
  "The credential entry this status refers to"
  entry: StatusEntryIdentifier!
  "Whether the credential is revoked"
  revoked: Boolean
  "Whether the credential is suspended"
  suspended: Boolean
}

#
# Data types for Audit Logs
#

"""
AuditLog record data type.
"""
type AuditLog {
  "An identifier for the audit log"
  requestId: ID!
  "The identifier of the user which performed the request"
  userId: ID!
  "The type for the user ID; whether the user was authorized via Bearer Token or API Key"
  userIdType: AuditLogUserIdType!
  "The time the audit log was stored in ISO 8601 format"
  timestamp: String!
  "The operation which was logged by the service"
  operationId: String!
  "The arguments provided by the user for the operation"
  operationArgs: String!
  "The resulting data from the user performed operation"
  operationResultData: String
  "The category of the audit log operation"
  operationCategory: AuditLogOperationCategory!
  "Whether the operation was successful or not"
  isSuccess: Boolean!
  "The user agent which was supplied with the operation request"
  userAgent: String
  "The HTTP status result of the operation executed"
  httpStatus: Int
  "The HTTP method of the operation request"
  httpMethod: String
}

"""
Type for the user authorization which gives context to the user ID
"""
enum AuditLogUserIdType {
  "The logged userId is the ID of the API Key"
  API_KEY_ID
  "The logged userId is the ID of the Admin User via Bearer Token"
  ADMIN_USER_ID
  "The logged userId is the a SHA256 hash of the API Key"
  API_KEY_SHA256
  "No user ID was logged, the service was accessed without authorization"
  NO_AUTH
}

"""
Categorization of an audit log operation
"""
enum AuditLogOperationCategory {
  "The logged operation was performed via the /graphql API endpoint"
  GRAPHQL
  "The logged operation was performed via the /acapy API endpoint"
  ACAPY
}

"""
Optional filters for the 'auditLogs' query. The filters are AND'd together
"""
input AuditLogFilter {
  "Filter audit logs by request IDs"
  requestIds: [String!]
  "Filter audit logs by user IDs"
  userIds: [String!]
  "Filter audit logs by user ID types"
  userIdTypes: [AuditLogUserIdType!]
  "Filter audit logs by operation IDs"
  operationIds: [String!]
  "Filter audit logs by operation category"
  operationCategories: [AuditLogOperationCategory!]
  "Filter audit logs by success status"
  isSuccess: Boolean
  """
  Filter audit logs by start time in ISO 8601 format.
  Returned results will be equal to or older than the date provided
  """
  startTimestamp: String
  """
  Filter audit logs by end time in ISO 8601 format.
  Returned results will be equal to or newer than the date provided
  """
  endTimestamp: String
  "Returns the results in chronological or reverse chronological order"
  sortOrder: SortOrder
}

"""
Paged result container of the 'AuditLogResult' query
"""
type AuditLogResult {
  "The result items"
  items: [AuditLog]!
  "The next page token, or null if no more pages exist"
  nextToken: String
}

#
# Other data types
#

"""
Aries Interop Profile (AIP) version
"""
enum AIPVersion {
  V1
  V2
}

"""
Credential format
"""
enum CredentialFormat {
  ANONCREDS
  W3C
}

"""
Page information for queries that support pagination
"""
input Page {
  "The maximum number of items to return in the page"
  limit: Int = null
  "The token of the next page"
  nextToken: String = null
}

"""
Used to specify the order of result items returned by some queries.
"""
enum SortOrder {
  """
  Increasing order, arranges items from the lowest value to the highest value.
  For datetime values this is oldest to newest.
  """
  ASCENDING
  """
  Decreasing order, arranges items from the highest value to the lowest value.
  For datetime values this is newest to oldest.
  """
  DESCENDING
}

#
# Queries
#

type Query {
  """
  Gets information about a connection exchange.

  Returns the AIPConnectionExchange record if the connection exchange with the supplied ID exists.
  """
  aipConnectionExchange(
    "The ID of the connection exchange to retrieve."
    connExId: ID!
  ): AIPConnectionExchange

  """
  Gets a page of connection exchange records.

  Returns an AIPConnectionExchangesResult object with the connection exchanges that match the supplied input filters.
  """
  aipConnectionExchanges(
    "Page filter for the query"
    page: Page!
    "Data filter for the query"
    filter: AIPConnectionExchangesFilter = null
  ): AIPConnectionExchangesResult!

  """
  Gets information about a connection.

  Returns the Connection record if the connection with the supplied ID exists.
  """
  connection(
    "The ID of the connection to retrieve"
    connectionId: ID!
  ): Connection

  """
  Gets a page of connections records.

  Returns a ConnectionsResult object with the connections that match the supplied input filters.
  """
  connections(
    "Page filter for the query"
    page: Page!
    "Data filter for the query"
    filter: ConnectionsFilter = null
  ): ConnectionsResult!

  """
  Gets a page of basic message records.

  Returns a BasicMessagesResult object with the basic messages that match the supplied input filters.
  """
  basicMessages(
    "Page filter for the query"
    page: Page!
    "Data filter for the query"
    filter: BasicMessagesFilter!
  ): BasicMessagesResult!

  """
  Gets information about an Anoncreds Schema.

  Returns an AnoncredsSchema if the schema with the supplied ID exists.
  """
  anoncredsSchema(
    "The ID of the schema to retrieve"
    schemaId: ID!
  ): AnoncredsSchema

  """
  Gets a page of Anoncreds Schema records.

  Returns an AnoncredsSchemasResult object containing the schemas known to this agent that match the supplied input filters.
  """
  anoncredsSchemas(
    "Page filter for the query. The page limit defaults to 100"
    page: Page!
    "Optional data filters for the query"
    filter: AnoncredsSchemasFilter
  ): AnoncredsSchemasResult!

  """
  Gets information about an Anoncreds Credential Definition.

  Returns an AnoncredsCredDef if the credDef with the supplied ID exists.
  """
  anoncredsCredDef(
    "The ID of the credDef to retrieve"
    credDefId: ID!
  ): AnoncredsCredDef

  """
  Gets a page of Anoncreds Credential Definition records.

  Returns an AnoncredsCredDefsResult object containing the credDefs known to this agent that match the supplied input filters.
  """
  anoncredsCredDefs(
    "Page filter for the query. The page limit defaults to 100"
    page: Page!
    "Optional data filters for the query"
    filter: AnoncredsCredDefsFilter
  ): AnoncredsCredDefsResult!

  """
  Get a Credential by its ID.
  """
  credential(credentialId: ID!): Credential

  """
  Returns a list of Credentials.
  """
  credentials(page: Page!, filter: CredentialsFilter): CredentialsResult!

  """
  Get an AIPCredentialExchange by its ID.
  """
  aipCredentialExchange(credExId: ID!): AIPCredentialExchange

  """
  Returns a list of AIPCredentialExchanges.
  Tip: To fetch records of credentials issued by this agent, filter this query
  with 'myRole' equal to 'ISSUER' and 'state' equal to 'DONE', then read the
  AIPCredIssued message of each returned AIPCredentialExchange.
  """
  aipCredentialExchanges(
    page: Page!
    filter: AIPCredentialExchangesFilter
  ): AIPCredentialExchangesResult!

  """
  Get an AIPProofExchange by its ID.
  """
  aipProofExchange(proofExId: ID!): AIPProofExchange

  """
  Returns a list of AIPProofExchanges.
  Tip: To fetch proof presentations, filter this query with 'state' equal to 'DONE',
  then read the AIPProofPresentation message of each returned AIPProofExchange.
  """
  aipProofExchanges(
    page: Page!
    filter: AIPProofExchangesFilter
  ): AIPProofExchangesResult!

  """
  Get a list of Credential IDs which can satisfy a proof exchange where myRole=PROVER and state=REQUEST.
  """
  credentialsForAipProofExchange(
    proofExId: ID!
  ): CredentialsForAIPProofExchangeResult!

  """
  Gets the current primary DID for this agent.

  Returns the Did record if a primary DID is set, null otherwise.
  """
  primaryDid: Did

  """
  Gets information about a DID.

  Returns the Did record if the DID with the supplied identifier exists.
  """
  did("The DID identifier to retrieve" did: String!): Did

  """
  Gets a page of DID records.

  Returns a DidsResult object with the DIDs that match the supplied input filters.
  """
  dids(
    "Page filter for the query"
    page: Page!
    "Data filter for the query"
    filter: DidsFilter
  ): DidsResult!

  """
  Gets a status list definition by its identifier.

  Returns the StatusListDefinition if found, or null if no definition exists for the given identifier.
  """
  statusListDefinition(
    "The unique identifier for the status list definition"
    identifier: ID!
  ): StatusListDefinition

  """
  Gets all status list definitions.

  Returns a StatusListDefinitionsResult containing all status list definitions managed by this agent.
  """
  statusListDefinitions: StatusListDefinitionsResult!

  """
  Gets the current status of one or more credentials.
  
  Returns the revocation and suspension status for each requested credential entry.
  """
  credentialStatuses(input: GetCredentialStatusesInput!): CredentialStatusesResult!

  """
  Gets a page of AuditLog records.

  Returns an AuditLogsResult object containing the auditLogs that match the supplied input filters.
  """
  auditLogs(
    "Page filter for the query. The page limit defaults to 100"
    page: Page!
    "Optional data filters for the query"
    filter: AuditLogFilter
  ): AuditLogResult!
}

#
# Mutations
#

type Mutation {
  """
  Creates a connection invitation and an associated connection exchange in the INVITATION state.

  Returns the created connection exchange, with the invitation attached within.
  The invitation must be delivered to the invitee out-of-band.
  """
  createAipConnectionInvitation(
    invitation: CreateAipConnectionInvitationInput!
  ): AIPConnectionExchange!

  """
  Receives and accepts a connection invitation, and creates an associated connection exchange in the REQUEST state.

  Returns the created connection exchange.
  """
  acceptAipConnectionInvitation(
    invitation: AcceptAipConnectionInvitationInput!
  ): AIPConnectionExchange!

  """
  Deletes a connection exchange.

  Returns true if the operation completes with records deleted, false if not.
  """
  deleteAipConnectionExchange(connExId: ID!): Boolean!

  """
  Deletes a connection.

  Returns true if the operation completes with records deleted, false if not.
  """
  deleteConnection(
    "The ID of the connection to delete"
    connectionId: ID!
  ): Boolean!

  """
  Sends a basic message to a peer of an established connection.

  Returns the BasicMessage record that was created if the message was sent successfully.
  """
  sendBasicMessage(
    "The ID of the connection to send the message to"
    connectionId: ID!
    "The message content"
    content: String!
  ): BasicMessage!

  """
  Broadcasts a basic message to peers of a set of established connections. A new basic message record is created per connection.

  Returns a BroadcastBasicMessageResult object with the connection IDs of all peers that the message was successfully sent to.
  """
  broadcastBasicMessage(
    "The IDs of the connections to broadcast the message to"
    connectionIds: [ID!]!
    "The message content"
    content: String!
  ): BroadcastBasicMessageResult!

  """
  Deletes basic message records by ID. The messages are deleted locally, not on the other end of the connection.

  Returns a DeleteBasicMessagesResult object with the message IDs of all messages that were deleted.
  """
  deleteBasicMessages(
    "The IDs of the messages to delete"
    messageIds: [ID!]!
  ): DeleteBasicMessagesResult!

  """
  Deletes _all_ basic message records for a given connection. The messages are deleted locally, not on the other end of the connection.

  Returns true if the operation completes with records deleted, false if not.
  """
  deleteBasicMessagesForConnection(
    "The ID of the connection to delete all messages for"
    connectionId: ID!
  ): Boolean!

  """
  Creates new Anoncreds Schema.

  Returns the new Anoncreds Schema.
  """
  createAnoncredsSchema(args: CreateAnoncredsSchemaArgs!): AnoncredsSchema!

  """
  Creates new Anoncreds Credential Definition.

  Returns the new Anoncreds Credential Definition.
  """
  createAnoncredsCredDef(args: CreateAnoncredsCredDefArgs!): AnoncredsCredDef!

  """
  Deletes a credential.

  Returns true if the operation completes with records deleted, false if not.
  """
  deleteCredential(
    "The ID of the credential to delete"
    credentialId: ID!
  ): Boolean!

  """
  Signs a W3C Verifiable Credential using the specified cryptographic securing mechanism.
  """
  signCredential(input: SignCredentialInput!): SignCredentialResult!

  """
  Creates a new credential exchange in the OFFER state and sends the offer to the specified connection.
  This mutation is intended to be called by the ISSUER role.
  The offer follows AIP version 2.
  """
  sendAipCredentialOffer(
    connectionId: ID!
    offer: AIPCredOfferInput!
  ): AIPCredentialExchange!

  """
  Accepts a received credential offer and sends a follow-up credential request.
  This will move the state of the specified credential exchange from OFFER to REQUEST.
  This mutation is intended to be called by the HOLDER role.
  Supports both AIP versions 1 and 2.
  """
  acceptAipCredentialOffer(
    credExId: ID!
    acceptOffer: AcceptAipCredOfferInput!
  ): AIPCredentialExchange!

  """
  Accepts a received credential request and finally issues the verifiable credential.
  This will move the state of the specified credential exchange from REQUEST to ISSUED.
  This mutation is intended to be called by the ISSUER role.
  Supports both AIP versions 1 and 2.
  """
  acceptAipCredentialRequest(
    credExId: ID!
    acceptRequest: AcceptAipCredRequestInput!
  ): AIPCredentialExchange!

  """
  Deletes a credential exchange.

  Returns true if the operation completes with records deleted, false if not.
  """
  deleteAipCredentialExchange(
    "The ID of the credential exchange to delete"
    credExId: ID!
  ): Boolean!

  """
  Creates a new proof exchange in the REQUEST state and sends the request to the specified connection.
  This mutation is intended to be called by the VERIFIER role.
  The request follows AIP version 2.
  """
  sendAipProofRequest(
    connectionId: ID!
    request: AIPProofRequestInput!
  ): AIPProofExchange!

  """
  Accepts a received proof request and finally sends the verifiable presentation.
  This will move the state of the specified proof exchange from REQUEST to PRESENTED.
  This mutation is intended to be called by the PROVER role.
  Supports both AIP versions 1 and 2.
  """
  acceptAipProofRequest(
    proofExId: ID!
    presentation: AIPProofPresentationInput!
  ): AIPProofExchange!

  """
  Deletes a proof exchange.

  Returns true if the operation completes with records deleted, false if not.
  """
  deleteAipProofExchange(
    "The ID of the proof exchange to delete"
    proofExId: ID!
  ): Boolean!

  """
  Creates a new DID.

  Returns the created DID record.
  """
  createDid(input: CreateDidOptions!): Did!

  """
  Sets a DID as the primary DID for this agent, unsetting any existing primary DID.

  Returns the updated DID record.
  """
  setPrimaryDid("The DID identifier to set as primary" did: String!): Did!

  """
  Updates a DID.

  Returns the updated DID record.
  """
  updateDid(
    "The DID identifier to update"
    did: String!
    "The update options"
    input: UpdateDidOptions!
  ): Did!

  """
  Creates a new status list definition for credential status management.

  Creates status lists for the specified purposes (revocation and/or suspension) associated with a credential identifier.
  Returns the created StatusListDefinition with all configured status lists.
  """
  createStatusListDefinition(
    "The options for creating the status list definition"
    input: CreateStatusListDefinitionInput!
  ): StatusListDefinition!

  """
  Publishes status lists to make them publicly available.

  Publishes all status lists associated with a status list definition identifier.
  Returns the result indicating which lists were successfully published and any failures.
  """
  publishStatusList(
    "The input specifying which status lists to publish"
    input: PublishStatusListInput!
  ): PublishStatusListResult!

  """
  Assigns a credential to status lists for tracking revocation and suspension.
  
  Returns the W3C credential status objects that should be embedded in the credential.
  """
  assignCredentialToStatusList(input: AssignCredentialToStatusListInput!): StatusListAssignment!

  """
  Updates the revocation and suspension status of one or more credentials.
  
  Returns the results indicating which updates succeeded or failed.
  """
  updateCredentialStatuses(input: UpdateCredentialStatusesInput!): UpdateCredentialStatusesResult!
}

```




---

[Next Page](/llms-full.txt/1)

