# GDPR and privacy

Companies conducting business within the European Union are legally required to comply with the General Data Protection Regulation (GDPR).

While many aspects of this regulation may not significantly affect your integration with Stream, the GDPR provisions regarding the right to data access and the right to erasure are directly pertinent. These provisions relate to data that is stored and managed on Stream's servers.

Because of this, Stream provides a set of methods that make complying with those portions of the law easy. They work the same way for every Stream product.

## The Right to Access Data

GDPR gives EU citizens the right to request access to their information and the right to have access to this information in a portable format. Stream covers this requirement with the Export Users method.

This method can only be used with server-side authentication.

<Tabs>

```js label="Node.js"
const response = await serverClient.exportUsers({
  user_ids: [userID],
});
const taskID = response.task_id;
```

```python label="Python"
response = client.export_users(user_ids=[user_id])

task_id = response.data.task_id
```

```ruby label="Ruby"
require 'getstream_ruby'
Models = GetStream::Generated::Models

response = client.common.export_users(Models::ExportUsersRequest.new(
  user_ids: [user_id]
))

task_id = response.task_id
```

```php label="PHP"
$response = $client->exportUsers(new Models\ExportUsersRequest(
    userIds: [$userId],
));

$taskId = $response->getData()->taskID;
```

```go label="Go"
response, err := client.ExportUsers(ctx, &getstream.ExportUsersRequest{
  UserIds: []string{userID},
})

taskId := response.Data.TaskID
```

```csharp label="C#"
var exportResponse = await client.ExportUsersAsync(new ExportUsersRequest
{
    UserIds = new List<string> { userId },
});
var taskId = exportResponse.Data.TaskID;
```

```java label="Java"
var response = client.exportUsers(ExportUsersRequest.builder()
    .userIds(List.of(userId))
    .build()).execute().getData();

var taskId = response.getTaskID();
```

</Tabs>

The export runs asynchronously and returns a task ID. Poll the task until its status is `completed` to obtain a URL for downloading the exported data in line-separated JSON format. Each user's export contains their profile, messages, reactions, calls, and custom data.

<Admonition type="info">

A single request can export up to 25 users with a maximum of 10,000 messages per user. [Contact support](https://getstream.io/contact/support/) to export users with more than 10,000 messages.

</Admonition>

### Retrieving the export

You can check the status of an export request using the task ID returned when the task was created. The result of the task contains the URL to the JSON file. Poll the task status endpoint, or use the SDK helper that waits for completion, as described in [Async operations](https://getstream.io/docs/platform/async-operations/).

### Exporting feeds data

Feeds data is exported with its own endpoint. Where the user is the owner, `exportFeedUserData` exports:

- User data
- Feeds
- Follows
- Activities
- Comments
- Reactions
- Bookmarks
- Bookmark folders
- Collections

This method can only be used with server-side authentication.

<Tabs>

```js label="Node.js"
// Start the export task
const response = await client.feeds.exportFeedUserData({
  user_id: userToExport.id,
});

// You have to poll this endpoint
const taskResponse = await client.getTask({ id: response.task_id });
console.log(taskResponse.status === "completed");
```

```python label="Python"
# Start the export task
response = client.feeds.export_feed_user_data(user_id=user_to_export.id)

# You have to poll this endpoint
task_response = client.get_task(response.task_id)
print(task_response.status == "completed")
```

```ruby label="Ruby"
# Start the export task
response = client.feeds.export_feed_user_data(
  GetStream::Generated::Models::ExportFeedUserDataRequest.new(
    user_id: user_to_export.id
  )
)

# You have to poll this endpoint
task_response = client.get_task(response.data.task_id)
puts task_response.data.status == 'completed'
```

```php label="PHP"
// Start the export task
$response = $feedsClient->exportFeedUserData(
    new GeneratedModels\ExportFeedUserDataRequest(
        userID: $userToExport->id
    )
);

// You have to poll this endpoint
$taskResponse = $client->getTask($response->getData()->taskID);
echo $taskResponse->getData()->status === 'completed';
```

```go label="Go"
// Start the export task
response, err := client.Feeds().ExportFeedUserData(context.Background(), &getstream.ExportFeedUserDataRequest{
    UserID: userToExport.ID,
})
if err != nil {
    log.Fatal(err)
}

// You have to poll this endpoint
taskResponse, err := client.GetTask(context.Background(), response.Data.TaskID, &getstream.GetTaskRequest{})
if err != nil {
    log.Fatal(err)
}

fmt.Println(taskResponse.Data.Status == "completed")
```

```csharp label="C#"
// Start the export task
var response = await _feedsV3Client.ExportFeedUserDataAsync(
    new ExportFeedUserDataRequest { UserID = userToExport.ID }
);

// You have to poll this endpoint
var taskResponse = await _client.GetTaskAsync(response.TaskID);
Console.WriteLine(taskResponse.Status == "completed");
```

```java label="Java"
// Start the export task
ExportFeedUserDataResponse response = feeds.exportFeedUserData(
    ExportFeedUserDataRequest.builder()
        .userID(userToExport.getId())
        .build()
).execute().getData();

// You have to poll this endpoint
var taskResponse = client.getTask(response.getTaskId()).execute().getData();
System.out.println("completed".equals(taskResponse.getStatus()));
```

</Tabs>

Poll the task ID the same way as for Export Users; the task result contains the URL to the JSON file.

<Admonition type="info">

The URL to the export file expires after 24 hours. The link is generated every time you request the export status. The export stays available for 60 days.

</Admonition>

## The Right to Erasure

The GDPR also grants EU citizens the right to request the deletion of their personal information. Stream offers mechanisms to delete users and their data in accordance with various use cases, ensuring compliance with these regulations.

### Deleting users

<Tabs>

```js label="Node.js"
client.deleteUsers({ user_ids: ["<id>"] });

//restore
client.restoreUsers({ user_ids: ["<id>"] });
```

```python label="Python"
# Delete users
client.delete_users(user_ids=["<id>"])

# Restore users
client.restore_users(user_ids=["<id>"])
```

```ruby label="Ruby"
# Delete users
client.delete_users(
  GetStream::Generated::Models::DeleteUsersRequest.new(
    user_ids: ['<id>']
  )
)

# Restore users
client.restore_users(
  GetStream::Generated::Models::RestoreUsersRequest.new(
    user_ids: ['<id>']
  )
)
```

```php label="PHP"
// Delete users (soft delete by default)
$client->deleteUsers(
    new GeneratedModels\DeleteUsersRequest(
        userIds: ['<id>']
    )
);

// Delete users with options (hard delete)
$client->deleteUsers(
    new GeneratedModels\DeleteUsersRequest(
        userIds: ['<id>'],
        user: 'hard',
        messages: 'hard',
        conversations: 'hard',
        newChannelOwnerID: 'new-owner-id'
    )
);

// Restore users
$client->restoreUsers(
    new GeneratedModels\RestoreUsersRequest(
        userIds: ['<id>']
    )
);
```

```go label="Go"
// Delete users
deleteRequest := &getstream.DeleteUsersRequest{
    UserIds: []string{"<id>"},
}
_, err = client.DeleteUsers(context.Background(), deleteRequest)

// Restore users
restoreRequest := &getstream.RestoreUsersRequest{
    UserIds: []string{"<id>"},
}
_, err = client.RestoreUsers(context.Background(), restoreRequest)
```

```csharp label="C#"
// Delete users
await _client.DeleteUsersAsync(new DeleteUsersRequest
{
    UserIds = new List<string> { "<id>" }
});

// Restore users
await _client.RestoreUsersAsync(new RestoreUsersRequest
{
    UserIds = new List<string> { "<id>" }
});
```

```java label="Java"
// Delete users
client.deleteUsers(
    DeleteUsersRequest.builder()
        .userIds(Arrays.asList("<id>"))
        .build()
).execute();

// Restore users
client.restoreUsers(
    RestoreUsersRequest.builder()
        .userIds(Arrays.asList("<id>"))
        .build()
).execute();
```

</Tabs>

By default users and their data are soft-deleted. The delete users endpoint supports the following parameters to control which data needs to be deleted and how.

| Name                   | Type                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Optional |
| ---------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `user`                 | Enum (soft, pruning, hard) | - Soft: marks user as deleted and retains all user data. <br /> - Pruning: marks user as deleted and nullifies user information. <br /> - Hard: deletes user completely. <br /><br /> When `user` is `hard` and `messages` or `conversations` is omitted, the omitted field is automatically set to `hard`. If either is explicitly set to a non-hard value, the request returns an error.                                                                                                                                                                | Yes      |
| `conversations`        | Enum (soft, hard)          | - Soft: marks all conversation channels as deleted (same effect as Delete Channels with 'hard' option disabled). <br /> - Hard: deletes channel and all its data completely including messages (same effect as Delete Channels with 'hard' option enabled). <br /><br /> A conversation channel is any channel with two or fewer members where one member is the deleted user. <br /><br /> When `user` is `hard`, omitting this field automatically promotes it to `hard`. Setting `conversations` to `soft` with `user` set to `hard` returns an error. | Yes      |
| `messages`             | Enum (soft, pruning, hard) | - Soft: marks all user messages as deleted without removing any related message data. <br /> - Pruning: marks all user messages as deleted, nullifies message information and removes some message data such as reactions and flags. <br /> - Hard: deletes messages completely with all related information. <br /><br /> When `user` is `hard`, omitting this field automatically promotes it to `hard`. Setting `messages` to `soft` or `pruning` with `user` set to `hard` returns an error.                                                          | Yes      |
| `new_channel_owner_id` | string                     | Channels owned by hard-deleted users will be transferred to this userID. If you don't provide a value, the channel owner will have a system generated ID like `delete-user-8219f6578a7395g`                                                                                                                                                                                                                                                                                                                                                               | Yes      |
| `calls`                | Enum (soft, hard)          | - Soft: marks calls and related data as deleted. <br /> - Hard: deletes calls and related data completely <br /> Note that this applies only to 1:1 calls, not group calls                                                                                                                                                                                                                                                                                                                                                                                | Yes      |

<Admonition type="caution">

When `user` is set to `hard`, the server requires that both `messages` and `conversations` are also `hard`. If either field is omitted, it is automatically promoted to `hard`. If either is explicitly set to a non-hard value (`soft` or `pruning`), the request returns a validation error.

</Admonition>

Deleting users is done asynchronously and can take some time to complete. A single request can delete up to 100 users. Monitor the returned task ID the same way as for exports.

After deletion, a user can no longer connect to Stream or be displayed when querying users. For the rest of the user lifecycle (deactivating, reactivating, restoring), see [Users](https://getstream.io/docs/platform/users/).

### Deleting product data

Each product has its own deletion endpoints and semantics for the resources a user leaves behind:

- Chat: [deleting a batch of users](https://getstream.io/docs/platform/users/#deleting-users) and [deleting a batch of channels](https://getstream.io/chat/docs/node/channel-delete/#deleting-many-channels)
- Video: [deleting calls](https://getstream.io/video/docs/api/gdpr/calls/)
- Feeds: [deleting a user's feeds data](#deleting-feeds-data), covered below

### Deleting feeds data

Use `deleteFeedUserData` to erase a user's feeds data without deleting the user account itself. The following data will be deleted:

- Follows where the user owns either the source or the target feed
- Feeds owned by the user, following the logic described in [Deleting a Feed](https://getstream.io/activity-feeds/docs/node/feeds/#deleting-a-feed)
- Activities owned by the user
- Comments owned by the user
- Reactions owned by the user
- Bookmarks and bookmark folders owned by the user
- Collections owned by the user

The `hard_delete` flag only affects how feeds, activities, comments, reactions, bookmarks, bookmark folders, and collections are removed. With `hard_delete: false` (the default), these entities are soft-deleted: the rows stay in the database with a `deleted_at` timestamp set and are filtered out of reads. With `hard_delete: true`, the rows are physically removed.

Follows are always hard-deleted regardless of the `hard_delete` flag. Both follow rows where the user is the source and rows where the user is the target are physically removed; there is no soft-delete state for follows. The deleted user's activities immediately stop appearing in the feeds of users who used to follow them, and the deleted user's own following list is cleared in full.

<Admonition type="danger">

Deleting feeds data is an irreversible operation. This goes for **both soft and hard deletes**.

</Admonition>

<Tabs>

```js label="Node.js"
// Start the delete task
const response = await serverClient.feeds.deleteFeedUserData({
  user_id: userToDelete.id,
  hard_delete: false,
});

// You have to poll this endpoint
const taskResponse = await client.getTask({ id: response.task_id });
console.log(taskResponse.status === "completed");
```

```python label="Python"
# Start the delete task
response = client.feeds.delete_feed_user_data(
    user_id=user_to_delete.id,
    hard_delete=False
)

# You have to poll this endpoint
task_response = client.get_task(response.task_id)
print(task_response.status == "completed")
```

```ruby label="Ruby"
# Start the delete task
response = client.feeds.delete_feed_user_data(
  GetStream::Generated::Models::DeleteFeedUserDataRequest.new(
    user_id: user_to_delete.id,
    hard_delete: false
  )
)

# You have to poll this endpoint
task_response = client.get_task(response.data.task_id)
puts task_response.data.status == 'completed'
```

```php label="PHP"
// Start the delete task
$response = $feedsClient->deleteFeedUserData(
    new GeneratedModels\DeleteFeedUserDataRequest(
        userID: $userToDelete->id,
        hardDelete: false
    )
);

// You have to poll this endpoint
$taskResponse = $client->getTask($response->getData()->taskID);
echo $taskResponse->getData()->status === 'completed';
```

```go label="Go"
// Start the delete task
response, err := client.Feeds().DeleteFeedUserData(context.Background(), &getstream.DeleteFeedUserDataRequest{
    UserID:     userToDelete.ID,
    HardDelete: getstream.PtrTo(false),
})
if err != nil {
    log.Fatal(err)
}

// You have to poll this endpoint
taskResponse, err := client.GetTask(context.Background(), response.Data.TaskID, &getstream.GetTaskRequest{})
if err != nil {
    log.Fatal(err)
}

fmt.Println(taskResponse.Data.Status == "completed")
```

```csharp label="C#"
// Start the delete task
var response = await _feedsV3Client.DeleteFeedUserDataAsync(
    new DeleteFeedUserDataRequest
    {
        UserID = userToDelete.ID,
        HardDelete = false
    }
);

// You have to poll this endpoint
var taskResponse = await _client.GetTaskAsync(response.TaskID);
Console.WriteLine(taskResponse.Status == "completed");
```

```java label="Java"
// Start the delete task
DeleteFeedUserDataResponse response = feeds.deleteFeedUserData(
    DeleteFeedUserDataRequest.builder()
        .userID(userToDelete.getId())
        .hardDelete(false)
        .build()
).execute().getData();

// You have to poll this endpoint
var taskResponse = client.getTask(response.getTaskId()).execute().getData();
System.out.println("completed".equals(taskResponse.getStatus()));
```

</Tabs>

## PDPB

The same API endpoints documented here can also be used to comply with India's Personal Data Protection Bill (PDPB) requirements.


---

This page was last updated at 2026-09-08T17:14:08.970Z.

For the most recent version of this documentation, visit [https://getstream.io/docs/platform/gdpr/](https://getstream.io/docs/platform/gdpr/).