> For the complete documentation index, see [llms.txt](https://docs.sudoplatform.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sudoplatform.com/guides/digital-account-protection/subscriptions.md).

# Subscriptions

The Privacy Interaction SDK supports real-time subscriptions that notify your application when resources change.

Rather than polling for updates, you can subscribe to events and react as scanning progresses, data holders are discovered, and analysis results are updated.

### Virtual Presence Subscriptions <a href="#virtual-presence-subscriptions" id="virtual-presence-subscriptions"></a>

Subscribe to virtual presence state changes (e.g., transitions to `SCANNING` or `NEEDS_REAUTH`). Provide a unique subscription id to safely manage the subscription lifecycle.&#x20;

#### Subscribe <a href="#subscribe" id="subscribe"></a>

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

```typescript
await privacyInteractionClient.subscribeToVirtualPresence(
  'my-subscription-id',
  {
    virtualPresenceUpdated(virtualPresence) {
      console.log('Virtual presence updated:', virtualPresence.id, virtualPresence.state)
    },
    connectionStatusChanged(state) {
      console.log('Connection state:', state)
      if (state === ConnectionState.Disconnected) {
        // Handle disconnection: consider re-subscribing
      }
    },
  },
)
```

{% endtab %}

{% tab title="Swift" %}

```swift
final class MyVirtualPresenceSubscriber: VirtualPresenceSubscriber {
    func virtualPresenceUpdated(_ virtualPresence: VirtualPresence) {
        print("Virtual presence updated:", virtualPresence.id, virtualPresence.state)
    }

    func connectionStatusChanged(_ state: ConnectionState) {
        print("Connection state:", state)
        if state == .disconnected {
            // Handle disconnection: consider re-subscribing
        }
    }
}

do {
    let subscriber = MyVirtualPresenceSubscriber()
    try await privacyInteractionClient.subscribeToVirtualPresence(
        subscriptionId: "my-subscription-id",
        subscriber: subscriber
    )
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        withContext(Dispatchers.IO) {
            privacyInteractionClient.subscribeToVirtualPresence(
                "my-subscription-id",
                object : VirtualPresenceSubscriber {
                    override fun virtualPresenceUpdated(virtualPresence: VirtualPresence) {
                        println("Virtual presence updated: ${virtualPresence.id} ${virtualPresence.state}")
                    }

                    override fun connectionStatusChanged(state: Subscriber.ConnectionState) {
                        println("Connection state: $state")
                        if (state == Subscriber.ConnectionState.DISCONNECTED) {
                            // Handle disconnection: consider re-subscribing
                        }
                    }
                },
            )
        }
    } catch (e: VirtualPresenceException) {
        // Handle/notify user of errors
    }
}

```

{% endtab %}
{% endtabs %}

The subscriber receives a full `VirtualPresence` object on each update.

#### Unsubscribe <a href="#unsubscribe" id="unsubscribe"></a>

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

```typescript
privacyInteractionClient.unsubscribeFromVirtualPresence('my-subscription-id')
```

{% endtab %}

{% tab title="Swift" %}

```swift
privacyInteractionClient.unsubscribeFromVirtualPresence(subscriptionId: "my-subscription-id")
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        privacyInteractionClient.unsubscribeFromVirtualPresence('my-subscription-id')
        // or
        privacyInteractionClient.unsubscribeAllFromVirtualPresence()
    } catch (e: VirtualPresenceException) {
        // Handle/notify the user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Data Holder Subscriptions <a href="#data-holder-subscriptions" id="data-holder-subscriptions"></a>

Subscribe to data holder updates. Provide a unique subscription id to safely manage the subscription lifecycle.&#x20;

Notifications are emitted as data holders are discovered or updated during scanning. Updates are delivered in batches.&#x20;

#### Subscribe <a href="#subscribe-1" id="subscribe-1"></a>

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

```typescript
await privacyInteractionClient.subscribeToDataHolders(
  'my-subscription-id',
  {
    dataHoldersUpdated(dataHolders) {
      console.log(`${dataHolders.length} data holders updated`)
      for (const dh of dataHolders) {
        console.log(dh.name, dh.protectionState)
      }
    },
    connectionStatusChanged(state) {
      console.log('Connection state:', state)
    },
  },
)
```

{% endtab %}

{% tab title="Swift" %}

```swift
final class MyDataHolderSubscriber: DataHolderSubscriber {
    func dataHoldersUpdated(_ dataHolders: [DataHolder]) {
        print("\(dataHolders.count) data holders updated")
        for dh in dataHolders {
            print(dh.name, dh.protectionState)
        }
    }

    func connectionStatusChanged(_ state: ConnectionState) {
        print("Connection state:", state)
    }
}

do {
    let subscriber = MyDataHolderSubscriber()
    try await privacyInteractionClient.subscribeToDataHolders(
        subscriptionId: "my-subscription-id",
        subscriber: subscriber
    )
} catch {
    // Handle/notify user of errors
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        withContext(Dispatchers.IO) {
            privacyInteractionClient.subscribeToDataHolders(
                "my-subscription-id",
                object : DataHolderSubscriber {
                    override fun dataHoldersUpdated(dataHolders: List<DataHolder>) {
                        println("${dataHolders.size} data holders updated")
                        for (dh in dataHolders) {
                            println("${dh.name} ${dh.protectionState}")
                        }
                    }

                    override fun connectionStatusChanged(state: Subscriber.ConnectionState) {
                        println("Connection state: $state")
                    }
                },
            )
        }
    } catch (e: DataHolderException) {
        // Handle/notify user of errors
    }
}
```

{% endtab %}
{% endtabs %}

The subscriber receives an array of `DataHolder` objects on each update batch.

#### Unsubscribe <a href="#unsubscribe-1" id="unsubscribe-1"></a>

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

```typescript
privacyInteractionClient.unsubscribeFromDataHolders('my-subscription-id')
```

{% endtab %}

{% tab title="Swift" %}

```swift
privacyInteractionClient.unsubscribeFromDataHolders(subscriptionId: "my-subscription-id")
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        privacyInteractionClient.unsubscribeFromDataHolders('my-subscription-id')
        // or
        privacyInteractionClient.unsubscribeAllFromDataHolders()
    } catch (e: DataHolderException) {
        // Handle/notify the user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Analysis Result Subscriptions <a href="#analysis-result-subscriptions" id="analysis-result-subscriptions"></a>

Subscribe to analysis result changes. Provide a unique subscription id to safely manage the subscription lifecycle.&#x20;

Notifications are emitted when a new analysis result is created, or when an analysis result changes status (e.g., from `PENDING` to `COMPLETE`) or is updated with new data.

#### Subscribe <a href="#subscribe-2" id="subscribe-2"></a>

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

```typescript
await privacyInteractionClient.subscribeToAnalysisResult(
  'my-subscription-id',
  {
    analysisResultUpdated(update) {
      console.log('Analysis result updated:', update.id, update.status)
      if (update.status === 'COMPLETE') {
        // Fetch the full analysis result to get the data payload
        const full = await privacyInteractionClient.getAnalysisResult(update.id)
      }
    },
    connectionStatusChanged(state) {
      console.log('Connection state:', state)
    },
  },
)
```

{% endtab %}

{% tab title="Swift" %}

```swift
final class MyAnalysisResultSubscriber: AnalysisResultSubscriber {
    func analysisResultUpdated(_ update: AnalysisResultUpdate) {
        print("Analysis result updated:", update.id, update.status)
        if update.status == .complete {
            // The notification is lightweight. Fetch the full analysis result
            // for the data payload:
            Task {
                let full = try? await privacyInteractionClient.getAnalysisResult(withId: update.id)
            }
        }
    }

    func connectionStatusChanged(_ state: ConnectionState) {
        print("Connection state:", state)
    }
}

do {
    let subscriber = MyAnalysisResultSubscriber()
    try await privacyInteractionClient.subscribeToAnalysisResult(
        subscriptionId: "my-subscription-id",
        subscriber: subscriber
    )
} catch {
    // Handle/notify user of errors
}

```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        withContext(Dispatchers.IO) {
            privacyInteractionClient.subscribeToAnalysisResult(
                "my-subscription-id",
                object : AnalysisResultSubscriber {
                    override fun analysisResultUpdated(update: AnalysisResultUpdate) {
                        println("Analysis result updated: ${update.id} ${update.status}")
                        if (update.status == AnalysisResultStatus.COMPLETE) {
                            // Fetch the full analysis result to get the data payload.
                            // (call getAnalysisResult(update.id) from a coroutine)
                        }
                    }

                    override fun connectionStatusChanged(state: Subscriber.ConnectionState) {
                        println("Connection state: $state")
                    }
                },
            )
        }
    } catch (e: AnalysisResultException) {
        // Handle/notify user of errors
    }
}
```

{% endtab %}
{% endtabs %}

The subscriber receives an `AnalysisResultUpdate` object; a lightweight notification containing the analysis result's metadata and status but not the full `data` payload. To retrieve the complete analysis data, call `getAnalysisResult` with the update's `id`.

#### Unsubscribe <a href="#unsubscribe-2" id="unsubscribe-2"></a>

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

```typescript
privacyInteractionClient.unsubscribeFromAnalysisResult('my-subscription-id')
```

{% endtab %}

{% tab title="Swift" %}

```swift
privacyInteractionClient.unsubscribeFromAnalysisResult(subscriptionId: "my-subscription-id")
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        privacyInteractionClient.unsubscribeFromAnalysisResult('my-subscription-id')
        // or
        privacyInteractionClient.unsubscribeAllFromAnalysisResult()
    } catch (e: AnalysisResultException) {
        // Handle/notify the user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Action Subscriptions <a href="#connection-state" id="connection-state"></a>

#### Subscribing to Action Updates <a href="#subscribing-to-action-updates" id="subscribing-to-action-updates"></a>

To receive real-time notifications each time an action's status changes, subscribe with a unique subscription identifier and an `ActionSubscriber` implementation. Your subscriber is notified whenever an action is updated — for example, when it transitions from `IN_PROGRESS` to `COMPLETED_UNVERIFIED`, or into `REQUIRES_USER_INPUT`.

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

```typescript
const subscriptionId = 'my-action-subscription'

await privacyInteractionClient.subscribeToActions(subscriptionId, {
  actionUpdated(action) {
    // An action's status has changed.
    // Inspect `action.status` to react to the update.
  },
  connectionStatusChanged(state) {
    if (state === ConnectionState.Disconnected) {
      // The subscription has disconnected; no further updates will be received
      // until it reconnects.
    }
  },
})
```

{% endtab %}

{% tab title="Swift" %}

```swift
final class MyActionSubscriber: ActionSubscriber {
    func actionUpdated(_ action: Action) {
        // An action's status has changed.
        // Inspect `action.status` to react to the update.
    }

    func connectionStatusChanged(_ state: ConnectionState) {
        if state == .disconnected {
            // The subscription has disconnected; no further updates will be received
            // until it reconnects.
        }
    }
}

do {
    let subscriber = MyActionSubscriber()
    try await privacyInteractionClient.subscribeToAction(
        subscriptionId: "my-action-subscription",
        subscriber: subscriber
    )
} catch {
    // Handle/notify user of errors
}

```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
val subscriptionId = "my-action-subscription"
launch {
    try {
        withContext(Dispatchers.IO) {
            privacyInteractionClient.subscribeToActions(
                subscriptionId,
                object : ActionSubscriber {
                    override fun actionUpdated(action: Action) {
                        // An action's status has changed.
                        // Inspect `action.status` to react to the update.
                    }

                    override fun connectionStatusChanged(state: Subscriber.ConnectionState) {
                        if (state == Subscriber.ConnectionState.DISCONNECTED) {
                            // The subscription has disconnected; no further updates will be
                            // received until it reconnects.
                        }
                    }
                },
            )
        }
    } catch (e: ActionException) {
        // Handle/notify user of errors
    }
}

```

{% endtab %}
{% endtabs %}

#### Unsubscribe

When you no longer need updates, unsubscribe using the same subscription identifier.

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

```typescript
privacyInteractionClient.unsubscribeFromActions('my-action-subscription')
```

{% endtab %}

{% tab title="Swift" %}

```swift
privacyInteractionClient.unsubscribeFromAction(subscriptionId: "my-action-subscription")
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
launch {
    try {
        privacyInteractionClient.unsubscribeFromActions('my-subscription-id')
        // or
        privacyInteractionClient.unsubscribeAllFromActions()
    } catch (e: ActionException) {
        // Handle/notify the user of exception
    }
}
```

{% endtab %}
{% endtabs %}

### Connection State <a href="#connection-state" id="connection-state"></a>

All subscription interfaces include a `connectionStatusChanged` callback that notifies you when the subscription connection state changes. Your subscriber will not receive resource update events until the connection state is `CONNECTED`, and will stop receiving them when it transitions to `DISCONNECTED`.

| State          | Description                                                   |
| -------------- | ------------------------------------------------------------- |
| `CONNECTED`    | The subscription is actively connected and receiving updates. |
| `DISCONNECTED` | The subscription is not connected.                            |

### Subscription Best Practices <a href="#subscription-best-practices" id="subscription-best-practices"></a>

* **Use unique subscription IDs.** If you call a subscribe method with the same ID twice, the second call will replace the first subscription.
* **Handle disconnections.** Monitor the `connectionStatusChanged` callback and re-subscribe if the connection is lost unexpectedly.
* **Unsubscribe when done.** Always unsubscribe when your component unmounts or the user navigates away to avoid memory leaks and unnecessary network activity.
* **Keep callbacks lightweight.** Subscription callbacks are invoked on the main thread. Perform heavy processing asynchronously to avoid blocking the UI.
* **Fetch full data on update.** For analysis results, the subscription notification is lightweight. Fetch the full resource via `getAnalysisResult` when you need the complete data payload.
