---
title: "Automate right to erasure requests"
url: /docs/en-us/cloud/webhooks/automate-right-to-erasure
last_updated: 2026-08-13T00:14:36Z
description: "Explains how to automate Right to Erasure requests with webhooks and Open Cloud APIs for data stores."
keywords: ["GDPR","CCPA","LGPD","PIPA","right to erasure","right to be forgotten","data protection","privacy","PII","personal data"]
---

# Automate right to erasure requests

Global data protection and privacy regulations grant individuals the right to control their data, including the right to request its deletion (often referred to as the "right to erasure" or "right to delete"). If you store any personal data or **Personally Identifiable Information (PII)**, such as User IDs, you are responsible for complying with applicable privacy framework(s) by permanently deleting this information upon receiving a user request. More information can be found in [RTBF and creators](/docs/en-us/production/publishing/RTBF-and-creators.md).

Instead of handling requests manually, you can [set up a webhook](/docs/en-us/cloud/webhooks/webhook-notifications.md) and use a bot within a third-party messaging application to automate the process. As [data stores](/docs/en-us/cloud-services/data-stores.md) being the most common way for storing PII data, this tutorial provides an example on how to create a bot within Discord that uses the [Open Cloud API for data stores](/docs/en-us/cloud/guides/data-stores.md) to delete PII data as an automation solution.

## Workflow

Upon completing this tutorial, you should be able to create a locally-running custom program that automates the handling of right to erasure requests from users. The workflow for this process is as follows:

1. Roblox Support receives a right to erasure request from a user.
2. Roblox webhook is triggered, containing the User ID and a list of Start Place IDs for the games they have joined in the payload.
3. Your bot listens for these webhook notifications, verifies their authenticity, and utilizes the [Open Cloud API for data stores](/docs/en-us/cloud/guides/data-stores.md) to delete the PII data stored in data stores. 
  > **Warning:** To use this solution, make sure your data store keys are identifiable by User IDs, such as containing User IDs as substrings, or you need to modify the scripts to match your own data schema.
4. The bot responds to the webhook message in Discord with the deletion status.

## Configure a webhook with third-party integration

Before creating a bot, set up a server with webhook integration on the third-party messaging application. Then use the server to configure a webhook on Creator Dashboard.

### Set up a server

The following steps show how to set up the server using Discord.

1. Create a new Discord server. If you are unfamiliar with the process, see [Discord Support](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-).
  > **Info:** It's recommended to set your server as a private server to protect user security. See [Discord Support](https://support.discord.com/hc/en-us/articles/206143407-How-do-I-set-up-a-private-server-) if you are unfamiliar with the process.
2. The server automatically creates a **#general** channel as your default channel. Click the **Edit Channel** icon of the **#general** channel.
3. Under **Permissions**, set the channel to private.
4. Create a webhook integration with the new server, name it to `RTBF Hook`. If you are unfamiliar with the process, see [Discord Support](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks).
5. Copy the webhook URL and store it in a secure place. Only allow trusted team members to access it, as leaking the URL can enable bad actors to send fake messages and potentially delete your user data.

### Configure a webhook on Roblox

After obtaining the third-party server URL, use it to [configure a webhook](/docs/en-us/cloud/webhooks/webhook-notifications.md#configure-webhooks-on-creator-dashboard) on Creator Dashboard. make sure you perform the following settings:

> **Info:** Currently, only group owners can receive Right to Erasure requests for group-owned games. To implement the automation solution for a group-owned game, make sure that the group owner configures the webhook.

- Add the Discord server URL as the **Webhook URL**.
- Include a custom **Secret**. Though a secret is optional for completing the configuration, you should include one to prevent bad actors from impersonating Roblox and deleting your data. For more information on the usage of a secret, see [Verify webhook security](/docs/en-us/cloud/webhooks/webhook-notifications.md#verifying-webhook-security).
- Select **Right to Erasure Request** under **Triggers**.

You can test the webhook using the **Test Response** button to see if you receive a notification in your server's **#general** channel from Roblox. If you don't receive the notification, try again or check your server settings to troubleshoot the error.

## Configure a bot

After you add the webhook, use it to configure the bot with the following steps. For more information, see the [Discord documentation](https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts).

1. Navigate to the [Applications page](https://discord.com/developers/applications).
2. Create a new application and name it `RTBF Bot`.
3. The system redirects you to the **General Information** settings of the bot. Copy and save its application ID in a secure place.
4. Under the settings menu, select **OAuth2**.
5. Navigate to the **OAuth2** settings
  1. Enable the **bot** scope, a list of additional Bot Permissions displays.
  2. Add the **Administrator** permission. Save your changes.
  3. Save the generated URL.
6. Copy the generated URL. Leave the application settings page unclosed.
7. Navigate to the generated URL. Select the target server. Click the **Continue** button and then the **Authorize** button.
8. Navigate back to application settings page and navigate to the Bot settings.
9. Under the **Privileged Gateway Intents** section, enable **Message Content Intent**.
10. In the Bot settings > **Build-A-Bot** section, save the bot token in a secure place for later steps. If you don't see the token, click the **Reset Token** button to generate a new one.

## Create an Open Cloud API key

To allow your third-party bot to access your data stores for storing PII data of users, [create an Open Cloud API key](/docs/en-us/cloud/auth/api-keys.md) that can access your games and add the **Delete Entry** permission of data stores for data deletion. If you use ordered data stores for storing PII, you also need to add the **Write** permission of ordered data stores. After completion, copy and save the API key in a secure location to use it in later steps.

## Obtain identifiers of games and places

For the bot to locate the PII data requested by users for deletion, obtain the following identifiers of all games that you intend to use the bot for:

- The **Universe ID**, the unique identifier of your game.
- The **Start Place ID**, the unique identifier of the start place of a game.

To obtain these identifiers:

1. Navigate to the [Creator Dashboard](https://create.roblox.com/dashboard/creations).
2. Hover over a game's thumbnail, click the **⋯** button, and select **Copy Universe ID** and **Copy Start Place ID**, respectively.

## Add scripts

After you finish setting up the webhook, bot, and API key for data stores, add them to the scripts that implement the bot's automation logic. The following example uses Python 3.

1. Install Python libraries using the following commands:```bash
pip3 install discord
pip3 install requests
pip3 install urllib3==1.26.6
```
2. Copy and save the following scripts corresponding to different parts of the bot logic in the same directory:```python
BOT_TOKEN = ""
OPEN_CLOUD_API_KEY = ""
ROBLOX_WEBHOOK_SECRET = ""

# Dictionary of the Start place ID to
# (universe ID, list of (data stores name, scope, and entry key)) for
# Standard Data Stores
# User data stored under these entries will be deleted

STANDARD_DATA_STORE_ENTRIES = {
    # Start Place ID
    111111111: (
        # Universe ID
        222222222,
        [
            ("StandardDataStore1", "Scope1", "Key1_{user_id}"),
            ("StandardDataStore1", "Scope1", "Key2_{user_id}"),
            ("StandardDataStore2", "Scope1", "Key3_{user_id}")
        ]
    ),
    33333333: (
        444444444,
        [
            ("StandardDataStore3", "Scope1", "Key1_{user_id}")
        ]
    )
}

# Dictionary of the Start place ID to
# (universe ID, list of (data stores name, scope, and entry key)) for
# Ordered Data Stores
# User data stored under these entries will be deleted

ORDERED_DATA_STORE_ENTRIES = {
    111111111: (
        222222222,
        [
            ("OrderedDataStore1", "Scope2", "Key4_{user_id}")
        ]
    )
}
``````python
import requests
import bot_config
from collections import defaultdict

"""
Calls Data Stores Open Cloud API to delete all entries for a user_id configured in
STANDARD_DATA_STORE_ENTRIES. Returns a list of successful deletions and failures to delete.
"""
def delete_standard_data_stores(user_id, start_place_ids):
    successes = defaultdict(list)
    failures = defaultdict(list)
    for owned_start_place_id in bot_config.STANDARD_DATA_STORE_ENTRIES:
        if owned_start_place_id not in start_place_ids:
            continue
        universe_id, universe_entries = bot_config.STANDARD_DATA_STORE_ENTRIES[owned_start_place_id]
        for (data_store_name, scope, entry_key) in universe_entries:
            entry_key = entry_key.replace("{user_id}", user_id)
            response = requests.delete(
                f"https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores/datastore/entries/entry",
                headers={"x-api-key": bot_config.OPEN_CLOUD_API_KEY},
                params={
                    "datastoreName": data_store_name,
                    "scope": scope,
                    "entryKey": entry_key
                }
            )
            if response.status_code in [200, 204]:
                successes[owned_start_place_id].append((data_store_name, scope, entry_key))
            else:
                failures[owned_start_place_id].append((data_store_name, scope, entry_key))
    return successes, failures

"""
Calls Ordered Data Stores Open Cloud API to delete all entries for a user_id configured in
ORDERED_DATA_STORE_ENTRIES. Returns a list of successful deletions and failures to delete.
"""
def delete_ordered_data_stores(user_id, start_place_ids):
    successes = defaultdict(list)
    failures = defaultdict(list)
    for owned_start_place_id in bot_config.ORDERED_DATA_STORE_ENTRIES:
        if owned_start_place_id not in start_place_ids:
            continue
        universe_id, universe_entries = bot_config.ORDERED_DATA_STORE_ENTRIES[owned_start_place_id]
        for (data_store_name, scope, entry_key) in universe_entries:
            entry_key = entry_key.replace("{user_id}", user_id)
            response = requests.delete(
                f"https://apis.roblox.com/ordered-data-stores/v1/universes/{universe_id}/orderedDatastores/{data_store_name}/scopes/{scope}/entries/{entry_key}",
                headers={"x-api-key": bot_config.OPEN_CLOUD_API_KEY}
            )
            if response.status_code in [200, 204, 404]:
                successes[owned_start_place_id].append((data_store_name, scope, entry_key))
            else:
                failures[owned_start_place_id].append((data_store_name, scope, entry_key))
    return successes, failures
``````python
import time
import hmac
import hashlib
import re
import base64

import bot_config

"""
Parses received message for Roblox signature and timestamp, the footer is only set if you
configured webhook secret
"""
def parse_footer(message):
    if not message.embeds[0].footer or \
        not message.embeds[0].footer.text:
        return "", 0
    footer_match = re.match(
        r"Roblox-Signature: (.*), Timestamp: (.*)",
        message.embeds[0].footer.text
    )
    if not footer_match:
        return "", 0
    else:
        signature = footer_match.group(1)
        timestamp = int(footer_match.group(2))
        return signature, timestamp

"""
Verifies Roblox signature with configured secret to check for validity
"""
def validate_signature(message, signature, timestamp):
    if not message or not signature or not timestamp:
        return False

    # Prevents replay attack within 300 seconds window
    request_timestamp_ms = timestamp * 1000
    window_time_ms = 300 * 1000
    oldest_timestamp_allowed = round(time.time() * 1000) - window_time_ms
    if request_timestamp_ms < oldest_timestamp_allowed:
        return False

    # Validates signature
    timestamp_message = "{}.{}".format(timestamp, message.embeds[0].description)
    digest = hmac.new(
        bot_config.ROBLOX_WEBHOOK_SECRET.encode(),
        msg=timestamp_message.encode(),
        digestmod=hashlib.sha256
    ).digest()
    validated_signature = base64.b64encode(digest).decode()
    if signature != validated_signature:
        return False

    # Valid signature
    return True

"""
Parses a received webhook messaged on Discord. Extracts user ID, prevents replay attack
based on timestamp received, and verifies Roblox signature with configured secret to check for
validity.
"""
def parse_message(message):
    # Parses received message for user ID and game ID
    if len(message.embeds) != 1 or \
        not message.embeds[0].description:
        return "", []
    description_match = re.match(
        r"You have received a new notification for Right to Erasure for the User Id: (.*) in " +
        r"the game\(s\) with Ids: (.*)",
        message.embeds[0].description
    )
    if not description_match:
        return "", []
    user_id = description_match.group(1)
    start_place_ids = set(int(item.strip()) for item in description_match.group(2).split(","))

    signature, timestamp = parse_footer(message)
    if validate_signature(message, signature, timestamp):
        return user_id, start_place_ids
    else:
        return "", []
``````python
import discord

import bot_config
import data_stores_api
import message_parser

def run():
    intents = discord.Intents.default()
    intents.message_content = True
    client = discord.Client(intents=intents)

    @client.event
    async def on_ready():
        print(f"{client.user} is listening to Right to Erasure messages")

    """
    Handler for webhook messages from Roblox
    """
    @client.event
    async def on_message(message):
        # Parses and validates message
        user_id, start_place_ids = message_parser.parse_message(message)
        if not user_id or not start_place_ids:
            return

        # Deletes standard data stores user data
        [successes, failures] = data_stores_api.delete_standard_data_stores(user_id, start_place_ids)
        if successes:
            await message.reply(f"Deleted standard data stores data for " +
                               f"user ID: {user_id}, data: {dict(successes)}")
        if failures:
            await message.reply(f"Failed to delete standard data stores data for " +
                               f"user ID: {user_id}, data: {dict(failures)}")

        # Deletes ordered data stores user data
        [successes, failures] = data_stores_api.delete_ordered_data_stores(user_id, start_place_ids)
        if successes:
            await message.reply(f"Deleted ordered data stores data for " +
                               f"user ID: {user_id}, data: {dict(successes)}")
        if failures:
            await message.reply(f"Failed to delete ordered data stores data for " +
                               f"user ID: {user_id}, data: {dict(failures)}")

    client.run(bot_config.BOT_TOKEN)

if __name__ == "__main__":
    run()
```
3. On the `bot_config.py` file for main configuration of the bot:
  1. Set `BOT_TOKEN` to the token generated by your bot.
  2. Set `OPEN_CLOUD_API_KEY` as the API key you created.
  3. Set `ROBLOX_WEBHOOK_SECRET` as the secret you set when configuring the webhook on Creator Dashboard.
  4. In `STANDARD_DATA_STORE_ENTRIES` and `ORDERED_DATA_STORE_ENTRIES` dictionaries for locating the data store of each record to delete:
    1. Add your copied Start Place IDs as keys.
    2. Add Universe IDs as the first element of the tuple value.
    3. Replace the second element of the tuple with the name, scope, entry key name, and associated User ID of your data stores. If you use a different data schema, modify to match your own data schema accordingly.
4. Execute the following command to run the bot:```bash
python3 discord_bot.py
```
5. The bot then starts to listen and verify Roblox webhooks for right to erasure Requests and calls the Open Cloud endpoint for deleting the corresponding data store.

> **Warning:** To ensure constant and secure execution of the scripts, save and run them locally only. Keep your local device or virtual machine running the scripts turned on at all times. In the event that your device goes offline, you need to manually manage any missed messages during the offline period and handle delivery failures according to the [retry policy](/docs/en-us/cloud/webhooks/webhook-notifications.md#delivery-failure-retry-policy).
## Test

You can create and run a test message to verify that your custom program can properly handle right to erasure requests and delete PII data:

1. Send an HTTP `POST` request to your Discord webhook server with the following request body:```bash
curl -X POST {serverUrl}
-H 'Content-Type: application/json'
-d '{
   "embeds":[{
      "title":"RightToErasureRequest",
      "description":"You have received a new notification for Right to Erasure for the User Id: {userIds} in the game(s) with Ids: {gameIds}",
      "footer":{
         "icon_url":"https://create.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png",
         "text":"Roblox-Signature: {robloxSignature}, Timestamp: {timestamp}"
      }
   }]
}'
```
2. If you have a webhook secret:
  1. Generate a `Roblox-Signature` by applying HMAC-SHA256 encoding to your webhook secret key.
  2. Set the current time using UTC timestamp in seconds as `Timestamp`.
3. Put together the `description` in the following format:```plain
{Timestamp}. You have received a new notification for Right to Erasure for the User Id: {userId} in the game(s) with Ids: {gameIds}\`.
``` For example:```plain
1683927229. You have received a new notification for Right to Erasure for the User Id: 2425654247 in the game(s) with Ids: 10539205763, 13260950955
```

Your program should be able to identify that your message is from the official Roblox source since you encoded the message with your secret. It should then delete the PII data associated with your request.

```json
{
  "embeds": [
    {
      "title": "RightToErasureRequest",
      "description": "You have received a new notification for Right to Erasure for the User Id: 2425654247 in the game(s) with Ids: 10539205763, 13260950955",
      "footer": {
        "icon_url": "https://create.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png",
        "text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
      }
    }
  ]
}
```