update rules

This commit is contained in:
Joe 2026-02-02 14:05:11 -06:00
parent 4c390fdd37
commit d5522b2fef
8549 changed files with 11783 additions and 1888 deletions

View file

@ -0,0 +1,241 @@
---
title: Manage API keys
description: Explains how to create API keys and add permissions to use Open Cloud web APIs for your experience.
---
Open Cloud authenticates and authorizes API access with the use of API keys,
which allow you to add granular permissions and security control for accessing
and utilizing certain resources in your experience, such as data stores and
places.
All Open Cloud APIs require you to create an API key with valid permissions and
include an `x-api-key` header in your request, which allows the application to
authenticate to Open Cloud on your behalf.
## Create API keys
<Alert severity="warning">
All references to API keys in the text below refer to user-owned keys.
</Alert>
You can create and configure API keys to access your resources. An API key's access is determined by the permissions of the user who owns it. This means it can generally access any resource the user has permissions for, including their individual experiences and any [group-owned](../../projects/groups.md) experiences where they have the appropriate role. Some scopes can be restricted to specific experiences, but not all.
For details on how to create API keys for managing group resources, see the [Create API keys for managing group-owned resources](#create-api-keys-for-managing-group-owned-resources) section below.
To create an API key:
1. In the [Creator Dashboard](https://create.roblox.com/dashboard/creations), go to the [API Keys](https://create.roblox.com/dashboard/credentials?activeTab=ApiKeysTab) page.
1. Click the **Create API Key** button.
1. Enter a unique name for your API key. Use a name that can help you recall the
purpose later, such as `PLACE_PUBLISHING_KEY` for publishing places to your
experience.
1. In the **Access Permissions** section, select an API from the **Select API
System** menu. Repeat this step if
you need to add multiple APIs to the key.
1. If applicable, select the experience that you want to access with the API key.
You can optionally disable **Restrict by Experience**. When disabled, your API key has access to all of your user-owned experiences and any group-owned experiences where you have the appropriate permissions, including any experiences you create in the future.
1. From the **Select Operations** dropdown, select the operations that you
want to enable for the API key.
Most operations in the [API reference](../index.md) include the required permission scopes. For example, the [flush memory store](/cloud/reference/MemoryStore#Cloud_FlushMemoryStore) operation requires the `universe.memory-store:flush` permission.
For a list of all scopes and the APIs they support, see [Scopes](../reference/scopes.md).
1. **(Optional)** In the **Security** section, explicitly restrict IP access to the key using [CIDR
notation](#cidr-format). You can find
the IP address of your local machine and add it to the **Accepted IP
Addresses** section along with additional IP addresses for those that need
access. If you don't have a fixed IP, or you are using the API key only in a
local environment, you can leave the `Restrict IP addresses` toggle unchecked to allow any IP to use your API key.
1. **(Optional)**: To add additional protection for your resources, set an expiration date for your key.
1. Click the **Save & Generate key** button.
1. Copy and save the API key string to a secure location, **not** a public repository for your code.
1. Verify the status of your API key on the
[API Extensions](https://create.roblox.com/dashboard/credentials) page of **Creator
Dashboard**.
<Alert severity="warning">
The API key string is equivalent to a password for your application. Never share it with untrusted parties, such as anyone outside of your development team.
</Alert>
## Create API keys for managing group-owned resources
An API key grants access to all resources the user account has permissions for, including personal experiences outside of the group. If you use your personal account's API key for group automation and that key is compromised, other resources you have access to are also at risk.
To prevent this, we **strongly recommend** creating a separate API key on a dedicated alternate account with access strictly limited to the target group. This new account dedicated for automation purposes should only be given access to the target group and granted the minimal permissions required for its task.
1. Create a new, dedicated Roblox account for your automation.
1. Invite the new account to your group.
1. Assign it a group role with the minimum permissions required for its task (e.g., only "Create and edit group experiences").
1. Log in to the new account and follow the steps in the section above to [create an API key](#create-api-keys).
1. Use the generated API key for group resource automation.
## Best Practices For Managing API Keys
API keys are sensitive credentials that should be kept secure to prevent unauthorized access to your data. Here are some best practices for managing API keys.
- **Create separate keys for each application**: Create separate API keys for each application or use case to isolate access and reduce the impact if a key is compromised.
- **Select the minimum permissions needed**: When configuring scopes, select the minimum permissions necessary for the key's intended use. For those scopes that allow you to restrict scope access by experience, limit access to only the specific experiences that are needed.
- **Use IP address restrictions**: Restrict API key access to specific IP addresses or CIDR ranges to prevent unauthorized usage from unknown locations. Do not use IP address restrictions when using your API key in Roblox places to ensure your key can be used with Roblox servers.
- **Set expiration dates**: For short-term use cases, configure expiration dates to automatically disable keys after a set period, reducing the risk if a key is compromised. Setting expiration dates is not recommended for longer-term use cases unless you have a key rotation process in place, as your automation can unexpectedly fail when the key expires.
- **Use dedicated alternate accounts for group resource management**: Use a dedicated account with minimal permissions for group resource management, as detailed in the [Create API keys for managing group-owned resources](#create-api-keys-for-managing-group-owned-resources) section.
- **Store API keys securely**: Never store API keys directly in your source code, version control systems, or scripts where they could be exposed. Use a secrets management system for storing and controlling access to your keys. In Roblox places, use a [Secrets Store](cloud-services/secrets).
- **Do not share API keys through public channels**: Never share API keys through public communication channels, forums, or social media. Only share keys through secure, private channels with trusted team members. Limit access to who you share your keys with to minimize the blast radius if a key is compromised.
## CIDR format
To further protect your resources, when [creating an API
key](#create-api-keys), specify IP addresses that can access the API key with
either normal IP addresses or using the [CIDR
notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation).
A CIDR IP address looks like a normal IP address except it ends with a slash and
a decimal that represents how many bits of the IP address are significant for
network routing:
- **Normal**: 192.168.0.0
- **CIDR**: 192.168.0.0/24
The former part is the IP address and the latter part is the **netmask**,
counting the bits of 1s in binary format. In the previous example, **24** means
**255.255.255.0** (24 1s) that allows all IPs between **192.168.0.0** and
**192.168.0.255**. Understanding CIDR format is particularly useful if you plan
to run your applications on a server.
## API key status
API keys initially have an active status, but they can become inactive over
their lifetime. To learn why an API key has changed status and how to return the
API key back to an active status, see the following table.
<Alert severity="warning"> If you or your group don't use or update an API key
for 60 days, it automatically expires, even if it doesn't have a set expiration
date. This reduces the risk of bad actors using old API keys. If you're no
longer using an API key, you should manually disable or delete it. </Alert>
<table>
<thead>
<tr>
<th>Status</th>
<th>Reason</th>
<th>Resolution</th>
</tr>
</thead>
<tbody>
<tr>
<td>Active</td>
<td>No issues. The user can use the key to authenticate API calls.</td>
<td>N/A</td>
</tr>
<tr>
<td>Disabled</td>
<td>The user disabled the key by disabling the <b>Enable Key</b> toggle.</td>
<td>Enable the <b>Enable Key</b> toggle.</td>
</tr>
<tr>
<td>Expired</td>
<td>The key's expiration date has passed.</td>
<td>Either remove or set a new expiration date.</td>
</tr>
<tr>
<td>Auto-Expired</td>
<td>The user hasn't used or updated the key in the past 60 days.</td>
<td>You can either disable then enable the <b>Enable Key</b> toggle, or you can update any of the key's properties, such as the name, description, or expiration date.</td>
</tr>
<tr>
<td>Revoked</td>
<td>For group keys only. The account that generated the key no longer has the sufficient access permission to manage the group's keys.</td>
<td>Click the <b>Regenerate Key</b> to get a new secret.</td>
</tr>
<tr>
<td>Moderated</td>
<td>A Roblox admin changed the key's secret for security reasons.</td>
<td>Click the <b>Regenerate Key</b> to get a new secret.</td>
</tr>
<tr>
<td>User Moderated</td>
<td>The account that generated the key is under moderation by Roblox.</td>
<td>Resolve the moderation issue on the account.</td>
</tr>
</tbody>
</table>
## Introspect API keys
### `POST` api-keys/v1/introspect
Retrieve information about an API key. Verifies whether the key can be used from the requester's IP address and whether the key or the last generated user is moderated.
<h4>Request</h4>
`(application/json)`
| Key | Value |
| -------------- | ---------------------- |
| apiKey | `<api_key>` |
```bash title="Example Introspect API Key Request"
curl --location --request POST 'https://apis.roblox.com/api-keys/v1/introspect' \
--header 'Content-Type: application/json' \
--data '{
"apiKey": "your-api-key"
}'
```
<h4>Response</h4>
There are four possible resource identifiers that can be present in each scope object:
- userId
- groupId
- universeId
- universeDatastore
The userId and groupId identifiers are only relevant to scopes with the creator target. The universeDatastore identifier is only relevant to scopes with the universe-datastore target. The resource identifier will be omitted for scopes that do not support resource selection.
An asterisk (`*`) in the resource identifier list indicates that the scope has permission on all resources of that type.
```json title="Example Introspect API Key Response"
{
"name": "test key",
"authorizedUserId": 234,
"scopes": [
{
"name": "universe-datastores.objects",
"operations": [
"create"
],
"universeDatastores": [
{
"universeId": "123",
"datastoreName": "playerData"
}
]
},
{
"name": "asset",
"operations": [
"write"
],
"groupIds": [
"*"
],
"userIds": [
"*"
]
}
],
"enabled": true,
"expired": false,
"expirationTimeUtc": "2026-01-01T12:00:00.000Z"
}
```

View file

@ -0,0 +1,206 @@
---
title: OAuth 2.0 app implementation
description: Introduces OAuth 2.0 authentication implementation.
---
Open Cloud supports the OAuth 2.0 authorization flow with and without
[PKCE](../../cloud/auth/oauth2-overview.md#authorization-code-flow-with-pkce),
depending on whether your clients are confidential or public.
- _Confidential clients_ are apps that are capable of keeping credentials
confidential, such as a website that can securely store and retrieve secrets on
its backend.
- _Public clients_ are apps that can't keep secrets, such as mobile
and web browser apps.
We recommend the OAuth 2.0 authorization code flow with PKCE for _all_ clients
and require it for public clients.
<Alert severity="warning">
Store the client secret in a secure place and never expose it to the public. Bad
actors can use it to impersonate your application.
</Alert>
To implement the authorization code flow, your app performs the following steps:
1. Generate a code verifier and code challenge (PKCE only). This lets you
include a challenge in your requests rather than the client secret, which
improves security.
1. Send a `GET` request to the authorization endpoint with the appropriate parameters.
1. Handle the authorization callback. After obtaining authorization, use the
authorization code from Roblox in a redirect request to the URL you specified
during app creation. Parse the authorization code for later use.
1. Send a `POST` request to the token endpoint with the authorization code. If
successful, you receive access and refresh tokens with which to make API
calls.
1. Call any Open Cloud API that supports OAuth 2.0 authentication based on the
reference documentation for the resources you want to access.
The following sections describe each step in greater depth.
## Generate a code challenge (for PKCE only)
Before initiating the authorization process, you need to generate a code
challenge from a code verifier. You can use libraries or built-in functions in
the programming language of your choice to create the code verifier, calculate
the hash, and perform Base64 encoding to generate the code challenge.
When creating the code verifier, only use unreserved characters, including
uppercase and lowercase letters (A-Z, a-z), decimal digits (0-9), hyphen (-),
period (.), underscore (\_), and tilde (~), with a minimum length of 43
characters and a maximum length of 128 characters.
The following example shows how to use Javascript to create a code verifier and
generate the code challenge using the SHA-256 hashing algorithm:
```javascript title="Generate Code Challenge"
const crypto = require('crypto');
// base64URL encode the verifier and challenge
function base64URLEncode(str) {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// create sha256 hash from code verifier
function sha256(buffer) {
return crypto.createHash('sha256').update(buffer).digest(`base64`);
}
// create a random code verifier
var code_verifier = base64URLEncode(crypto.randomBytes(32));
// generate a challenge from the code verifier
var code_challenge = base64URLEncode(sha256(code_verifier));
```
For PKCE, you need both the code verifier and challenge values in later steps.
## Construct the authorization URL
<Alert severity="info">
See the [authorization endpoint reference documentation](oauth2-reference.md#get-v1authorize)
for complete information on how to construct the authorization URL.
</Alert>
To initiate the user authorization process, construct an
authorization URL with the required parameters:
- `client_id`: Your app's client ID obtained after [registering your app](oauth2-registration.md).
- `redirect_uri`: Your app's redirect URI, the re-entry point of your app.
- `scope`: The requested scopes that define the access permissions your app
needs in a space-separated list.
- `response_type`: Set it to `code` to indicate the authorization code flow.
**Required for PKCE only**
- `code_challenge`: The code challenge generated from a code verifier.
- `code_challenge_method`: Set this parameter value to `S256` to indicate that
the code challenge was transformed using the SHA-256 algorithm, the most
widely supported and secure code challenge method.
- `state`: An opaque, secure random value to maintain state
between the request and callback.
**Required for non-PKCE only**
- `client_secret`: Your app's client secret obtained after [registering your app](oauth2-registration.md). If you are using authentication with
PKCE, omit this parameter.
Your authorization request URL must be well formatted, like in the following
example:
```bash title="Example PKCE Authorization URL"
https://apis.roblox.com/oauth/v1/authorize?client_id=7290610391231237934964
&code_challenge=PLEKKVCjdD1V_07wOKlAm7P02NC-LZ_1hQfdu5XSXEI
&code_challenge_method=S256
&redirect_uri=https://example.com/redirect
&scope=openid%20profile
&response_type=code
&state=abc123
```
When users visit the URL, they are taken through the authorization flow. If
successful, Roblox redirects the user to the specified `redirect_uri`.
## Handle authorization callbacks
When an authorization flow is successful, your app receives a `GET` request from
the Roblox authorization server at the `redirect_uri` that you specified. In the
request, you receive `code` (authorization code) and `state`(if you provided a
value previously) parameters.
- The `code` parameter contains an authorization code that the app can
exchange for an access token from the authorization server. Most backend
server languages have standard ways to access query parameters as decoded
objects. You'll need to obtain the `code` parameter and use it to exchange
for access tokens.
- The `state` parameter is an opaque value that you initially provided in the
authorization request. You can use this parameter to maintain the state or
context of the authorization process.
For example, you might receive a `GET` request with the following URL if you
specified your redirect URI to be `https://example.com/redirect`.
```bash title="Example Redirect URL"
https://example.com/redirect?code=10c45PNuquKnFJ6pUcy5-fHkghEM6lSegm-7hj9mVEprub1dSDuStuKK_EAUXY7AHTD63xcnmvxSLthp-C8g3jzIGZVzuXSd20Y2dEYI9hx0LZmPg95ME4z2K03UheiZbroyXUjYyB3ReoMqobzDVPzyx6IS8kj2Uu-11Xq_0JiTYxtDatuqXRNIAmJT8gMJmbSyOLOP_vnDvbeMUiBsqCRrkTGVbWSwYSc8sTVVE-535kYdqQOgNjH1ffCoZLGl8YYxLnpb2CXJlRQPrcjkA&state=6789
```
If authorization fails, your app receives a `GET` request to the specified
redirect URL with the `error`, `error_description`, and `state` (if applicable)
parameters.
- The `error` parameter indicates the specific OAuth 2.0 error that occurred
during the authorization process.
- The `error_description` parameter provides additional details of the error.
- The `state` parameter helps your app maintain state in the case of a
failure.
## Exchange an authorization code for access tokens
When you have parsed the authorization `code`, exchange it for
tokens to access desired Roblox resources:
1. Request an access token and refresh token by sending a `POST` request to the
[token exchange endpoint](oauth2-reference.md#post-v1token). The
request must include the authorization code, client ID, and the code
verifier value (PKCE) or client secret (non-PKCE) in `x-www-form-urlencoded` format.
1. Parse the applicable tokens from the response received. The access token is
valid for 15 minutes. Store the refresh token securely, typically on the server side, so you can use it to obtain new tokens in the future.
```json title="Example Token Endpoint Response"
{
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IlBOeHhpb2JFNE8zbGhQUUlUZG9QQ3FCTE81amh3aXZFS1pHOWhfTGJNOWMiLCJ0eXAiOiJKV11234.eyJzdWIiOiIyMDY3MjQzOTU5IiwiYWlkIjoiM2Q2MWU3NDctM2ExNS00NTE4LWJiNDEtMWU3M2VhNDUyZWIwIiwic2NvcGUiOiJvcGVuaWQ6cmVhZCBwcm9maWxlOnJlYWQiLCJqdGkiOiJBVC5QbmFWVHpJU3k2YkI5TG5QYnZpTCIsIm5iZiI6MTY5MTYzOTY5OCwiZXhwIjoxNjkxNjQwNTk4LCJpYXQiOjE2OTE2Mzk2OTgsImlzcyI6Imh0dHBzOi8vYXBpcy5yb2Jsb3guY29tL29hdXRoLyIsImF1ZCI6IjcyOTA2MTAzOTc5ODc5MzQ5Nj1234.BjwMkC8Q5a_iP1Q5Th8FrS7ntioAollv_zW9mprF1ats9CD2axCvupZydVzYphzQ8TawunnYXp0Xe8k0t8ithg",
"refresh_token": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoidGpHd1BHaURDWkprZEZkREg1dFZ5emVzRWQyQ0o1NDgtUi1Ya1J1TTBBRSIsInR5cCI6IkpXVCJ9..nKYZvjvXH6msDG8Udluuuw.PwP-_HJIjrgYdY-gMR0Q3cabNwIbmItcMEQHx5r7qStVVa5l4CbrKwJvjY-w9xZ9VFb6P70WmXndNifnio5BPZmivW5QkJgv5_sxLoCwsqB1bmEkz2nFF4ANLzQLCQMvQwgXHPMfCK-lclpVEwnHk4kemrCFOvfuH4qJ1V0Q0j0WjsSU026M67zMaFrrhSKwQh-SzhmXejhKJOjhNfY9hAmeS-LsLLdszAq_JyN7fIvZl1fWDnER_CeDAbQDj5K5ECNOHAQ3RemQ2dADVlc07VEt2KpSqUlHlq3rcaIcNRHCue4GfbCc1lZwQsALbM1aSIzF68klXs1Cj_ZmXxOSOyHxwmbQCHwY7aa16f3VEJzCYa6m0m5U_oHy84iQzsC-_JvBaeFCachrLWmFY818S-nH5fCIORdYgc4s7Fj5HdULnnVwiKeQLKSaYsfneHtqwOc_ux2QYv6Cv6Xn04tkB2TEsuZ7dFwPI-Hw2O30vCzLTcZ-Fl08ER0J0hhq4ep7B641IOnPpMZ1m0gpJJRPbHX_ooqHol9zHZ0gcLKMdYy1wUgsmn_nK_THK3m0RmENXNtepyLw_tSd5vqqIWZ5NFglKSqVnbomEkxneEJRgoFhBGMZiR-3FXMaVryUjq-N.Q_t4NGxTUSMsLVEppkTu0Q6rwt2rKJfFGuvy3s12345",
"token_type": "Bearer",
"expires_in": 899,
"id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IkNWWDU1Mi1zeWh4Y1VGdW5vNktScmtReFB1eW15YTRQVllodWdsd3hnNzgiLCJ0eXAiOiJKV11234.eyJzdWIiOiIyMDY3MjQzOTU5IiwibmFtZSI6ImxpbmtzZ29hdCIsIm5pY2tuYW1lIjoibGlua3Nnb2F0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoibGlua3Nnb2F0IiwiY3JlYXRlZF9hdCI6MTYwNzM1NDIzMiwicHJvZmlsZSI6Imh0dHBzOi8vd3d3LnJvYmxveC5jb20vdXNlcnMvMjA2NzI0Mzk1OS9wcm9maWxlIiwibm9uY2UiOiIxMjM0NSIsImp0aSI6IklELnltd3ZjTUdpOVg4azkyNm9qd1I5IiwibmJmIjoxNjkxNjM5Njk4LCJleHAiOjE2OTE2NzU2OTgsImlhdCI6MTY5MTYzOTY5OCwiaXNzIjoiaHR0cHM6Ly9hcGlzLnJvYmxveC5jb20vb2F1dGgvIiwiYXVkIjoiNzI5MDYxMDM5Nzk4NzkzNDk2NCJ9.kZgCMJQGsariwCi8HqsUadUBMM8ZOmf_IPDoWyQY9gVX4Kx3PubDz-Q6MvZ9eU5spNFz0-PEH-G2WSvq2ljDyg",
"scope": "openid profile"
}
```
## Make a call to a resource method
Now that you have the required access token, you can use it to make
authenticated calls to resource methods. Include the access token in the header of all API requests to
authorize them.
For example, you can test if your app functions correctly by
going through the entire authorization flow and then making a `GET` request
to the
[user information endpoint](oauth2-reference.md#get-v1userinfo) with an access
token. Ensure you have the `openid` or both the `openid` and `profile`
scopes before calling this endpoint. If successful, the response from that
endpoint looks like this:
```json title="Example User Information Response"
{
"sub": "12345678",
"name": "Jane Doe",
"nickname": "robloxjanedoe",
"preferred_username": "robloxjanedoe",
"created_at": 1607354232,
"profile": "https://www.roblox.com/users/12345678/profile"
}
```

View file

@ -0,0 +1,167 @@
---
title: OAuth 2.0 overview
description: Introduces the 3rd-party app creation support with OAuth 2.0 authorization framework.
---
You can build or authorize apps that use Open Cloud APIs to access Roblox
resources. Open Cloud provides authentication for these apps using OAuth 2.0.
<dl>
<dt>**As an experience creator or group owner**</dt>
<dd>
You can securely use tools created by others to improve your creation
productivity. The OAuth 2.0 authorization layer allows you to grant permissions
to third-party apps to access your or your group's experiences without giving
them your credentials and personal information. You select the access
permissions of your specific Roblox resources, and Roblox handles the
authorization process for you with the OAuth 2.0 framework.
<Alert severity="info">
You must have a 13+ account to authorize OAuth 2.0 apps.
</Alert>
</dd>
<dt>**As an app developer**</dt>
<dd>
You can create apps for yourself and others in the Roblox community. OAuth 2.0
defines the roles involved in the authorization process, the protocol of how
roles interact with each other, and the authorization flows that you need to
follow to develop secure and compatible apps.
<Alert severity="info">
You must be [ID verified](../../production/publishing/account-verification.md#verify-through-government-id)
to register and publish OAuth 2.0 apps.
</Alert>
</dd>
</dl>
## Roles
The Open Cloud OAuth 2.0 protocol has the following roles. It's useful to
understand the specific roles before learning about how they interact with one
another in authorization flows.
- **Resource owner**: An entity capable of granting access to a protected
resource. For example, a creator who allows a third-party app to access
their Roblox resources through Open Cloud Web APIs.
- **Resource server**: A Roblox service that hosts protected resources and
responds to requests from a resource owner.
- **Client**: An app that accesses protected resources on behalf of the resource
owner (with the owner's authorization).
- **Authorization server**: The Roblox server that authenticates the identity of
the resource owner and issues access tokens to the client.
## Grant types
Authorization flows, or grant types, are the steps of actions that roles perform
during the authorization process. Roblox supports the OAuth 2.0 authorization
code flow and its Proof Key for Code Exchange (PKCE) extension, with different
implementation requirements for apps that are capable or incapable of storing
client secrets.
### Authorization code flow
Through the authorization code flow, a client exchanges an authorization code
for an access token and a refresh token to complete the authorization process in
the following steps:
1. The client sends an authorization request to the Roblox authorization server.
1. The authorization server verifies the identity of the resource owner.
1. The authorization server receives permissions for accessing specific Roblox
resources from the resource owner.
1. The authorization server redirects the resource owner back to the client with
an authorization code.
1. The client requests an access token using the authorization code at the token
endpoint.
1. The client receives a response from the token endpoint containing an access
token, an ID token, and a refresh token.
1. The client retrieves the permitted resources after getting the access token.
The following figure describes the interactions between roles in the
authorization code flow that you'll read in the following sections:
<img src="../../assets/open-cloud/protocol-flow.png" width="80%" />
### Authorization code flow with PKCE
The [PKCE extension](https://www.rfc-editor.org/rfc/rfc7636) of the
authorization code flow helps reduce risk of leaking the authorization code and
prevent cross-site request forgery (CSRF), an attack that tricks users into
submitting unintended web requests. This flow completes the authorization
process with the following steps:
1. The client generates a unique and cryptographically random key called a
_code verifier_ for every authorization request.
1. The client runs a SHA-256 hash algorithm on the code verifier to generate a
_code challenge_.
1. If the client:
- Is a [public client](oauth2-develop.md#public-clients),
instead of using the client secret, it passes the client ID and the code
challenge in the authorization request.
- Is a [confidential client](oauth2-develop.md#confidential-clients),
it adds the code challenge along with the client ID and secret in the
request.
1. The client sends an authorization request to the Roblox authorization server.
1. The authorization server verifies the identity of the resource owner.
1. The authorization server receives permissions for accessing specific Roblox
resources from the resource owner.
1. The authorization server redirects the resource owner back to the client with
an authorization code.
1. The client includes the authorization code and the original code verifier
in the token request to the [token endpoint](oauth2-reference.md#token-exchange).
1. The authorization server verifies the authorization code and the associated
code verifier.
1. The client receives a response from the token endpoint containing an access
token, an ID token, and a refresh token.
1. The client retrieves the permitted resources after getting the access token.
## OpenID Connect support
Roblox uses [OpenID Connect (OIDC)](https://openid.net/connect/) as an
identity layer on top of the OAuth 2.0 protocol for authentication to protect
sensitive account information. OIDC allows applications to verify the identity
of users and obtain their basic public profile information, such as user ID,
usernames, display names, and profile links.
<Alert severity="info">
When [adding permission scopes](oauth2-registration.md#register-an-app) to your app on
**Creator Dashboard**, make sure to select the `openid` identity scope for
receiving an ID token in the token response as part of the authentication
process.
</Alert>
## Registration and implementation
To implement a web or mobile app that uses authorization code flow, you need to:
1. [Register](oauth2-registration.md) your app with Roblox.
This lets you obtain a client ID and secret to register your app with Roblox
and make calls to your endpoints.
1. [Implement](oauth2-develop.md) the authorization code
flow. For a complete reference of the OAuth 2.0 endpoints that you need to
call, see the [Authentication](oauth2-reference.md) reference.
1. Go through the [review process](oauth2-registration.md#submit-for-review)
to get more user quota.

View file

@ -0,0 +1,427 @@
---
title: OAuth 2.0 authentication
description: Describes how to make authenticated calls to Open Cloud with API keys and OAuth 2.0
---
<Alert severity="warning">
OAuth 2.0 authentication is a beta feature that might be subject to change for
future releases. All Open Cloud endpoints also support
[API key authentication](../auth/api-keys.md).
</Alert>
<Alert severity="info">
For complete implementation guides and information on OAuth 2.0, including a
sample app, see the [OAuth 2.0 overview](../auth/oauth2-overview.md).
</Alert>
This document describes endpoints that you call to implement the OAuth 2.0
authorization code flow, as well as other endpoints useful for implementing
authentication in your apps.
```http title="Base URL"
https://apis.roblox.com/oauth
```
```http title="Endpoints"
GET v1/authorize
POST v1/token
POST v1/token/introspect
POST v1/token/resources
POST v1/token/revoke
GET v1/userinfo
GET .well-known/openid-configuration
```
## Authorization
### `GET` v1/authorize
Obtains authorization from the user to authenticate with their Roblox account.
Expects a valid authorization URL constructed with
the specified parameters. This endpoint supports PKCE authorization.
<h4>Query parameters</h4>
| Name | Description | Required | Example |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------- |
| client_id | The app's client ID. | yes | `816547628409595165403873012` |
| redirect_uri | The URL that users are redirected back to after the completing authorization flow. | yes | `https://www.roblox.com/example-redirect` |
| scope | The requested scopes, space delimited. Use the `openid` scope to receive an ID token. Use the `openid` and `profile` scope to obtain more user information. | yes | `openid profile` |
| response_type | The credentials the app wants returned. The default is the authorization code grant type. | yes | `none`, `code` |
| prompt | Specifies what authentication and consent pages are shown to users. Certain screens are required for third-party apps and can't be skipped. | no | `none`, `login`, `consent`, `select_account` |
| nonce | The cryptographic number that binds the token with the client. | no | `<random_value_1>` |
| state | An opaque value that prevents cross-site request forgery, a type of malicious attack. Passed back to the app after authorization. | no | `<app-provided-opaque-value>` |
| code_challenge | The resulting string of applying the `code_challenge_method` to the `code_verifier`. | no | Base64-URL-encoded string |
| code_challenge_method | The function that is applied to the `code_verifier`. | no | S256 |
<h4>Request</h4>
```plain title="Example Request of Directing to Authorization Flow"
https://apis.roblox.com/oauth/v1/authorize?client_id=816547628409595165403873012&redirect_uri=https://my-app.com/redirect&scope=openid&response_type=code&nonce=12345&state=6789
```
<h4>Response</h4>
After calling this endpoint, the user is redirected to the specified redirect
URL with the authorization code in a `code` query parameter.
The authorization code:
- Has a lifetime of one minute.
- Can only be redeemed once.
- Is invalid after one use.
## Token exchange
To obtain tokens to access APIs, exchange an **authorization code** for a set of
confidential tokens. All token endpoints support two types of client
authentication:
1. HTTP Basic Authentication Scheme with an authorization header:
`Authorization: Basic Base64 encoded(<client_id>:<client_secret>)`.
1. Client ID and secret in the request body as parameters.
The following list describes the various tokens you receive from this
endpoint.
- **Access token** - Represents the authorization from a creator or user for a
third-party app to access their protected Roblox resources. It's a string
denoting a specific scope, lifetime, and other access attributes. Once the
Roblox authorization server issues an access token to an app, the token:
- Is valid for 15 minutes.
- Can be used multiple times before it expires.
- Can be invalidated before it expires if an app user revokes the authorization.
- **Refresh token** - Refreshes an authorization session. An app can use
the refresh token to obtain a new set of tokens, which includes an access token,
a refresh token, and an ID token. A refresh token:
- Is valid for 90 days.
- Can only be used once before it expires to refresh tokens.
- Can be invalidated before it expires if an app user revokes the authorization.
- **ID token** - Provides proof that a user's identity has been authenticated. Its
content depends on the scopes requested and can contain basic user information,
including a user's Roblox display name and username. The ID token is only for
identity authentication purposes and doesn't provide access to any Roblox
resources.
### `POST` v1/token
Obtain a set of tokens with an authorization code.
<h4>Request</h4>
`(x-www-form-urlencoded)`
| Key | Value |
| ------------- | ---------------------------- |
| code | `<authorization code>` |
| code_verifier | `<pkce code verifier value>` |
| grant_type | authorization_code |
| client_id | `<client_id>` |
| client_secret | `<client_secret>` |
```bash title="Example Obtain Token Request"
curl --location --request POST 'https://apis.roblox.com/oauth/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=840974200211308101' \
--data-urlencode 'client_secret=RBX-CR9...St12L' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=yCnq4ofX1...XmGpdx'
```
<h4>Response</h4>
```json title="Example Obtain Token Response"
{
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 899,
"scope": "universe-messaging-service:publish"
}
```
### `POST` v1/token
Obtain a set of tokens with a refresh token.
<h4>Request</h4>
`(x-www-form-urlencoded)`
| Key | Value |
| ------------- | ----------------- |
| grant_type | refresh_token |
| refresh_token | `<refresh_token>` |
| client_id | `<client_id>` |
| client_secret | `<client_secret>` |
```bash title="Example Refresh Token Request"
curl --location --request POST 'https://apis.roblox.com/oauth/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=Ujfstayclfdlbm...BGydlsnU' \
--data-urlencode 'client_id=840974200211308101' \
--data-urlencode 'client_secret=RBX-CR9...St12L'
```
<h4>Response</h4>
```json title="Example Refresh Token Request"
{
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 899,
"scope": "universe-messaging-service:publish"
}
```
### `POST` v1/token/introspect
Receive information about a token. Verifies whether the token is presently valid
and hasn't expired yet. Useful for stateless validation. Use only if the API
you're accessing doesn't require a resource, such as Assets API, or if you just
want to see specific claims of the token.
<Alert severity="info"> Even if the user has revoked the authorization linked to
the access token, the endpoint still shows the access token as `valid: true`
because it checks whether the token is active based on its lifetime (typically
15 minutes for access tokens). </Alert>
<h4>Request</h4>
`(x-www-form-urlencoded)`
| Key | Value |
| ------------- | --------------------------------------------------- |
| token | `<access_token>`, `<refresh_token>` or `<id_token>` |
| client_id | `<client_id>` |
| client_secret | `<client_secret>` |
```bash title="Example Introspect Token Request"
curl --location --request POST 'https://apis.roblox.com/oauth/v1/token/introspect' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'token=eyjlflabtfl...4gxqYBG' \
--data-urlencode 'client_id=840974200211308101' \
--data-urlencode 'client_secret=RBX-CR9...St12L'
```
<h4>Response</h4>
```json title="Example Introspect Token Response"
{
"active": true,
"jti": "RT.2GcjvTduKzk6QY9tjTfm",
"iss": "https://apis.roblox.com/oauth/",
"token_type": "Bearer",
"client_id": "840974200211308101",
"aud": "4239311013248676173",
"sub": "1516563360",
"scope": "universe-messaging-service:publish",
"exp": 1676394509,
"iat": 1660842510
}
```
### `POST` v1/token/resources
Check whether a token can access a specific resource by obtaining the list of
user resources that the user gave permission for. This is useful for stateful
validation.
<h4>Request</h4>
`(x-www-form-urlencoded)`
| Key | Value |
| ------------- | ----------------- |
| token | `<access_token>` |
| client_id | `<client_id>` |
| client_secret | `<client_secret>` |
```bash title="Example Obtain Token Resources Request"
curl --location --request POST https://apis.roblox.com/oauth/v1/token/resources' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'token=eyjlflabtfl...4gxqYBG' \
--data-urlencode 'client_id=840974200211308101' \
--data-urlencode 'client_secret=RBX-CR9...St12L'
```
<h4>Response</h4>
The value `U` in `ids` indicates that a scope has granted access to a resource owned by the authorizing `owner`.
```json title="Example Obtain Token Resources Response"
{
"resource_infos": [
{
"owner": {
"id": "1516563360",
"type": "User"
},
"resources": {
"universe": {
"ids": ["3828411582"]
},
"creator": {
"ids": ["U"]
}
}
}
]
}
```
### `POST` v1/token/revoke
Revoke an authorization session using the provided refresh token.
<h4>Request</h4>
`(x-www-form-urlencoded)`
| Key | Value |
| ------------- | ----------------- |
| token | `<refresh_token>` |
| client_id | `<client_id>` |
| client_secret | `<client_secret>` |
```bash title="Example Revoke Token Request"
curl --location --request POST https://apis.roblox.com/oauth/v1/token/revoke' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'token=Ujfstayclfdlbm...BGydlsnU' \
--data-urlencode 'client_id=840974200211308101' \
--data-urlencode 'client_secret=RBX-CR9...St12L'
```
<h4>Response</h4>
`200 OK` with an empty response
## User Information
### `GET` /v1/userinfo
Gets the Roblox user ID and other user metadata.
<h4>Request</h4>
**Authorization header**: `Authorization: Bearer <access_token>`
```bash title="Example Get User Info Request"
curl --location --request GET 'https://apis.roblox.com/oauth/v1/userinfo' \
--header 'Authorization: Bearer eyjlflabtfl...4gxqYBG'
```
<h4>Response</h4>
You can use the `sub` value to uniquely identify the user. Users can change
their Roblox username and display name, so don't use them as unique identifiers to refer to users on your app.
| Claim | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| sub | Roblox user ID. |
| name | Roblox display name. |
| nickname | Roblox display name. |
| preferred_username | Roblox username. |
| created_at | Creation time of the Roblox account as a Unix timestamp. |
| profile | Roblox account profile URL. |
| picture | Roblox avatar headshot image. Can be null if the avatar headshot image hasn't yet been generated or has been moderated. |
```json title="Example User with Profile Scope"
{
"sub": "1516563360",
"name": "exampleuser",
"nickname": "exampleuser",
"preferred_username": "exampleuser",
"created_at": 1584682495,
"profile": "https://www.roblox.com/users/1516563360/profile",
"picture": "https://tr.rbxcdn.com/03dc2a9abe7b1aacaaf93ea46d5c0646/150/150/AvatarHeadshot/Png"
}
```
```json title="Example User without Profile Scope"
{
"sub": "1516563360"
}
```
## Discovery
The OpenID Connect (OIDC) Discovery Document is a JSON document that
contains metadata about the Open Cloud configuration details, including a list
of identity-related scopes and claims that are supported. You can use it to
dynamically discover information about Open Cloud OAuth 2.0 endpoints and
configuration, such as the authorization endpoint, token endpoint, and public
key set.
After retrieving and fetching the Discovery Document from the Discovery document
URI, you can either manually inspect fields in the response to verify the
information, or you can create your own custom library mapping to fields in the
response schema to automate your workflow.
### `GET` .well-known/openid-configuration
<h4>Response</h4>
All Discovery Document responses follow the same schema as the following example response.
<Alert severity="warning">
Not all of the following scopes and claims are available to third-party app creators, as some can only be used by official Roblox apps.
</Alert>
```json title="Example Discovery Document Response"
{
"issuer": "https://apis.roblox.com/oauth/",
"authorization_endpoint": "https://apis.roblox.com/oauth/v1/authorize",
"token_endpoint": "https://apis.roblox.com/oauth/v1/token",
"introspection_endpoint": "https://apis.roblox.com/oauth/v1/token/introspect",
"revocation_endpoint": "https://apis.roblox.com/oauth/v1/token/revoke",
"resources_endpoint": "https://apis.roblox.com/oauth/v1/token/resources",
"userinfo_endpoint": "https://apis.roblox.com/oauth/v1/userinfo",
"jwks_uri": "https://apis.roblox.com/oauth/v1/certs",
"registration_endpoint": "https://create.roblox.com/dashboard/credentials",
"service_documentation": "https://create.roblox.com/docs/reference/cloud",
"scopes_supported": [
"openid",
"profile",
"email",
"verification",
"credentials",
"age",
"premium",
"roles"
],
"response_types_supported": ["none", "code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["ES256"],
"claims_supported": [
"sub",
"type",
"iss",
"aud",
"exp",
"iat",
"nonce",
"name",
"nickname",
"preferred_username",
"created_at",
"profile",
"email",
"email_verified",
"verified",
"age_bracket",
"premium",
"roles",
"internal_user"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
]
}
```

View file

@ -0,0 +1,144 @@
---
title: OAuth 2.0 app registration
description: Explains how to register an app and how the review workflow works.
---
Registering your OAuth 2.0 app allows it to access Roblox resources. After
registration, Roblox assigns the app a unique client ID and secret that you can
use to obtain authorization from users to access their Roblox information and
resources.
<Alert severity="info">
You can only register apps for individual accounts or groups that you own.
</Alert>
## Register your app
To register an app:
1. In the [Creator Dashboard](https://create.roblox.com/dashboard/creations), go to the [OAuth 2.0 Apps](https://create.roblox.com/dashboard/credentials?activeTab=OAuthTab) page.
1. Click the **Create App** button.
1. Enter a valid and globally unique **Application Name**.
1. Read and agree to the **Roblox Terms of Service** and click **Create**.
1. Copy and store the **Client ID** and **Secret** in a secure place. After
closing the page, you no longer have access to the secret and can only
generate a new one. After you finish registering an app, you can still access
and edit the app's other properties.
1. Click **Continue to Edit** to configure your app, which is described in the
following sections.
## Configure general information
Once you've [registered](#register-your-app) your app, you should configure its general information, including:
- A description, which is visible to everyone in the Roblox community.
- A thumbnail image for your app. The recommended file size is at least 150x150
pixels.
- An **Entry Link** as the entry point of your app, such as the app's website
homepage.
- A **Privacy Terms URL** and a **Terms of Service URL** for end-users to read
before authorizing your app. Each URL must be HTTPS and no more than 256
characters.
<Alert severity="info">
You can skip this section during app development and testing, but you must
complete it during the review process to publish your app to users.
</Alert>
## Add permissions
To ensure proper access control and security within your app, you must add
**Permission Scopes**. There are two types of permission scopes available:
- **Identity scopes** for authentication, such as `openid` for user IDs and
`profile` for the full profiles. If you select the `profile` scope, you must
also select `openid`.
- **API-specific permission scopes**, such as `asset:read` for the read
permission of assets. Select only the minimum number of scopes that you need
for your app.
<Alert severity="info">
Each app is expected to request the minimal number of scopes needed for its function. The set of scopes for an app must fall under a single app category listed in the [Creator Third Party App Policy](https://en.help.roblox.com/hc/en-us/articles/37924211313044-Creator-Third-Party-App-Policy).
</Alert>
## Add redirect URLs
**Redirect URLs** are the reentry points of your app that users are redirected
to when they finish authorizing your app. Ensure your
redirect URLs meet the following requirements:
- All redirect URLs must be in one of the following forms:
- Plain HTTPS, for example: `https://www.example.com`.
- Paths on localhost with HTTP or HTTPS (self-signed certificate) for local
debugging purposes, for example:
- `http://localhost:80`
- `https://localhost:80`
- Custom schemes for specific applications or purposes, for example:
- `my-app-scheme:/`
- `my-app-scheme://foo/bar/...`
- `my-app://action?param1=value1&param2=value2`
- The maximum length of a redirect URL is 256 characters.
- You can add up to 10 redirect URLs. With multiple redirect URLs, you can have
test environments and allow users to migrate between two sites without
downtime.
<Alert severity="info">
You can skip this step during app registration and add the redirect URLs later
when you have a testable version of your app.
</Alert>
## Submit for review
To minimize the chances of malicious apps harming the community, registering an
app doesn't make it broadly accessible. Instead, it remains in **private mode**
with a limit of 10 unique users. This mode is helpful for testing and
debugging within your team.
If you want to extend the user base for your app, you need to publish it in
public mode by submitting it for review:
1. Go to the [OAuth 2.0 apps](https://create.roblox.com/dashboard/credentials?activeTab=OAuthTab) page.
1. Click the **Edit and Publish** button for the app that you want to publish.
1. Fill in all the required information and click the **Review and Publish**
button. A message with limits on your app permissions during the pending
review period appears.
1. Click **Submit for Review**.
While your app review is pending, you can't edit or submit another request until
the current review request is approved or rejected. Your app remains in
private mode during the review. When your app is approved, it transitions to
public mode, and you can't revert it back to private mode.
Regardless of whether your app is approved or rejected, you receive an email
notification of the result when the review is completed.
## Edit and delete your app
You can edit or delete your app with the following steps if it's not under
review:
1. Go to the [OAuth 2.0 apps](https://create.roblox.com/dashboard/credentials?activeTab=OAuthTab) page.
1. Click **Edit** or **Delete** for the desired app.
- If you chose to edit an app, follow the same steps and requirements of
[registration](#register-your-app).
- If you chose to delete an app, click **Yes, Delete** to confirm deletion.
After you delete an app, you can't recover it.
Modifying your app's general information or redirect URLs does not require users
to reauthorize the app. However, if you add or modify permission scopes, you
must obtain authorization for the new permissions from your users and acquire a
new set of tokens. If you don't obtain authorization for the new scopes, your
app can continue using the existing tokens, but is limited to the previous
scopes.
For apps in private mode, the system automatically updates your edits with your
users. For apps in public mode, you must
[submit the app for review](#submit-for-review) again to reflect these changes.
Otherwise, the system saves the unpublished changes for you without releasing it
to the community.
<Alert severity ='info'> You can clone a copy of your public app and keep it in
private mode for iterative testing and debugging. This allows for a controlled
environment where you can safely make changes and resolve any issues before
submitting the app for review.</Alert>

View file

@ -0,0 +1,79 @@
---
title: OAuth 2.0 sample app
description: Provides a working OAuth 2.0 sample application.
---
Roblox provides a Node.js sample app that shows how to use OAuth 2.0 to let users log in to their Roblox accounts and broadcast messages across their experiences. This app uses the [authorization code flow _without_ PKCE](oauth2-overview.md#grant-types) and is thus only suitable for confidential clients, such as private servers.
<a href="../../assets/open-cloud/open-cloud-nodejs-sample-oauth2-main.zip">
<Button variant="contained">Get the Code</Button>
</a>
<br />
After downloading the `.zip` file, extract it to its own folder.
## Register the app
The first step to setting up the app is to [register it on the Roblox website](oauth2-registration.md) and copy the client ID and secret somewhere safe. Then follow the standard registration steps with these settings:
1. Under **Permissions**, add the `openid`, `profile`, and `universe-messaging-service:publish` scopes.
1. Under **Redirect URLs**, add the `http://localhost:3000/oauth/callback` redirect. If you want to use a non-default port for your app, specify it here.
## Set environment variables
Rather than storing them in code (not recommended), the app uses environment variables for your client ID and secret. The process for adding environment variables differs by operating system.
On Windows, run the following PowerShell commands:
```powershell
$env:ROBLOX_CLIENT_ID='your_client_id_here'
$env:ROBLOX_CLIENT_SECRET='your_client_secret_here'
$env:ROBLOX_PORT=3000 # Optional. Default is 3000.
```
On Mac and most Linux distributions, run these commands at the terminal:
```bash
export ROBLOX_CLIENT_ID=your_client_id_here
export ROBLOX_CLIENT_SECRET=your_client_secret_here
export ROBLOX_PORT=3000 # Optional. Default is 3000.
```
If you specified a non-default port in the redirect URL when registering your app, be sure to add the `ROBLOX_PORT` variable.
<Alert severity="info">
These commands only set environment variables for the duration of your terminal session. If you want to load these variables every time you open a new terminal window, add the commands to your shell configuration file, such as `.zshrc` or `.bashrc`. Many hosting services have features to help you add environment variables to your servers.
</Alert>
## Install dependencies
The app has a handful of dependencies, visible in `package.json`. To install them, run:
```bash
npm ci
```
## Run the app
To start the app, run:
```bash
npm start
```
Then navigate to `http://localhost:3000` (or your non-default port) in a web browser.
The app immediately redirects you to the Roblox login screen, at which point you can log in, verify the permissions that the app is requesting, and click **Confirm and Give Access**.
Roblox then redirects you back to `localhost`, where you can see that the app now displays some minimal user information, a link back to your profile on Roblox, and fields that you can use to broadcast messages across your experiences.
### About the app
In broad strokes, `index.js` performs the following operations:
1. Starts a new web server using `express`.
1. Retrieves the Roblox OpenID Connect (OIDC) configuration, which includes endpoints for authorization, user information, etc. For more information about these endpoints, see [OAuth 2.0 authentication](oauth2-reference.md).
1. Creates a new Open ID client using `openid-client` and your stored credentials. This client dramatically simplifies the process of properly forming and sending HTTP requests to the OAuth 2.0 endpoints.
1. Defines the routes for the app, including redirects for the login and logout flows and the OAuth 2.0 callback.
1. After a successful login, stores the various tokens as cookies, along with some minimal user information that it displays as HTML with help from `getHomeHtml.js`.

View file

@ -0,0 +1,339 @@
---
title: Open Cloud data stores
description: Explains how to use Open Cloud APIs to access and modify data stores.
---
In addition to accessing data stores from the Engine API in Studio or live experiences (`Class.DataStoreService`), you can use the Open Cloud APIs to access [standard](/cloud/reference/DataStoreEntry) and [ordered data stores](/cloud/reference/OrderedDataStoreEntry) from external scripts and other tools.
Open Cloud access to your data stores unlocks many potential use cases, including:
- A customer support portal that lets your team directly handle support requests, such as modifying user inventories or issuing refunds
- Global leaderboards that you can display on an external website
- Schema updates with scripts that read entries from the current data store, map it to the new schema, and write entries back to a new data store
The examples on this page demonstrate how to build a [user inventory support portal](#user-inventory-support-portal) and an [external leaderboard](#external-persistent-leaderboard)) with Node.js and Python, but use whichever language you prefer; the Open Cloud APIs support any programming language that can send an HTTP request.
## Differences from the Engine API
Although the Open Cloud APIs access the same underlying data stores and are similar to working with `Class.DataStoreService`, there are a few key differences:
- **Universe ID**: Unlike the Engine API, Open Cloud APIs are stateless and can come from anywhere, so you must always provide the **universe ID**, the unique identifier of your experience.
- **Separate permissions for creating and updating**: The Engine API creates new entries if they don't exist when you call `Class.DataStore:SetAsync()`, but Open Cloud methods for creating and updating entries are separate. Separate permissions can be safer and more flexible in certain situations. For example, you might want your customer support tool to only be able to edit an existing user's profile, not create a new one.
- **Data serialization**: All Open Cloud endpoints require you to serialize data before sending it. Serialization means converting an object into a string. Deserialization is the opposite, converting a string into an object. The Engine API serializes and deserializes entry content automatically, but for Open Cloud, you must generate or parse your entry to and from JSON on your own.
## Permissions
Data stores often store sensitive information, such as user profiles and virtual currency. To maintain security, each Open Cloud method has corresponding permissions, called **scopes**, that you must add to your API key, such as the `universe-datastores.control:list` scope for the [List Data Stores](/cloud/reference/DataStore#Cloud_ListDataStores) method. If you don't add the required permissions, your API call returns an error. See the reference documentation for the required scopes for each endpoint.
For more information on managing permissions, see [Manage API keys](../../auth/api-keys.md).
## User inventory support portal
This example uses a data store named `Inventory` and a schema for each entry of `"userId": {"currency": number, "weapon": string, "level": number}`. The key is `userId`.
### Required scopes
When [creating an API Key](../../auth/api-keys.md) for this example, add the following scopes to your key:
- `universe-datastores.objects:list`
- `universe-datastores.objects:read`
- `universe-datastores.objects:update`
Optionally, add the permissions for only the `Inventory` data store, set an IP address restriction, and set an expiration date.
### Add scripts for the user inventory support portal
After creating the API key with permissions required for the example app, you can create a script to make requests to the endpoints. These scripts get the first 10 entries in the data store, increment the `currency` value for each by 10, and then update each entry. For a larger data store, you would need to deal with [pagination](../../reference/patterns.md#pagination) using the `maxPageSize` and `pageToken` query parameters.
<Tabs>
<TabItem key = "1" label="Node.js">
```javascript title="incrementCurrency.js"
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error('The API_KEY environment variable is not set.');
}
const apiHeaderKey = 'x-api-key';
const universeId = '';
const dataStoreId = 'Inventory';
const baseUrl = 'https://apis.roblox.com/cloud/v2/';
async function listEntries(universe, dataStore) {
const listPath = `universes/${universe}/data-stores/${dataStore}/entries`;
const url = baseUrl + listPath;
const response = await fetch(url, {
headers: { [apiHeaderKey]: apiKey }
});
return response.json();
}
async function getEntry(path) {
const url = baseUrl + path;
const response = await fetch(url, {
headers: { [apiHeaderKey]: apiKey }
});
return response.json();
}
async function updateEntry(path, payload) {
const url = baseUrl + path;
const response = await fetch(url, {
method: 'PATCH',
headers: {
[apiHeaderKey]: apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload) // The body must be a string
});
return response;
}
(async () => {
try {
const entries = await listEntries(universeId, dataStoreId);
for (const entry of entries.dataStoreEntries) {
const path = entry.path;
console.log(`\nProcessing entry: ${path}`);
const currentData = await getEntry(path);
currentData.value.currency += 10;
const payload = { value: currentData.value };
const updateResponse = await updateEntry(path, payload);
console.log(`Status: ${updateResponse.status}`);
console.log(`Response: ${await updateResponse.text()}`);
}
} catch (error) {
console.error('An error occurred during execution:', error);
}
})();
```
To test, set the `API_KEY` environment variable, install dependencies, and run the script:
```bash
export API_KEY=<your_key>
node incrementCurrency.js
```
</TabItem>
<TabItem key = "2" label="Python">
```python title="increment_currency.py"
import json
import os
import requests
apiKey = str(os.environ['API_KEY'])
apiHeaderKey = 'x-api-key'
universe_id = ''
data_store_id = 'Inventory'
base_url = 'https://apis.roblox.com/cloud/v2/'
def list_entries(universe, data_store):
list_path = f'universes/{universe}/data-stores/{data_store}/entries'
url = base_url + list_path
return requests.get(url, params={}, headers={apiHeaderKey: apiKey})
def get_entry(path):
url = base_url + path
return requests.get(url, params={}, headers={apiHeaderKey: apiKey})
def update_entry(path, payload):
url = base_url + path
return requests.patch(url, params={}, headers={apiHeaderKey: apiKey, 'Content-Type': 'application/json'}, data=payload)
entries = list_entries(universe_id, data_store_id).json()
for entry in entries['dataStoreEntries']:
path = entry['path']
get_response = get_entry(path).json()
get_response['value']['currency'] += 10
json_payload = json.dumps({'value': get_response['value']})
update_response = update_entry(path, json_payload)
print(update_response.status_code)
print(update_response.text)
print()
```
To test, set the `API_KEY` environment variable, install dependencies, and run the script:
```bash
export API_KEY=<your_key>
python3 increment_currency.py
```
</TabItem>
</Tabs>
## External persistent leaderboard
This example creates a predefined list of users for demo purposes, but for it to be useful in a real experience, you would need an actual data store of users.
### Required scopes
When [creating an API Key](../../auth/api-keys.md) for this example, add the following scopes to your key:
- `universe.ordered-data-store.scope.entry:read`
- `universe.ordered-data-store.scope.entry:write`
### Add scripts for the leaderboard
After creating the API key with permissions required for the example app, you can create a script to make requests to the endpoints. These scripts add some sample entries to the ordered data store with random numbers and then retrieve them from highest to lowest value. For a larger data store, you would need to deal with [pagination](../../reference/patterns.md#pagination) using the `maxPageSize` and `pageToken` query parameters.
<Tabs>
<TabItem key = "1" label="Node.js">
```javascript title="leaderboard.js"
const apiKey = process.env.API_KEY;
const apiHeaderKey = 'x-api-key';
const universeId = '';
const orderedDataStoreId = 'PlayerScores';
const scopeId = 'global';
const baseUrl = 'https://apis.roblox.com/cloud/v2/';
async function createOrderedEntry(universe, orderedDataStore, entryId, payload) {
const createPath = `universes/${universe}/ordered-data-stores/${orderedDataStore}/scopes/${scopeId}/entries`;
const url = new URL(baseUrl + createPath);
url.searchParams.append('id', entryId);
const response = await fetch(url, {
method: 'POST',
headers: {
[apiHeaderKey]: apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API Error (${response.status}): ${errorText}`);
}
return response.text();
}
async function listOrderedEntries(universe, orderedDataStore) {
const listPath = `universes/${universe}/ordered-data-stores/${orderedDataStore}/scopes/${scopeId}/entries`;
const url = new URL(baseUrl + listPath);
url.searchParams.append('orderBy', 'value desc');
return fetch(url, {
headers: {
[apiHeaderKey]: apiKey,
},
});
}
async function main() {
if (!apiKey) {
console.error('Error: The API_KEY environment variable is not set.');
process.exit(1);
}
const entryNames = ['Ragdoll', 'Balinese', 'Tabby', 'Siamese'];
console.log('Creating sample data...');
for (const name of entryNames) {
try {
const randomValue = Math.floor(Math.random() * 50) + 1;
const payload = { value: randomValue };
const responseText = await createOrderedEntry(universeId, orderedDataStoreId, name, payload);
console.log(responseText);
} catch (error) {
console.error(`Failed to create entry for "${name}": ${error.message}`);
}
}
console.log('\nGetting sorted list of entries...');
try {
const playerScoresResponse = await listOrderedEntries(universeId, orderedDataStoreId);
console.log(playerScoresResponse.status);
const responseText = await playerScoresResponse.text();
console.log(responseText);
} catch (error) {
console.error(`Failed to list entries: ${error.message}`);
}
}
main();
```
To test, set the `API_KEY` environment variable, install dependencies, and run the script:
```bash
export API_KEY=<your_key>
node leaderboard.js
```
</TabItem>
<TabItem key = "2" label="Python">
```python title="leaderboard.py"
import json
import os
import random
import requests
apiKey = str(os.environ['API_KEY'])
apiHeaderKey = 'x-api-key'
universe_id = ''
ordered_data_store_id = 'PlayerScores'
scope_id = 'global'
base_url = 'https://apis.roblox.com/cloud/v2/'
def create_ordered_entry(universe, ordered_data_store, entry_id, payload):
create_path = f'universes/{universe}/ordered-data-stores/{ordered_data_store}/scopes/{scope_id}/entries'
url = base_url + create_path
return requests.post(url, params={'id': entry_id}, headers={apiHeaderKey: apiKey, 'Content-Type': 'application/json'}, data=payload)
def list_ordered_entries(universe, ordered_data_store):
list_path = f'universes/{universe}/ordered-data-stores/{ordered_data_store}/scopes/{scope_id}/entries'
url = base_url + list_path
return requests.get(url, params={'orderBy': 'value desc'}, headers={apiHeaderKey: apiKey})
# Create sample data
entryNames = ["Ragdoll", "Balinese", "Tabby", "Siamese"]
for name in entryNames:
random_value = random.randint(1, 50)
json_payload = json.dumps({'value': random_value})
create_response = create_ordered_entry(universe_id, ordered_data_store_id, name, json_payload)
print(create_response.text)
# Get list
player_scores = list_ordered_entries(universe_id, ordered_data_store_id)
print(player_scores.status_code)
print(player_scores.text)
```
To test, set the `API_KEY` environment variable, install dependencies, and run the script:
```bash
export API_KEY=<your_key>
python3 leaderboard.py
```
</TabItem>
</Tabs>

View file

@ -0,0 +1,57 @@
---
title: Handle Open Cloud data store requests
description: Explains how to handle Open Cloud API requests for data stores.
---
There are a number of considerations around sending requests to and handling responses from the Open Cloud data store APIs.
## HTTP headers
Like all Open Cloud endpoints, data store requests that use API key authentication must include the `x-api-key` header. For more information, see [Manage API keys](../../auth/api-keys.md).
- An incorrect or missing API key results in a 401 error.
- A valid API key that lacks the correct permissions results in a 403 error.
If a request includes a body, such as a create or update request, include the `Content-Type: application/json` and `Content-Length` headers. Most clients include these headers automatically.
## Pagination
Data stores often contain many entries, which means your code, particularly when calling [List Data Store Entries](/cloud/reference/DataStoreEntry#Cloud_ListDataStoreEntries__Using_Universes), must deal with a maximum number of results and pages. To learn more, see [Pagination](../../reference/patterns.md#pagination).
## Filters
If you use prefixes in the names of your data store entries, you likely want to filter your GET calls, like so:
```python
def list_entries(universe, data_store, filter):
list_path = f'universes/{universe}/data-stores/{data_store}/entries'
url = base_url + list_path
return requests.get(url, params={'filter': f'id.startsWith("global/{filter}")'}, headers={apiHeaderKey: apiKey})
```
Filter syntax is extremely limited. See the reference documentation for available filters for [standard](/cloud/reference/DataStoreEntry#Cloud_ListDataStoreEntries__Using_Universes) and [ordered](/cloud/reference/OrderedDataStoreEntry#Cloud_ListOrderedDataStoreEntries) data stores.
## Allow missing flags
The PATCH (update) methods for standard and ordered data stores have an `allow_missing` parameter. If set to true, this parameter lets you create entries that don't already exist rather than throwing an error. Leave this parameter at its default value of false unless you have a good reason not to; updating an entry that was never created often represents an error in programming logic.
## Scopes
Many requests require you to specify a scope, even if it's the default value of `global`. These scopes are distinct from permission scopes and let you subdivide and organize the entries in a data store.
Rather than creating non-default scopes, however, we strongly recommend using prefixes. To learn more, see [Listing and prefixes](../../../cloud-services/data-stores/versioning-listing-and-caching.md#listing-and-prefixes).
## Content-MD5
<Alert severity="info">
This section only applies to the Open Cloud v1 API.
</Alert>
The Open Cloud v1 API supports an optional base64-encoded MD5 checksum in some requests, which can catch data integrity issues. Many languages have built-in functions to calculate the value of the `content-md5` header. The following example uses Python:
```python
$ echo "750" | python -c "import base64, hashlib; print(str(base64.b64encode(hashlib.md5(bytes(input(), encoding='utf8')).digest()), encoding='utf8'))"
sTf90fedVsft8zZf6nUg8g==
```
If you run into issues generating a valid `content-md5` value, you might need to encode your request body in UTF-8 before computing the checksum.

View file

@ -0,0 +1,118 @@
---
title: Rate limits and throttling
description: Covers rate limits for the Open Cloud v2 Data Stores API.
---
The [Data Stores API](/cloud/reference/DataStore) enforces two types of throttling: **requests per minute** and **throughput**. Each experience allows a certain number of requests per minute and a certain amount of data per minute. These limits are enforced for the universe regardless of the number of API keys used.
Unlike the Engine API, these limits do **not** scale based on user count. Exceeding either one causes the endpoint to return `429 Too Many Requests`.
<Alert severity="info">
Data store rate limits for the [Open Cloud v1](/reference/cloud/datastores-api/v1) and [Open Cloud v2](/cloud/reference/DataStore) APIs are identical.
</Alert>
## Standard data stores throttling limits
<table>
<thead>
<tr>
<th>Request type</th>
<th>Method</th>
<th>Throttle limits</th>
</tr>
</thead>
<tbody>
<tr>
<td>Write</td>
<td>
<ul>
<li>Create Data Store Entry</li>
<li>Delete Data Store Entry</li>
<li>Update Data Store Entry</li>
<li>Increment Data Store Entry</li>
</ul>
</td>
<td>
<ul>
<li>10 MB/minute/universe write throughput</li>
<li>300 requests/minute/universe</li>
</ul>
</td>
</tr>
<tr>
<td>Read</td>
<td>
<ul>
<li>List Data Stores</li>
<li>List Data Store Entries</li>
<li>Get Data Store Entry</li>
<li>List Data Store Entry Revisions</li>
</ul>
</td>
<td>
<ul>
<li>20 MB/minute/universe read throughput</li>
<li>300 requests/minute/universe</li>
</ul>
</td>
</tr>
<tr>
<td>Delete</td>
<td>
<ul>
<li>Delete Data Store</li>
<li>Undelete Data Store</li>
</ul>
</td>
<td>
<ul>
<li>300 requests/minute/universe</li>
</ul>
</td>
</tr>
</tbody>
</table>
## Ordered data stores throttling limits
<table>
<thead>
<tr>
<th>Request type</th>
<th>Method</th>
<th>Throttle limits</th>
</tr>
</thead>
<tbody>
<tr>
<td>Write</td>
<td>
<ul>
<li>Create Ordered Data Store Entry</li>
<li>Delete Ordered Data Store Entry</li>
<li>Update Ordered Data Store Entry</li>
<li>Increment Ordered Data Store Entry</li>
</ul>
</td>
<td>
<ul>
<li>300 requests/minute/universe</li>
</ul>
</td>
</tr>
<tr>
<td>Read</td>
<td>
<ul>
<li>List Ordered Data Store Entries</li>
<li>Get Ordered Data Store Entry</li>
</ul>
</td>
<td>
<ul>
<li>300 requests/minute/universe</li>
</ul>
</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,107 @@
---
title: User notifications
description: Use Open Cloud to send experience notifications to users. Help players keep up with their favorite experiences through timely, personalized notifications.
---
import Intro from '../../includes/experience-notifications/intro.md'
<Intro components={props.components} />
After they receive a notification, users can join the experience directly via the **Join** button and spawn according to your [launch data](#include-launch-and-analytics-data).
For more information on features, eligibility requirements, usage guidelines, and the corresponding Engine API, see the [Experiences guide](../../production/promotion/experience-notifications.md).
## Implementation
The [UserNotification](/cloud/reference/UserNotification) resource lets you send experience notifications to users. Before using it, you must [generate an API key](../../cloud/auth/api-keys.md) or [configure OAuth 2.0](../../cloud/auth/oauth2-overview.md) for your app. The examples on this page use API keys.
To send an experience notification to a user:
1. [Create a notification string](../../production/promotion/experience-notifications.md#create-a-notification-string) in the [Creator Dashboard](https://create.roblox.com/dashboard/creations) (this step must be done in the Creator Dashboard; there's no Open Cloud API for it).
1. Form the request:
1. Copy the API key to the `x-api-key` request header.
1. Copy the notification string asset ID as the value of the `payload.message_id` property.
1. Set `payload.type` to `"MOMENT"`.
1. Set `source.universe` to be the universe resource URL `"universes/${UniverseID}"`.
```bash title="Send an experience notification"
curl --location 'https://apis.roblox.com/cloud/v2/users/${UserId}/notifications' \
--header 'x-api-key: ${ApiKey}' \
--header 'Content-Type: application/json' \
--data '{
"source": {
"universe": "universes/${UniverseID}"
},
"payload": {
"message_id": "${AssetID}",
"type": "MOMENT"
}
}'
```
Example response which returns the notification ID in the `id` field:
```json
{
"path": "users/505306092/notifications/6ca4d981-36fa-4255-82a1-14d95c116889",
"id": "6ca4d981-36fa-4255-82a1-14d95c116889"
}
```
### Customize notifications using parameters
To customize the notification for each recipient, include **parameters** in the [notification string](#implementation). Then customize the parameters when calling the API. For example, you can define the notification string as:
<p><Chip label="{userId-friend} beat your high score by {points} points! Time to level up?" size="large" color="primary" variant="outlined" /></p>
Add the `userId-friend` and `points` parameters in the script:
```bash title="Customize Notification Using Parameters"
curl --location 'https://apis.roblox.com/cloud/v2/users/${UserId}/notifications' \
--header 'x-api-key: ${ApiKey}' \
--header 'Content-Type: application/json' \
--data '{
"source": {
"universe": "universes/${UniverseID}"
},
"payload": {
"message_id": "${AssetID}",
"type": "MOMENT",
"parameters": {
"userId-friend": {"int64_value": 3702832553},
"points": {"string_value": "5"}
}
}
}'
```
### Include launch and analytics data
To further improve user experience, you can include **launch data** in the notification, useful for scenarios such as routing users to a coordinate location or personalizing the joining experience. Additionally, you can include [analytics](../../production/promotion/experience-notifications.md#analytics) data to segment the performance of different categories of notifications.
```bash title="Include Launch Data and Analytics Data"
curl --location 'https://apis.roblox.com/cloud/v2/users/${UserId}/notifications' \
--header 'x-api-key: ${ApiKey}' \
--header 'Content-Type: application/json' \
--data '{
"source": {
"universe": "universes/${UniverseID}"
},
"payload": {
"message_id": "${AssetID}",
"type": "MOMENT"
},
"join_experience": {
"launch_data": "Test_Launch_Data"
},
"analytics_data": {
"category": "Test_Analytics_Category"
}
}'
```
## Rate limits and delivery
Each user can receive **one** notification per day from a given experience, and you receive transparent feedback when a user's throttle limit is reached.
There are many other reasons that a notification might not be delivered. For more information, see [Delivery system](../../production/promotion/experience-notifications.md#delivery-system) in the Engine guide.

View file

@ -0,0 +1,14 @@
---
title: Open Cloud guides
description: In addition to using Engine and Studio tools to create experiences on Roblox, you can automate your internal workflows, improve your efficiency creating content, and support your experience operation needs from the web.
---
The Open Cloud guides provide detailed walkthroughs for popular Open Cloud use cases, such as [retrieving user inventories](inventory.md), [sending announcements](usage-messaging.md), or [using webhooks](../webhooks/webhook-notifications.md) to notify your apps of certain events.
Guides often include sample code in Python or Node.js and are a great way to get started, understand a resource, and spark ideas for your own usage.
For a comprehensive summary of available resources, see the Open Cloud API reference.
<a href="../index.md">
<Button variant="contained">Open Cloud API Reference</Button>
</a>

View file

@ -0,0 +1,442 @@
---
title: Engine instances
description: Explains how to use Open Cloud APIs to access the Roblox Engine Instances.
---
The Engine Open Cloud APIs let you manage `Class.Instance` objects in your Roblox experiences from the web.
## Beta restrictions
These APIs are currently in beta and have the following restrictions:
- You can only read and update `Class.Script`, `Class.LocalScript`, and
`Class.ModuleScript` objects.
- You can't update scripts that are currently open in Roblox Studio.
- You can't update scripts that are part of a [package](../../projects/assets/packages.md).
- You can only use API key authentication. Create an
[API key](../auth/api-keys.md) with **universe-place-instances** added as an API system.
You must specify the experiences you want the key to have access to as well as the desired read and write permission scopes.
![The Creator Hub API key screen](../../assets/open-cloud/instance-api.png)
- You must have a [collaborative](../../projects/collaboration.md) session enabled for the experience that you want to access.
- Request bodies, such as to [Update Instance](/cloud/reference/Instance#Cloud_UpdateInstance), are limited to 200 KB.
## List children
List all children of a specific instance by specifying an instance ID and
calling the [List Instance Children](/cloud/reference/Instance#Cloud_ListInstanceChildren)
method.
<Alert severity="info">
The first time you use these methods, specify `root` for the instance ID. After
you receive the list of top-level children and their IDs, you can make
additional calls to find instances deeper within your place's instance
hierarchy.
</Alert>
<Tabs>
<TabItem key = "1" label="Python">
```python
import requests
# Generate at https://create.roblox.com/dashboard/credentials
apiKey = "<API_KEY>"
# Find at https://create.roblox.com/dashboard/creations in the overflow menu of an experience tile
universeId = "<UNIVERSE_ID>"
# Find Start Place ID at https://create.roblox.com/dashboard/creations in the overflow menu of an experience tile
placeId = "<PLACE_ID>"
# The default ID for the root of any place's data model
instanceId = "root"
# Request header
apiKeyHeaderKey = "x-api-key"
# Endpoint URL for List Children method
listChildrenUrl = "https://apis.roblox.com/cloud/v2/universes/%s/places/%s/instances/%s:listChildren"
def ListChildren():
url = listChildrenUrl % (universeId, placeId, instanceId)
headerData = {apiKeyHeaderKey: apiKey}
results = requests.get(url, headers = headerData)
return results
response = ListChildren()
print("Operation Results:", response.status_code, response.text)
# Parse the Operation object's path to later obtain the Instance resource. See the Polling for Results section for more information.
operationPath = response.json()['path']
```
</TabItem>
<TabItem key = "2" label="cURL">
```bash
curl --include --location --request GET "https://apis.roblox.com/cloud/v2/universes/<universeId>/places/<placeId>/instances/<instanceId>:listChildren" --header "x-api-key: <api-key>"
```
</TabItem>
</Tabs>
Rather than just listing the children, the response includes an `Operation`
object with a different endpoint. [Poll this endpoint](#poll-for-results) to
asynchronously retrieve the actual list of children. A more complete code sample
looks like this:
```python
import requests
import time
apiKey = "<API_KEY>"
universeId = "<UNIVERSE_ID>"
placeId = "<PLACE_ID>"
instanceId = "root"
apiKeyHeaderKey = "x-api-key"
listChildrenUrl = "https://apis.roblox.com/cloud/v2/universes/%s/places/%s/instances/%s:listChildren"
getOperationUrl = "https://apis.roblox.com/cloud/v2/%s"
numberOfRetries = 10
retryPollingCadence = 5
doneJSONKey = "done"
def ListChildren():
url = listChildrenUrl % (universeId, placeId, instanceId)
headerData = {apiKeyHeaderKey: apiKey}
results = requests.get(url, headers = headerData)
return results
def GetOperation(operationPath):
url = getOperationUrl % (operationPath)
headerData = {apiKeyHeaderKey: apiKey}
results = requests.get(url, headers = headerData)
return results
def PollForResults(operationPath):
currentRetries = 0
while (currentRetries < numberOfRetries):
time.sleep(retryPollingCadence)
results = GetOperation(operationPath)
currentRetries += 1
if (results.status_code != 200 or results.json()[doneJSONKey]):
return results
response = ListChildren()
print("Operation Results:", response.status_code, response.text)
# Parse the Operation object's path to use in polling for the instance resource.
operationPath = response.json()['path']
response = PollForResults(operationPath)
print("Response:", response.status_code, response.text)
```
The final response might look something like this:
```json
{
"path": "universes/1234567890/places/98765432109/instances/root/operations/a1a2a3a4-a1a2-a1a2-a1a2-a1a2a3a4a5a6",
"done": true,
"response": {
"@type": "type.googleapis.com/roblox.open_cloud.cloud.v2.ListInstanceChildrenResponse",
"instances": [
{
"path": "universes/1234567890/places/98765432109/instances/b1b2b3b4-b1b2-b1b2-b1b2-b1b2b3b4b5b6",
"hasChildren": true,
"engineInstance": {
"Id": "b1b2b3b4-b1b2-b1b2-b1b2-b1b2b3b4b5b6",
"Parent": "a1a2a3a4-a1a2-a1a2-a1a2-a1a2a3a4a5a6",
"Name": "Workspace",
"Details": {}
}
},
...
]
}
}
```
You can then iterate over the JSON to find a particular instance ID:
```python
instances = response.json()['response']['instances']
replicatedStorageId = ""
for i in instances:
if i['engineInstance']['Name'] == "ReplicatedStorage":
replicatedStorageId = i['engineInstance']['Id']
if replicatedStorageId:
# You now have an instance ID and can get or update it.
else:
# The name wasn't found.
```
Scripts contain some additional information in the `Details` object, such as the script type, source, and whether they're enabled.
## Get an instance
This method returns a single [Instance](/cloud/reference/Instance).
<Tabs>
<TabItem key = "1" label="Python">
```python
import requests
# Generate at https://create.roblox.com/dashboard/credentials
apiKey = "<API_KEY>"
# Find at https://create.roblox.com/dashboard/creations in the overflow menu of an experience tile
universeId = "<UNIVERSE_ID>"
# Find Start Place ID at https://create.roblox.com/dashboard/creations in the overflow menu of an experience tile
placeId = "<PLACE_ID>"
# The default ID for the root of any place's data model
instanceId = "<INSTANCE_ID>"
# Request header
apiKeyHeaderKey = "x-api-key"
# Endpoint URL for Get Instance method
getInstanceUrl = "https://apis.roblox.com/cloud/v2/universes/%s/places/%s/instances/%s"
def GetInstance():
url = getInstanceUrl % (universeId, placeId, instanceId)
headerData = {apiKeyHeaderKey: apiKey}
return requests.get(url, headers = headerData)
response = GetInstance()
print("Response:", response.status_code, response.text)
# Parse the Operation object's path from response. See the Polling for Results section for more information.
operationPath = response.json()['path']
```
</TabItem>
<TabItem key = "2" label="cURL">
```bash
curl --include --location --request GET "https://apis.roblox.com/cloud/v2/universes/<universeId>/places/<placeId>/instances/<instanceId>" --header "x-api-key: <api-key>"
```
</TabItem>
</Tabs>
Just like the List Instance Children method, the response includes an
`Operation` object that you poll to retrieve the actual instance. See
[Poll for results](#poll-for-results) for more information.
## Update instances
After you obtain the appropriate instance ID, you can update it.
[Poll for results](#poll-for-results) after making the initial update
request.
<Tabs>
<TabItem key = "1" label="Python">
```python
import json
import requests
# Generate at https://create.roblox.com/dashboard/credentials
apiKey = "<API_KEY>"
# Find at https://create.roblox.com/dashboard/creations in the overflow menu of an experience tile
universeId = "<UNIVERSE_ID>"
# Find Start Place ID at https://create.roblox.com/dashboard/creations in the overflow menu of an experience tile
placeId = "<PLACE_ID>"
instanceId = "<INSTANCE_ID>"
instanceType = ""
propertyName = ""
propertyValue = ""
# Request header
apiKeyHeaderKey = "x-api-key"
contentTypeHeaderKey = "Content-type"
contentTypeHeaderValue = "application/json"
# Endpoint URL for Update Instance method
updateInstanceUrl = "https://apis.roblox.com/cloud/v2/universes/%s/places/%s/instances/%s"
# JSON keys
detailsJSONKey = "Details"
engineInstanceJSONKey = "engineInstance"
def GeneratePostData():
propertiesDict = {propertyName: propertyValue}
detailsDict = {instanceType: propertiesDict}
instanceDict = {detailsJSONKey: detailsDict}
outerDict = {engineInstanceJSONKey: instanceDict}
return json.dumps(outerDict)
def UpdateInstance(postData):
url = updateInstanceUrl % (universeId, placeId, instanceId)
headerData = {apiKeyHeaderKey: apiKey,
contentTypeHeaderKey: contentTypeHeaderValue}
return requests.patch(url, headers = headerData, data = postData)
postData = GeneratePostData()
response = UpdateInstance(postData)
print("Response:", response.status_code, response.text)
# Parse the Operation object's path from response. Poll for results to perform
# the update.
operationPath = response.json()['path']
```
</TabItem>
<TabItem key = "2" label="cURL">
```bash
curl --include --location --request PATCH "https://apis.roblox.com/cloud/v2/universes/<universeId>/places/<placeId>/instances/<instanceId>" --header "x-api-key: <api key>" --header "Content-Type: application/json" --data '{"engineInstance": {"details": {"<instanceType>": {"<propertyName>": "<propertyValue>"}}}}'
```
</TabItem>
</Tabs>
<Alert severity="info">
If you receive a `TypeError` when attempting to update a script, verify that the script isn't open in Roblox Studio. If it is, close its Script Editor tab. You can also check the Studio output for additional errors, such as `Engine_OC_API: Processing Error - Live scripting session is active`.
</Alert>
## Poll for results
All current [Instance](/cloud/reference/Instance) methods return an
`Operation` object instead of the resource you requested. This object lets you
asynchronously perform the original operation. Use the `Operation` object's
path (included in the initial response) to poll for when the resource is ready.
You can use a variety of polling strategies, such as using exponential backoff
or a fixed delay in between requests. The following example polls every five
seconds, up to 10 times.
<Tabs>
<TabItem key = "1" label="Python">
```python
import requests
import time
# Generate at https://create.roblox.com/dashboard/credentials
apiKey = "<API_KEY>"
# Use the Operation path from your initial request
# Takes the form of "universes/<UNIVERSE_ID>/places/<PLACE_ID>/instances/<INSTANCE_ID>/operations/<OPERATION_ID>"
operationPath = "<OPERATION_PATH>"
# Polling constants
numberOfRetries = 10
retryPollingCadence = 5
# Request header
apiKeyHeaderKey = "x-api-key"
# Endpoint URL for long-running operation polling
getOperationUrl = "https://apis.roblox.com/cloud/v2/%s"
# JSON keys
doneJSONKey = "done"
def GetOperation(operationPath):
url = getOperationUrl % (operationPath)
headerData = {apiKeyHeaderKey: apiKey}
results = requests.get(url, headers = headerData)
return results
def PollForResults(operationPath):
currentRetries = 0
while (currentRetries < numberOfRetries):
time.sleep(retryPollingCadence)
results = GetOperation(operationPath)
currentRetries += 1
if (results.status_code != 200 or results.json()[doneJSONKey]):
return results
response = PollForResults(operationPath)
print("Response:", response.status_code, response.text)
```
</TabItem>
<TabItem key = "2" label="cURL">
```bash
curl --include --location --request GET "https://apis.roblox.com/cloud/v2/universes/<universeId>/places/<placeId>/instances/<instanceId>/operations/<operationId>" --header "x-api-key:<api key>"
```
</TabItem>
</Tabs>
## Potion Shop demo
The Potion Shop Google Sheets demo shows you to update an experience's script
from the web. The demo consists of the following:
- An uncopylocked place with a mock UI that displays various potions and their
prices.
- A `.ods` file that you import into Google Sheets. This spreadsheet lets you
specify and update values for properties within the experience.
- Code for a Google Apps Script that reads data from the Google Sheet and
updates the **ReplicatedStorage > ItemList** script in the Potion Shop
experience.
### Set up the demo
1. Go to the [Potion Shop Demo](https://www.roblox.com/games/14215142052/Potion-Shop-Demo)
Discover page. Click on the overflow menu, and then **Edit in Studio**.
Studio opens with a copy of the place.
1. Click **File** &rarr; **Save to Roblox** and fill out the necessary information to
save the Potion Shop Demo as the default place for your experience. Test the
experience. You should see the UI for the Potion Shop Demo. Make a note of
the name, cost, and colors of the potions. You'll change them later using
Open Cloud!
### Set up the sheet
1. [Download](../../assets/open-cloud/open-cloud-potion-shop-demo.ods) the
Potion Shop spreadsheet file.
1. Go to [Google Sheets](https://sheets.google.com/) and click **Blank
Spreadsheet**.
1. In the sheet that appears, click **File > Import**, and click the
**Upload** tab.
1. Drag the Potion Shop spreadsheet file into the upload window.
1. Choose **Replace spreadsheet** and then **Import data**.
1. In the AppScript tab of the sheet, copy the code in cell A1.
1. Click the **Extensions > Apps Script** menu and paste the code into the
`Code.gs` file. Then save the project.
1. Back in the spreadsheet, select the **Update Potion Shop** tab. Click the
**Update Script** button, click the overflow menu, and select **Assign script**.
1. In the **Assign script** window, enter `UpdateScript` and click **OK**.
### Create an API key
1. Go to the [Creator Hub Open Cloud API Keys](https://create.roblox.com/dashboard/credentials?activeTab=ApiKeysTab) page and click **Create API Key**.
1. Fill out the form with the following information.
- **Name**: PotionShop
- **API System**: Add the **universe-place-instances** API system. Add your Potion Shop experience to the system. For **Experience Operations**, add read and write access.
- **Accepted IP Addresses**: Add 0.0.0.0/0 as an IP Address
- **Expiration**: No Expiration
- Click **Save & Generate Key** and then **Copy Key to Clipboard**.
1. Paste the API key to the API Key cell (D2) on the Intro tab of your Google
Sheet.
### Obtain the universe and place ID
1. Go to the [Creator Hub Creations](https://create.roblox.com/dashboard/creations) page, hover over
the Potion Shop's experience tile, and click the overflow menu.
1. Select **Copy Universe ID** and paste it into the **Universe ID** cell
(E2) on the Intro tab of your Google Sheet.
1. Select **Copy Start Place ID** and paste it into the **Place ID** cell
(F2) on the Intro tab of your Google Sheet.
### Update the script values
1. In the **Update Potion Shop** tab of the sheet, modify any values you'd
like and click the **Update Script** button.
1. If Google Sheets prompts you to for authorization, click **OK** and allow
your account to run the script.
1. In Studio, playtest the Potion Shop to see any reflected changes. From the
**Explorer** window, you can also open the **ReplicatedStorage >
ItemList** script to inspect the changes.

View file

@ -0,0 +1,159 @@
---
title: User inventories
description: Covers core use cases for the Inventory API, such as verifying that a user owns a particular item and filtering return values for categories of items.
---
The [Inventory API][1] lets you access most of the same information as the Roblox **My Inventory** page. You can check whether a user owns individual items and retrieve the full list of items that a player owns.
Responses from the [Inventory API][1] include items from the following categories:
- Clothing (accessories, bottoms, classic clothing, shoes, tops)
- Purchases and rewards (badges, passes, purchased places, private servers)
- Avatar items (avatar animations, classic heads, emotes, faces, hair, heads)
Before using the [Inventory API][1], you must [generate an API key](../auth/api-keys.md) or [configure OAuth 2](../auth/oauth2-overview.md) for your app. The examples on this page use API keys.
## Check item ownership
If you want to check whether a user owns a particular item (for example, a limited, badge, pass, or private server), use the `filter` parameter to check for a comma-separated list of one or more IDs. This code sample checks for three asset IDs:
<Tabs>
<TabItem key = "1" label="Node.js">
```js
const https = require('node:https');
const userId = 11111111111;
const hostname = 'apis.roblox.com';
const path = `/cloud/v2/users/${userId}/inventory-items`;
const params = '?filter=assetIds=62724852,1028595,4773588762';
const url = 'https://' + hostname + path + params;
const apiKey = '123456789012345678901234567890123456789012345678';
const options = {
headers: {
'x-api-key': `${apiKey}`,
},
};
https
.get(url, options, (response) => {
console.log('statusCode:', response.statusCode);
let data = '';
response.on('data', (d) => {
data += d;
});
response.on('end', () => {
if (response.statusCode === 200) {
const jsonData = JSON.parse(data);
console.log('Response Data:', JSON.stringify(jsonData, null, 2));
} else {
console.error('Error:', response.statusCode, response.statusMessage);
}
});
})
.on('error', (e) => {
console.error(e);
});
```
</TabItem>
<TabItem key = "2" label="Python">
```python
import requests
import json
userId = 11111111111
apiKey = '123456789012345678901234567890123456789012345678'
hostname = 'https://apis.roblox.com'
path = f'/cloud/v2/users/{userId}/inventory-items'
url = hostname + path
parameters = {
'filter': 'assetIds=62724852,1028595,4773588762'
}
headers = {'x-api-key': f'{apiKey}'}
response = requests.get(url, params=parameters, headers=headers)
print(json.dumps(json.loads(response.text), indent=2))
```
</TabItem>
</Tabs>
The following response indicates that the user owns one of the three items:
```json
{
"inventoryItems": [
{
"path": "users/11111111111/inventory-items/VVNFUl9BU1NFVF9JRD0yMDAxMDUxMTkzODg",
"assetDetails": {
"assetId": "1028595",
"inventoryItemAssetType": "CLASSIC_TSHIRT",
"instanceId": "200105119388"
}
}
],
"nextPageToken": ""
}
```
## Filter items
If you want to display, for example, only the collectibles that a user owns, use the same code as above, just with a different `filter` parameter.
<Tabs>
<TabItem key = "1" label="Node.js">
```js
const params = '?filter=onlyCollectibles=true;inventoryItemAssetTypes=*';
```
</TabItem>
<TabItem key = "2" label="Python">
```python
parameters = {
'filter': 'onlyCollectibles=true;inventoryItemAssetTypes=*'
}
```
</TabItem>
</Tabs>
Mix and match filters using a semicolon-separated list. Here are a few examples:
```text
filter=onlyCollectibles=true;inventoryItemAssetTypes=HAT,CLASSIC_PANTS
filter=badgeIds=111111,222222;gamePassIds=777777;privateServerIds=999999
filter=gamePasses=true;badges=true
```
Most calls to the API do not require any specific permissions, but several filters require Inventory read permissions. For more information, see [Filtering](../reference/patterns.md#list-inventory-items).
## Paginate results
If a response includes a value for `nextPageToken`, use that value in the
`pageToken` parameter of the subsequent request to retrieve the next page. For
more information, see [Pagination](../reference/patterns.md#pagination).
If the number of results is divisible by `maxPageSize` (for example, you have 50 results and a `maxPageSize` of 25), you can encounter a situation in which your response includes a value for `nextPageToken`, but a request using that token returns no results:
```json
GET /cloud/v2/users/{userId}/inventory-items?maxPageSize=25&pageToken=cccDDD
{
"inventoryItems": [],
"nextPageToken": ""
}
```
When implementing pagination in your app, best practice is to check not just for a non-empty `nextPageToken`, but also for a non-empty `inventoryItems` array.
[1]: /cloud/reference/InventoryItem

View file

@ -0,0 +1,74 @@
---
title: Secrets stores
description: Covers usage for the Secrets store API.
---
In addition to [managing your secrets within experiences](../../cloud-services/secrets.md), you can manage secrets using the Open Cloud secrets store API.
Before using the API, you must [generate an API key](../auth/api-keys.md) with the `secret-store` API system or [configure your OAuth 2.0 app](../auth/oauth2-overview.md) with the `universe.secret` scope type. The examples on this page use API keys.
## Secret encryption
When creating or updating secrets on Roblox, you must encrypt secrets with a [LibSodium sealed box](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes) and your experience's public key, and then base64-encode the result.
First, get your experience's public key:
```bash
curl --location 'https://apis.roblox.com/cloud/v2/universes/{universeId}/secrets/public-key' \
--request GET \
--header 'x-api-key: <your-secret-here>' \
```
Next, create a sealed box and base64-encode it. The example below uses Python and [PyNaCl](https://pynacl.readthedocs.io/en/latest/public/#nacl-public-sealedbox). (Run `pip install pynacl` to install it locally).
```python
from base64 import b64encode
from nacl import encoding, public
public_key = "Zgj4+V7vSaEZ06rXazKJUIcUnVa95tUNiwXAif/vdHo="
secret_content = "my_api_key_content"
public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder())
sealed_box = public.SealedBox(public_key)
encrypted = sealed_box.encrypt(secret_content.encode("utf-8"))
print(b64encode(encrypted).decode("utf-8"))
```
You can then create or update a secret using the output. This example creates a new secret:
```bash
curl --location 'https://apis.roblox.com/cloud/v2/universes/6930499524/secrets' \
--request POST \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-secret-here>' \
--data '{
"id": "mySecret",
"secret": "fP9scJkcDk492F4c1VHZ5QS8v2qsAg7uI+NVVEw6zC0GBnj7xpi7UrNr++lCfr4wyq3ia9Uuu+Ao8HtIXz2gRxBX",
"key_id": "1200590785272263122"
}'
```
After you create one, see [Use secrets](../../cloud-services/secrets.md#use-secrets) to use your secret in experience.
## Update secrets
To update the above secret:
```bash
curl --location 'https://apis.roblox.com/cloud/v2/universes/6930499524/secrets/mySecret' \
--request PATCH \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-secret-here>' \
--data '{
"secret": "2Fczw/PL7woOzHnGHQ65sT0MbzJjEOlfibyKxy374CqzFyEb2QTS8grtNBgG/0sfIvSHEo9JWN+pUr0NTPs0V6lj",
"key_id": "1200590785272263122"
}'
```
If you need to clean up a secret, a deletion request looks like this:
```bash
curl --location 'https://apis.roblox.com/cloud/v2/universes/6930499524/secrets/mySecret' \
--request DELETE \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-secret-here>'
```

View file

@ -0,0 +1,414 @@
---
title: Usage guide for assets
description: Explains how to use Open Cloud Web APIs for assets to support usage such as uploading and updating.
---
The [Assets API](../../reference/cloud/assets/v1.json) of Open Cloud allows you to upload and update assets with a single HTTP request rather than manually importing them to Studio. This API supports:
- Uploading new assets.
- Updating existing assets with version control.
- Updating asset metadata, including descriptions, display names, icons, and previews.
- Managing asset versions, such as rolling back to a specified previous version.
- Checking existing information of an asset, including metadata, versions, and any in-process updating operations.
<Alert severity="info">
This API contains beta endpoints that might be subject to changes for future releases.
</Alert>
## Supported asset types and limits
For endpoints that don't create a new asset or update the content of existing assets, there are no restrictions and limits. However, the asset content uploading functionality powered by the **Create Asset** and **Update Asset** endpoints only supports limited types of assets with restrictions. For each call, you can only create or update one asset with the file size up to 20 MB with the following limits:
<Alert severity="info">
Updating asset metadata using the **Update Asset** endpoint is not subject to the following limits.
</Alert>
<table>
<thead>
<tr>
<th>Asset type</th>
<th>Format</th>
<th>Content type</th>
<th>Restrictions</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="../../animation/index.md">Animation</a></td>
<td>
<ul>
<li>`.rbxm`</li>
<li>`.rbxmx`</li>
</ul>
</td>
<td>
<ul>
<li>`model/x-rbxm`</li>
<li>`model/x-rbxm`</li>
</ul>
</td>
<td>
<ul>
<li>`.rbxm` or `.rbxmx` files edited outside of [Roblox Studio](../../studio/setup.md) might not upload or function.</li>
</ul>
</td>
</tr>
<tr>
<td><a href="../../audio/assets.md">Audio</a></td>
<td>
<ul>
<li>`.mp3`</li>
<li>`.ogg`</li>
<li>`.wav`</li>
<li>`.flac`</li>
</ul>
</td>
<td>
<ul>
<li>`audio/mpeg`</li>
<li>`audio/ogg`</li>
<li>`audio/wav`</li>
<li>`audio/flac`</li>
</ul>
</td>
<td>
<ul>
<li>Up to 7 minutes of duration.</li>
<li>Up to 100 uploads per month if you're ID-verified.</li>
<li>Up to 10 total uploads per month if you aren't ID-verified.</li>
<li>Not available for updating.</li>
</ul>
</td>
</tr>
<tr>
<td><a href="../../parts/textures-decals.md">Decal, Image</a></td>
<td>
<ul>
<li>`.png`</li>
<li>`.jpeg`</li>
<li>`.bmp`</li>
<li>`.tga`</li>
</ul>
</td>
<td>
<ul>
<li>`image/png`</li>
<li>`image/jpeg`</li>
<li>`image/bmp`</li>
<li>`image/tga`</li>
</ul>
</td>
<td>
<ul>
<li>Must be smaller than 8000x8000 pixels.</li>
<li>Not available for updating.</li>
</ul>
</td>
</tr>
<tr>
<td><a href="../../parts/meshes.md">Mesh</a></td>
<td>
<ul>
Roblox only
</ul>
</td>
<td>
<ul>
<li>`model/x-file-mesh-data`</li>
</ul>
</td>
<td>
<ul>
<li>Only content downloaded from [Asset delivery API](../api/asset-delivery.md) is accepted. If you are not trying to download and re-upload meshes, then use [3D Importer](../../art/modeling/3d-importer.md) to import meshes instead.</li>
<li>Not available for updating.</li>
</ul>
</td>
</tr>
<tr>
<td><a href="../../parts/models.md">Model</a></td>
<td>
<ul>
<li>`.fbx`</li>
<li>`.gltf`</li>
<li>`.glb`</li>
<li>`.rbxm`</li>
<li>`.rbxmx`</li>
</ul>
</td>
<td>
<ul>
<li>`model/fbx`</li>
<li>`model/gltf+json`</li>
<li>`model/gltf-binary`</li>
<li>`model/x-rbxm`</li>
<li>`model/x-rbxm`</li>
</ul>
</td>
<td>
<ul>
<li>Imports custom 3D models as a `Class.Model` container containing one or more `Class.MeshPart` objects.</li>
<ul>
<li>Depending on your use-case, consider uploading custom 3D models manually using the [3D Importer](../../art/modeling/3d-importer.md).</li>
<li>The 3D Importer provides a 3D preview, various error-checking, and many customizable import settings.</li>
</ul>
<li>`.rbxm` or `.rbxmx` files edited outside of [Roblox Studio](../../studio/setup.md) might not upload or function.</li>
</ul>
</td>
</tr>
<tr>
<td><a href="../../ui/video-frames.md">Video</a></td>
<td>
<ul>
<li>`.mp4`</li>
<li>`.mov`</li>
</ul>
</td>
<td>
<ul>
<li>`video/mp4`</li>
<li>`video/mov`</li>
</ul>
</td>
<td>
<ul>
<li>Up to 5 minutes of duration.</li>
<li>Up to 4096x2160 resolution.</li>
<li>Up to 3.75 GB.</li>
<li>Up to 20 uploads per day if you're 13+ and ID-verified.</li>
</ul>
</td>
</tr>
</tbody>
</table>
## Security permissions
The API supports both first-party use with [API key authorization](../auth/api-keys.md) and third-party use in [OAuth 2 applications](../auth/oauth2-overview.md). Each way requires different security permission settings.
### API keys
To use the API in your own scripts or tools, you need to [create an API key](../auth/api-keys.md#create-api-keys) for authorization and security.
<Alert severity="warning">
To create an API key for managing group assets, you must have the corresponding permissions. For more information on granting group permissions, see [Group roles and permissions](../../projects/groups.md#roles-and-permissions).
</Alert>
When creating an API key, make sure to add the following permissions:
1. Add **assets** to **Access Permissions**.
1. Add **Read** and **Write** operation permissions to your selected experience, depending on the required scopes of the endpoints you plan to call.
Once you have the API key, copy it to the `x-api-key` request header. All endpoints require the `x-api-key` request header.
```bash title ="Example API Request Header"
--header 'x-api-key: ${ApiKey}' \
```
### OAuth 2.0 apps
To use the API for a third-party OAuth 2.0 application, add the `asset:read` and `asset:write` permission scopes when [registering your app](../auth/oauth2-registration.md#add-permissions). Choose these scopes based on the requirements of the endpoints you plan to use.
## Create a new asset
To upload a new asset by an HTTP request:
1. Copy the API key to the `x-api-key` request header of the [Create Asset](../../reference/cloud/assets/v1.json#POST-v1-assets) endpoint.
1. In your request:
1. Specify the target [asset type](#supported-asset-types-and-limits).
1. Add your asset name and description.
1. Add the creator information.
- If you want to create the asset **on your own behalf**, add your user ID. You can find your user ID on the URL of your Roblox profile. For example, for `https://www.roblox.com/users/1234567/profile`, your user ID is `1234567`.
- If you want to create the asset **as a group asset**, add the group ID of your group. You can find the group ID on the URL of your group's page. For example, for `https://www.roblox.com/groups/7654321/example-group#!/`, the group ID is `7654321`.
1. Add the file path and content type of your asset.
```bash title ="Example Request for Create Asset"
curl --location 'https://apis.roblox.com/assets/v1/assets' \
--header 'x-api-key: ${ApiKey}' \
--form 'request="{
\"assetType\": \"Model\",
\"displayName\": \"Name\",
\"description\": \"This is a description\",
\"creationContext\": {
\"creator\": {
\"userId\": \"${userId}\" # Use groupId for creating a group asset
}
}
}"' \
--form 'fileContent=@"/filepath/model.fbx";type=model/fbx'
```
## Update an existing asset
<Alert severity="info">
Asset updating is powered by beta endpoints that might be subject to changes for future releases.
</Alert>
To update an existing asset by an HTTP request:
1. Copy the API key to the `x-api-key` request header of the [Update Asset](../../reference/cloud/assets/v1.json#PATCH-v1-assets-_asset_) endpoint.
1. Add the asset type and asset ID in your request. To copy your asset ID:
1. Navigate to the [Creation](https://create.roblox.com/dashboard/creations) page of the **Creator Dashboard**.
1. Select the **Development Items** category.
1. Select the category of your asset and find the target asset.
1. Hover over the thumbnail of the target asset and click the **&ctdot;** button to display a list of options, then select **Copy Asset ID** from the list.
<Tabs>
<TabItem label="Asset Content">
<Alert severity="warning">
Currently, you can only update the asset content for `.fbx` files. The update creates a new version.
</Alert>
```bash title ="Example Request for Updating Asset Content"
curl --location --request PATCH 'https://apis.roblox.com/assets/v1/assets/{assetId}' \
--header 'x-api-key: {apiKey}' \
--form 'request={
\"assetType\": \"{assetType}\",
\"assetId\": \"{assetId}\",
\"creationContext\": {
\"creator\": {
\"userId\": {userId}
},
\"expectedPrice\":{expectedPrice}
},
}' \
--form 'fileContent=@"{file-path}"'
```
</TabItem>
<TabItem label="Metadata">
```bash title ="Example Request for Updating Asset Metadata"
curl --location --request PATCH 'https://apis.roblox.com/assets/v1/assets/{assetId}?updateMask=description%2CdisplayName' \
--header 'x-api-key;' \
--form 'request={
\"assetType\": \"{assetType}\",
\"assetId\": \"{assetId}\",
\"displayName\": \"{new display name}\",
\"description\": \"{new description}\"
}'
```
</TabItem>
<TabItem label="Content and Metadata">
```bash title ="Example Request for Updating Both Asset Content and Metadata"
curl --location --request PATCH 'https://apis.roblox.com/assets/v1/assets/{assetId}?updateMask=description%2CdisplayName' \
--header 'x-api-key: {apiKey}' \
--form 'request={
\"assetType\": \"{assetType}\",
\"assetId\": \"{assetId}\",
\"displayName\": \"{new display name}\",
\"description\": \"{new description}\",
\"creationContext\": {
\"creator\": {
\"userId\": {userId}
},
\"expectedPrice\":{expectedPrice}
},
}' \
--form 'fileContent=@\"{file-path}\"'
```
</TabItem>
</Tabs>
## Retrieve asset operation status
If your request for creating a new asset or updating an existing asset succeeds, it returns an **Operation ID** in the format of `{ "path": "operations/${operationId}" }`. You can use it to check the status and result of your upload with the following steps:
1. Copy the API key to the `x-api-key` request header of the [Get Operation](../../reference/cloud/assets/v1.json#GET-v1-operations-_operationId_) method and send the request, like the following code sample:
```bash title ="Example Request for Get Operation"
curl --location 'https://apis.roblox.com/assets/v1/operations/{operationId}' \
--header 'x-api-key: {$ApiKey}'
```
2. If your request succeeds, it returns an `Operation` object, either including a `response` representing the uploaded asset information or a `status` explaining why the asset upload fails as the following code sample shows:
```json title ="Example Response for Get Operation"
{
"path": "operations/{operationId}",
"done": true,
"response": {
"@type": "type.googleapis.com/roblox.open_cloud.assets.v1.Asset",
"path": "assets/2205400862",
"revisionId": "1",
"revisionCreateTime": "2023-03-02T22:27:04.062164400Z",
"assetId": "2205400862",
"displayName": "Name",
"description": "This is a description",
"assetType": "ASSET_TYPE_DECAL",
"creationContext": {
"creator": {
"userId": "11112938575"
}
},
"moderationResult": {
"moderationState": "MODERATION_STATE_APPROVED"
}
}
}
```
3. (Optional) Check the created asset on your Roblox account.
1. Navigate to the **Inventory** page of your [Roblox account](https://www.roblox.com/home).
2. Select the **Category** of the asset that you want to check.
3. Find the target asset and click its thumbnail to view the asset.
## Add assets API to OAuth 2.0 apps
You can create [OAuth 2.0 applications](../../cloud/auth/oauth2-overview.md) supporting Assets API to allow your users to upload and update assets to Roblox.
<Alert severity="warning">
Third-Party app support through OAuth 2.0 is a Beta feature that might be subject to changes for future releases.
</Alert>
To use Assets API for your application and request permissions from your users, perform the following settings:
1. When [registering your application](../auth/oauth2-registration.md#register-your-app), under **Permissions**, select **asset:read** and **asset:write** scopes.
1. When [implementing the authorization flow](../../cloud/auth/oauth2-overview.md#implement-authorization-flows), include `asset:read` and `asset:write` as the scope parameters of the authorization URL that redirects users back to your application, like the following example:
```plain
https://authorize.roblox.com?client_id=819547628404595165403873012&redirect_uri=https://my-app.com/redirect&scope=asset:read+asset:write&response_type=Code&prompts=login+consent&nonce=12345&state=6789
```
1. When sending the request, include the access token in the authorization header and the form data of the asset content to create or update in the request URI in. The following example shows a sample request for uploading a new asset:
```bash title="Example Request"
curl --location --request POST 'https://apis.roblox.com/assets/v1/assets' \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--form 'request="{
\"assetType\": \"Decal\",
\"displayName\": \"DecalDemo123\",
\"description\": \"This is a description\",
\"creationContext\": {
\"creator\": {
\"userId\": \"<user_id>\"
}
}
}"' \
--form 'fileContent=@"/filepath/p1.png"'
```

View file

@ -0,0 +1,149 @@
---
title: Messaging usage guide
description: Explains how to use Open Cloud Messaging Service API to support cross-server messaging.
---
The [Messaging Service API](/cloud/reference/Universe#Cloud_PublishUniverseMessage) is the Open Cloud equivalent of the Engine `Class.MessagingService`, which lets you communicate across your experience's servers and clients. The Engine API only allows you to write and update scripts manually in Studio for publishing messages, but the Open Cloud API lets you send messages to live servers from external tools to automate and improve your operations workflows.
## Usage
There are several helpful tools that you can build by supporting cross-server communication with the Messaging Service API, including:
- **Announcement Portals**: A web portal can be helpful to support sending announcements to all users across servers in your experience, such as announcing an upcoming event, an update, and the winner for a competition. On the portal, you can edit a message and click a button that calls the API to send the message out for all users or selected users.
- **Moderation System**: A moderation system can help keep your experience safe and secure. When detecting a user with inappropriate behavior, you can publish a message to trigger the experience server to warn or ban the specific user. You can also use [data stores](/cloud/reference/DataStore) in the moderation system to add user accounts to a blocklist that prevents them from rejoining.
- **LiveOps Dashboard**: LiveOps dashboards are useful tools for managing live events, such as a Halloween party. On the dashboard, you can pre-code an event, update event messages, trigger the event when it's ready, and reward selected users with special items like a virtual crown without updating any of the experience's code.
<Alert severity="info">
Currently, the API can only target live experience servers through HTTP.
</Alert>
## Limits
| Limit | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Rate** | Roblox throttles message requests at `50 + (5 * number_of_players_in_experience)`. For example, an experience with 20 players begins to throttle at 150 message requests per minute. |
| **Topic size** | 80 characters |
| **Message size** | 1,024 characters (1 KiB) |
## Set up a topic for messaging
Before you can publish a message to your experience's live servers, you must set up a **topic**, which is a customized message channel that is accessible from multiple servers. After defining a topic, you subscribe users to the topic in order to receive your incoming messages.
Currently, you can only define a topic in Studio and use the Luau API `Class.MessagingService:SubscribeAsync()` to subscribe users to it. The following code sample subscribes any user to a topic when they join the experience:
```lua title= 'Set up and Subscribe Users to a Topic'
local MessagingService = game:GetService("MessagingService")
local Players = game:GetService("Players")
local function onPlayerAdded(player)
-- Define and subscribe to the topic
local topic = "YourTopic"
local connection = MessagingService:SubscribeAsync(topic, function(message)
print(message.Data)
end)
player.AncestryChanged:Connect(function()
-- Unsubscribe from the topic upon player ancestry change
connection:Disconnect()
end)
end
Players.PlayerAdded:Connect(onPlayerAdded)
```
## Publish messages to live servers
After [setting up](#set-up-a-topic-for-messaging) a topic, publish a message to your experience's live servers:
1. [Create an API key](../auth/api-keys.md#create-api-keys) on [Creator Dashboard](https://create.roblox.com/dashboard/credentials?activeTab=ApiKeysTab) and copy it somewhere safe. Make sure you perform the following settings:
1. Add **messaging-service** to **Access Permissions**.
2. Select an experience, and add the **universe-messaging-service:publish** operation.
2. Get the **Universe ID** for your experience:
1. Navigate to the [Creator Dashboard](https://create.roblox.com/dashboard/creations).
1. Find the experience that you want to publish your messages to.
1. Hover over an experience's thumbnail, click the **&ctdot;** button, and select **Copy Universe ID**.
<img src="../../assets/creator-dashboard/Options-Button-Experience-Public.png" width="200" />
3. Add the API key and universe to a `POST` request, as in this example:
```bash title="Example Request for Publishing a Message"
curl -L -X POST 'https://apis.roblox.com/cloud/v2/universes/{universe}:publishMessage' \
-H 'x-api-key: {api-key}' \
-H 'Content-Type: application/json' \
--data '{
"topic": "your-topic",
"message": "Hello, everyone!"
}'
```
4. Send the HTTP request to publish the message.
<Alert severity="info">
If your request succeeds, it returns HTTP 200 with an empty response body.
</Alert>
## Add the Messaging Service API to OAuth 2.0 apps
You can create [OAuth 2.0 applications](../../cloud/auth/oauth2-overview.md) that allow your users to publish messages to their own live servers.
<Alert severity="warning">
Third-party app support through OAuth 2.0 is a beta feature that might be subject to changes for future releases.
</Alert>
To use Messaging Service API for your application and request permissions from your users, perform the following settings:
1. When [registering your application](../auth/oauth2-registration.md#register-your-app), under **Permissions**, select the **universe-messaging-service:publish** scope.
2. When [implementing the authorization flow](../../cloud/auth/oauth2-overview.md), include `universe-messaging-service:publish` in the `scope` parameter of the authorization URL that redirects users back to your application, like the following example:
```plain
https://authorize.roblox.com?client_id=816547628409595165403873012&redirect_uri=https://my-app.com/redirect&scope=openid+universe-messaging-service:publish&response_type=Code&prompts=login+consent&nonce=12345&state=6789
```
3. Request access to the `universeId` of the experience that the user wants to publish their messages to. Your application can send a `POST` request to the [token resources endpoint](../auth/oauth2-reference.md#token-exchange) with the access token, client ID and secret or the `code challenge`, depending on your [implementation of your authorization flow](../../cloud/auth/oauth2-overview.md#implementing-authorization-flows), as request parameters to get a list of `universeIds` of experiences that the user granted permission to:
```bash title="Example Request"
curl --location --request POST 'https://apis.roblox.com/oauth/v1/token/resources' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'token=<access_token>' \
--data-urlencode 'client_id=<client_id>' \
--data-urlencode 'client_secret=<client_secret>'
```
This endpoint returns a list of `universeIds` like the following example response:
```json title="Example Response"
{
"resource_infos": [
{
"owner": {
"id": "1516563360",
"type": "User"
},
"resources": {
"universe": {
"ids": ["3828411582"]
}
}
}
]
}
```
4. Your application can now send messages to any experience that a user has granted permission. When sending the request, include the access token in the authorization header and the `universeId` and topic in the request URI in the following format:
```bash title="Example Request"
curl --location --request POST 'https://apis.roblox.com/cloud/v2/universes/{universe}:publishMessage' \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data-raw '{"topic": "some-topic","message":"message to publish"}'
```
## Use with HTTPService
This API is one of the [Open Cloud endpoints supported](../../cloud-services/http-service.md#supported-open-cloud-endpoints) by `Class.HttpService`. You can use it to publish messages across different experiences from within an experience.

View file

@ -0,0 +1,67 @@
---
title: Usage guide for place publishing
description: Explains how to use Open Cloud Place Publishing API to publish places programmatically with version control.
---
The [place publishing API](../../reference/cloud/universes-api/v1.json) offers similar functionality to [publishing to Roblox](../../production/publishing/publish-experiences-and-places.md) in Studio, with extra permission control and automation in continuous release workflow. You can use this API to update existing places of an experience to a new version, making it useful for automating your publishing workflow. For example, you can call this API from a GitHub action and have it automatically push a binary place file to Roblox after successful integration testing.
## Limitations
- This API only supports HTTPS requests.
- The place publishing API doesn't update certain instance types. If your experience contains `Class.EditableImage`, `Class.EditableMesh`, `Class.PartOperation`, `Class.SurfaceAppearance`, or `Class.BaseWrap` instances, publish from Studio after modifying them.
## Places on Roblox
Experiences on Roblox can have multiple places, with one **starting place** as the user entry point of your experience and optional other places that you can [teleport users between](../../projects/teleport.md). You can use the API to publish either the starting place or other places of your experience with version control.
Before you can update an existing place of an experience, you need the place's **Place ID** and the experience's **Universe ID**. The **Place ID** identifies the place and the **Universe ID** identifies the experience. Each is unique even if your experience has only one place.
## Publish a place
To publish an existing place of an experience with [Place Publishing API](../../reference/cloud/universes-api/v1.json):
1. [Create an API key](../auth/api-keys.md#create-api-keys) on the **Creator Dashboard**. Make sure you perform the following settings:
1. Add **universe-places** to **Access Permissions**.
1. Add **Write** operation to your selected experience.
1. Get the **Universe ID** of the experience in which you want to publish the place.
1. Navigate to the [Creator Dashboard](https://create.roblox.com/dashboard/creations).
1. Find the experience with the place that you want to update.
1. Hover over the experience's thumbnail, click the **&ctdot;** button, and select **Copy Universe ID**.
<img src="../../assets/creator-dashboard/Options-Button-Experience-Public.png" width="200" />
1. Get the **Place ID** of the place that you want to update.
1. Stay on the [Creations](https://create.roblox.com/dashboard/creations) page on **Creator Dashboard** and click the thumbnail of the target experience.
1. On the left navigation menu, click the **Places** tab.
1. Find the place that you want to update and click its thumbnail.
1. The **Place ID** is in the redirected URL. For example, in the URL `https://create.roblox.com/dashboard/creations/experiences/0000000/places/111111/configure`, the **Place ID** is `111111`.
1. Add the API Key in the **x-api-key** header of a `POST` request to the API. The following two example requests reference a Roblox XML place file (`.rbxlx`) and a binary place file (`.rbxl`).
```bash title="XML Place File Request"
$ curl --verbose --location POST 'https://apis.roblox.com/universes/v1/{universeId}/places/{placeId}/versions?versionType=Published' \
--header 'x-api-key: <your-api-key>' \
--header 'Content-Type: application/xml' \
--data-binary @/home/placefiles/place1.rbxlx
```
```bash title="Binary Place File Request"
$ curl --verbose --location POST 'https://apis.roblox.com/universes/v1/{universeId}/places/{placeId}/versions?versionType=Published' \
--header 'x-api-key: <your-api-key>' \
--header 'Content-Type: application/octet-stream' \
--data-binary @/home/placefiles/place1.rbxl
```
To run the commands, replace `{universeId}` and `{placeId}` with the actual **Universe ID** and **Place ID** of the experience and place that you want to publish.
1. If you send your request correctly, you receive a success response body with the place version number in the following format:
```json title="Example Response Body"
{ "versionNumber": 7 }
```
1. (Optional) Verify the upload in Studio or on [Creator Dashboard](https://create.roblox.com/dashboard/creations).

View file

@ -0,0 +1,35 @@
---
title: Cloud API reference
description: Get comprehensive API reference documentation for Open Cloud.
---
With Open Cloud, you can access Roblox resources through standard [REST](https://en.wikipedia.org/wiki/REST) APIs, which lets you build everything from command line automation tools to complex web apps. You can update experiences, restart servers, work with your data stores and memory stores, manage user restrictions, list inventory items, and much, much more.
This reference is broken into two sections:
- A [section that separates endpoints by feature](/cloud/reference/features/accounts) (Avatars, Game Passes, Users, etc.)
- A [section that separates endpoints by domain](/cloud/reference/domains/apis) (base URL)
**Both sections** contain the full list of available API endpoints. We recommend the [features section](/cloud/reference/features/accounts) since it helps consolidate endpoints by use case, but experienced Open Cloud developers might prefer to browse by domain.
- Whenever possible, use endpoints that support [API keys](./auth/api-keys.md) or [OAuth 2.0](./auth/oauth2-overview.md) for authentication. They have strong stability guarantees and receive regular updates.
- Legacy APIs use cookie-based authentication, can incorporate breaking changes without notice, and have minimal stability guarantees. We don't recommend them for production applications.
<Alert severity="info">
Roblox also offers [webhooks](./webhooks/webhook-notifications.md), which can notify your applications when certain events occur, such as refunds or changes to subscriptions.
</Alert>
## Get started with Open Cloud
1. Set up authentication for your application.
See the documentation for how to use [API keys](./auth/api-keys.md) or [OAuth 2.0](./auth/oauth2-overview.md). API keys are the easiest way to get started.
1. Test API calls using tools like [Postman](https://www.postman.com) with [OpenAPI descriptions](./reference/openapi.md) or the [OAuth 2.0 sample app](./auth/oauth2-sample.md).
1. Review the [resource guides](./guides/index.md) for end-to-end walkthroughs of using certain APIs.
1. Explore the left navigation for the full list of features, [common API patterns](./reference/patterns.md), [types](./reference/types.md), and [error codes](./reference/errors.md).
## Make requests within experiences
`Class.HttpService` lets you make HTTP requests to a subset of the Open Cloud endpoints. For more information, see [In-experience HTTP requests](../cloud-services/http-service.md).

View file

@ -0,0 +1,219 @@
---
title: Errors
description: Defines the errors that are returned by Open Cloud APIs
---
The following sections describe the error handling for Open Cloud APIs. Due to implementation differences across endpoints and validation layers, error responses can vary significantly in format.
## Gateway errors
For authentication or routing issues, both Open Cloud v1 and v2 APIs may return errors in this format:
```json title="Example gateway error"
{
"errors": [
{
"code": 0,
"message": "Invalid API Key"
}
]
}
```
## Open Cloud v2
Open Cloud v2 APIs generally follow a consistent error format when the error occurs within the API endpoint itself.
### Standard v2 error format
Open Cloud v2 APIs return errors in this format:
- `code` - A string representing the error type (e.g. `INVALID_ARGUMENT`, `NOT_FOUND`).
- `message` - A human-readable message explaining the error.
- `details` - An optional array containing additional error-specific information.
```json title="Example v2 error"
{
"code": "INVALID_ARGUMENT",
"message": "Invalid User ID in the request."
}
```
```json title="Example v2 error with details"
{
"code": "INVALID_ARGUMENT",
"message": "The provided filter is invalid.",
"details": [
{
...
}
]
}
```
### v2 error codes
The following table describes possible values for `code` in v2 API responses.
<table>
<thead>
<tr>
<th>Code</th>
<th>HTTP Status</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`INVALID_ARGUMENT`</td>
<td>400</td>
<td>You passed an invalid argument, such as an invalid `universeId`. You might also have missing or invalid headers, such as `Content-Length` and `Content-Type`.</td>
</tr>
<tr>
<td>`PERMISSION_DENIED`</td>
<td>403</td>
<td>Your request doesn't have sufficient permissions or scopes to perform the operation.</td>
</tr>
<tr>
<td>`NOT_FOUND`</td>
<td>404</td>
<td>The system can't find your specified resources, such as a data store entry.</td>
</tr>
<tr>
<td>`ABORTED`</td>
<td>409</td>
<td>The operation was aborted.</td>
</tr>
<tr>
<td>`RESOURCE_EXHAUSTED`</td>
<td>429</td>
<td>You don't have enough quota to perform the operation, typically due to sending too many requests.</td>
</tr>
<tr>
<td>`CANCELLED`</td>
<td>499</td>
<td>The system terminates the request, typically due to a client side timeout.</td>
</tr>
<tr>
<td>`INTERNAL`</td>
<td>500</td>
<td>Internal server error, typically due to a server bug.</td>
</tr>
<tr>
<td>`NOT_IMPLEMENTED`</td>
<td>501</td>
<td>The server doesn't implement the API method.</td>
</tr>
<tr>
<td>`UNAVAILABLE`</td>
<td>503</td>
<td>Service is unavailable, typically returned when the server is down.</td>
</tr>
</tbody>
</table>
## Open Cloud v1
Open Cloud v1 APIs have inconsistent error response formats. The format depends on the specific endpoint, the type of error, and where the error occurs in the request processing pipeline.
Most v1 endpoints return errors in one of these three formats:
```json title="Example v1 error with error field"
{
"error": "INVALID_ARGUMENT",
"message": "Invalid cursor.",
"errorDetails": [
{
"errorDetailType": "DatastoreErrorInfo",
"datastoreErrorCode": "InvalidCursor"
}
]
}
```
```json title="Example v1 error with code field"
{
"code": "INVALID_ARGUMENT",
"message": "Invalid cursor."
}
```
```json title="Example v1 detailed validation error"
{
"errors": {
"assetId": ["The value 'a' is not valid."]
},
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"extensions": {
"traceId": "00-427917f0fc3b8375ee33e4603a7f0693-f3f6ad560ff1a122-00"
}
}
```
### v1 error codes
The following table describes possible values for `error` or `code` in v1 API responses:
<table>
<thead>
<tr>
<th>HTTP Status Code</th>
<th>Error</th>
<th>Descriptions</th>
</tr>
</thead>
<tbody>
<tr>
<td>400</td>
<td>INVALID_ARGUMENT</td>
<td>You passed an invalid argument, such as an invalid `universeId`. You might also have missing or invalid headers, such as `Content-Length` and `Content-Type`.</td>
</tr>
<tr>
<td>403</td>
<td>INSUFFICIENT_SCOPE</td>
<td>The request requires higher privileges than provided by the access token.</td>
</tr>
<tr>
<td>403</td>
<td>PERMISSION_DENIED</td>
<td>Your request doesn't have sufficient scope to perform the operation.</td>
</tr>
<tr>
<td>404</td>
<td>NOT_FOUND</td>
<td>The system can't find your specified resources, such as a data store.</td>
</tr>
<tr>
<td>409</td>
<td>ABORTED</td>
<td>The operation was aborted due to a conflict, such as publishing a place that is not part of the universe.</td>
</tr>
<tr>
<td>429</td>
<td>RESOURCE_EXHAUSTED</td>
<td>You don't have enough quota to perform the operation, typically due to sending too many requests.</td>
</tr>
<tr>
<td>499</td>
<td>CANCELLED</td>
<td>The system terminates the request, typically due to a client side timeout.</td>
</tr>
<tr>
<td>500</td>
<td>INTERNAL</td>
<td>Internal server error. Typically a server bug.</td>
</tr>
<tr>
<td>501</td>
<td>NOT_IMPLEMENTED</td>
<td>The server doesn't implement the API method.</td>
</tr>
<tr>
<td>503</td>
<td>UNAVAILABLE</td>
<td>Service unavailable. Typically the server is down.</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,32 @@
---
title: OpenAPI document
description: Provides a link to a OpenAPI document for the Roblox Cloud APIs that is compliant to the OpenAPI specification.
---
Roblox publishes an [OpenAPI 3.0.4 document](https://spec.openapis.org/oas/v3.0.4) (formerly known as a Swagger file) that contains **all** of the Roblox Cloud APIs. You can find this file, `openapi.json`, in the open source [creator-docs](https://github.com/Roblox/creator-docs/tree/main/content/en-us/reference/cloud) repository.
You can use OpenAPI documents with tools like the [Swagger Editor](https://editor.swagger.io/) and [Postman](https://www.postman.com/) to test calls and help streamline development. Tools like [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) can also generate client libraries for your preferred programming languages.
<Alert severity="warning">
This document is still under active development and may contain issues. Report a bug if you notice inaccurate information.
</Alert>
<a href="https://github.com/Roblox/creator-docs/blob/main/content/en-us/reference/cloud/">
<Button variant="contained">Download openapi.json here</Button>
</a><br />
### Vendor extensions
Roblox uses [specification extensions](https://spec.openapis.org/oas/v3.0.4#specification-extensions) to extend the OpenAPI specification. These extensions represent information that the specification doesn't capture by default. Extensions may appear throughout the file where allowed by the OpenAPI specification.
The majority of these specification extensions are experimental. The extensions used are subject to change without notice in favor of fields found within the official OpenAPI specification.
| Extension | Location | Description |
| ----------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `x-roblox-extensions-version` | OpenAPI object | A version string, using semantic versioning, that represents the versions of the extensions specified here and other common structures |
| `x-roblox-stability` | Operation object | A string that represents an operation's stability level. |
| `x-roblox-deprecated` | Operation object | An object that provides additional information related to an endpoint's deprecation status. |
| `x-roblox-alternatives` | Operation object | An object that provides additional information related to an endpoint's alternatives. |
| `x-roblox-rate-limits` | Operation object | An object that provides rate limiting information for an endpoint. |
| `x-roblox-scopes` | Operation object | An object that provides the list of required, conditional, and optional scopes that are associated with an endpoint. |
| `x-roblox-engine-usability` | Operation object | An object indicating the endpoint's usability with the Roblox engine. |

View file

@ -0,0 +1,234 @@
---
title: Patterns
description: Explains considerations and guidelines for making proper requests to Open Cloud endpoints and interpreting responses.
---
This page covers common patterns with the Open Cloud APIs, particularly around making requests and handling responses.
## Paths
To make a request to the Open Cloud APIs, you must first form a URL. This URL is a combination of the base URL (e.g. `https://apis.roblox.com`), the Open Cloud API path (for example, `/cloud/v2/universes/{universe_id}/places/{place_id}/user-restrictions`), and any query parameters (for example, `?maxPageSize=25`). A full request URL might look like this:
```json
https://apis.roblox.com/cloud/v2/users/4687549151/inventory-items?maxPageSize=100
```
Many paths, including the example above, have **path parameters**, designated by curly brackets in the API reference. Path parameters are just variables that you insert before making the request and are almost always IDs: user IDs, group IDs, place IDs, etc. IDs are often numeric, but not necessarily; for example, data store and memory store IDs support a wider character set.
## Content length and type
Many API calls, particularly those that create or update, require a JSON request body. If your request has a body, be sure to include the `Content-Length` and `Content-Type` headers. Most HTTP clients add these headers automatically.
## Pagination
If you specify `maxPageSize` in your request, some methods return paginated
results—essentially partial responses:
```json
GET /cloud/v2/users/{user_id}/inventory-items?maxPageSize=25
{
"inventoryItems": [
...
],
"nextPageToken": "aaaBBB"
}
```
```json
GET /cloud/v2/universes/{universe_id}/data-stores?maxPageSize=25
{
"dataStores": [
...
],
"nextPageToken": "datastore1"
}
```
If a response includes a value for `nextPageToken`, use that value in the
`pageToken` parameter of the subsequent request to retrieve the next page. When
`nextPageToken` is empty or omitted entirely, you've reached the end of your results:
```json
GET /cloud/v2/users/{user_id}/inventory-items?maxPageSize=25&pageToken=aaaBBB
{
"inventoryItems": [
...
],
"nextPageToken": ""
}
```
```json
GET /cloud/v2/universes/{universe_id}/data-stores?maxPageSize=25&pageToken=datastore1
{
"dataStores": [
...
]
}
```
Aside from the `pageToken`, you must use the same query for pagination to work
properly. Altering any filter parameter results in a 400 error.
## Open Cloud v2
### Multiple paths
Some resources have multiple path patterns, visible under the **Resource Paths** header in the API reference. For example, the URL for [List User Restrictions](/cloud/reference/UserRestriction#Cloud_ListUserRestrictions__Using_Universes) can be either of the following:
- `https://apis.roblox.com/cloud/cloud/v2/universes/{universe_id}/user-restrictions`
- `https://apis.roblox.com/cloud/cloud/v2/universes/{universe_id}/places/{place_id}/user-restrictions`
You can probably infer the difference between the two: some user restrictions apply to an entire universe (experience), whereas others apply to specific places within a universe. Aside from the small addition to the path and extra path parameter, the calls are identical.
Many endpoints return a path as part of their response, which you can use to make further requests. If an API needs more than a few seconds to fulfill a request, it often returns an [operation](#long-running-operations) rather than the resource or response itself.
### Long running operations
Some methods return an `Operation` object that represents a long-running request
that returns an asynchronous response later. The object contains the following
fields:
- **path** - The endpoint path to call to poll for the request's completion.
Append the path to the original base URL of the resource method.
- **done** - A boolean value that represents whether or not the operation has completed.
- **response** - The response object. This field is empty until the `done` field has a value of `true`.
- **metadata** - Custom metadata specific to the request being made.
```json title="Example Operation Object"
{
"path": "v1/assets/12345/operation/xyz",
"done": true,
"response": {
"value1": "myValue",
"value2": 1234
},
"metadata": {
"metadata1": "string",
"metadata2": 5678
}
}
```
Use the `Operation` object's path to poll for when the resource is ready. A good strategy is to use exponential backoff. For example, you might poll immediately, then after one second, two seconds, four seconds, etc.
```python
def PollForResults(operationPath):
currentRetries = 0
maxRetries = 10
retryPollingDelay = 1
retryPollingMultiplier = 2
while (currentRetries < maxRetries):
# No delay on the first check
if (currentRetries == 0):
results = GetOperation(operationPath)
# Retry logic for subsequent checks
else:
time.sleep(retryPollingDelay)
results = GetOperation(operationPath)
# Exponential backoff
retryPollingDelay *= retryPollingMultiplier
# Check for results and return if they exist
if (results.status_code != 200 or results.json()[doneJSONKey]):
return results
# Otherwise, increment the retry count
else:
currentRetries += 1
```
For a more complete code sample that uses a fixed retry interval rather than exponential backoff, see [Poll for results](../guides/instance.md#poll-for-results).
### Filtering
Some methods let you filter the response by including a `filter` parameter in
the request. The following sections describe filter syntax and guidelines for
the specified endpoints.
#### List group memberships
The wildcard character `-` can be used in place of group ID in order to list
memberships across all groups: `groups/-/memberships`.
Filtering conforms to Common Expression Language (CEL). Only two operators
are supported:
- `==`
- `in [...]`
If you specify a group ID in the resource path, you can filter memberships by
either user or role in the following formats:
- **User filter**: `filter=user == 'users/9876543210'`
- **Role filter**: `filter=role == 'groups/123/roles/7920705'`
If you specify the wildcard character for the group ID, you must filter
memberships by user (up to 50) in the following format:
- **User filter**: `filter=user in ['users/1', 'users/156', 'users/9876543210', ...]`
#### List inventory items
You can filter by collectible status, inventory item type, and inventory
item ID. If you don't provide a filter, the user's entire inventory is returned.
The filter format is a semicolon-separated list:
`filter={field}={value};{field}={value},{value},...;{field}=...`
- `{field}` can be any of the predefined type fields or ID fields in the
following tables.
- `{value}` can be a string, boolean, or enum. Depending on the field type,
this can be a single value (boolean fields) or multiple values (list fields).
<Alert severity="info">
You can't combine type and ID fields in the same filter.
</Alert>
**Type fields**
| Filter | Type | Description |
| :------------------------ | :------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `badges` | boolean | Include badges in the response. Default is false. |
| `gamePasses` | boolean | Include game passes in the response. Default is false. |
| `inventoryItemAssetTypes` | See [assetDetails][1] for the full list of asset types. | Comma-separated list of asset types to include. Default is none. Specify `*` for all asset types. Must have Inventory read scope to filter by purchased places. |
| `onlyCollectibles` | boolean | Include **only** collectibles in the response. Default is false. This field must be used with the `inventoryItemAssetTypes` field in order to return items and only returns non-UGC limited items. |
| `privateServers` | boolean | Include private servers in the response. Default is false. Must have Inventory read scope to filter by this field. |
**ID Fields**
| Filter | Type | Description |
| :----------------- | :----- | :--------------------------------------------------------------------------------------------------------------------- |
| `assetIds` | string | Comma-separated list of numeric asset IDs to include. |
| `badgeIds` | string | Comma-separated list of numeric badge IDs to include. |
| `gamePassIds` | string | Comma-separated list of numeric game pass IDs to include. |
| `privateServerIds` | string | Comma-separated list of numeric private server IDs to include. Must have Inventory read scope to filter by this field. |
**Examples**
- Returns all collectible items that the user owns:
`filter=onlyCollectibles=true;inventoryItemAssetTypes=*`
- Returns all items of the specified types:
`filter=inventoryItemAssetTypes=HAT,CLASSIC_PANTS,TSHIRT_ACCESSORY`
- Returns all items of the listed types or any game passes. Excludes badges from
the results (the same behavior as if `badges` wasn't included in the filter):
`filter=inventoryItemAssetTypes=HAT,CLASSIC_PANTS,TSHIRT_ACCESSORY;gamePasses=true;badges=false`
- Returns assets that match the specified IDs:
`filter=assetIds=1,2,3,4`
- Returns assets, badges, game passes, and private servers that match the
specified IDs:
`filter=assetIds=1,2,3,4;badgeIds=1,2,3,4;gamePassIds=1,2,3,4;privateServerIds=1,2,3,4`
[1]: /cloud/reference/InventoryItem

View file

@ -0,0 +1,63 @@
---
title: Rate limits
description: Explains rate limiting on Open Cloud endpoints.
---
Rate limits control the number of requests you can make to endpoints within a specific time window. These limits help ensure service stability and protect Roblox resources from abuse.
## Discovery
There are two ways to determine rate limits: the reference documentation and response headers.
### Documentation pages
Public rate limits can be found under the **Limits** section for each endpoint on their documentation pages. View these sections to understand the rate limits that apply before making requests.
<Alert severity="info">
We strive to be as transparent as possible about rate limits, but additional, undocumented limits may apply, including for DDoS protection and service stability. Always ensure your application handles HTTP 429 rate limit responses. See [Handling rate limits](#handling-rate-limits) for guidance.
If you hit an undocumented rate limit that blocks your use case (or you would like higher limits for an endpoint), please leave feedback explaining your needs in the [Developer Forum](https://devforum.roblox.com/).
</Alert>
### Response headers
Many API responses include headers that provide information about your current rate limit status.
| Header | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `x-ratelimit-limit` | The total number of requests allowed within the rate limit window. This may include multiple limit values for different time windows. |
| `x-ratelimit-remaining` | The number of requests you can still make with your current authentication. When this reaches 0, you receive HTTP 429 responses. |
| `x-ratelimit-reset` | The number of seconds remaining until your rate limit quota resets to the full limit. |
Here's an example of rate limit headers in an API response:
```text
x-ratelimit-limit: 1000, 1000;w=60, 1000;w=60
x-ratelimit-remaining: 998
x-ratelimit-reset: 20
```
In this example:
- You have 998 requests remaining out of your total limit.
- Your quota will reset in 20 seconds.
## Behavior
Rate limit behavior differs by authentication method and request origin, including whether you make requests from within a Roblox experience.
### API key
Rate limits for API keys are applied across all API keys per owner, which can be either a user or a group. These limits are enforced consistently regardless of where the requests originate—whether from `Class.HttpService`, web applications, or other sources.
#### In-experience with HttpService
When calling endpoints in-experience using `Class.HttpService`, requests also contribute to the fixed limit of 500 HTTP requests per minute per Roblox game server. See [In-experience HTTP requests](/cloud-services/http-service) for more details.
### OAuth 2.0
OAuth 2.0 rate limits are applied per access token. Since each access token represents a unique combination of user and application, every user using your app receives their own independent rate limit quota.
## Handling rate limits
Write your code to expect and handle HTTP 429 (Too Many Requests) responses due to documented limits, abuse prevention, or high-load scenarios. When rate limited, check the `retry-after` response header as a guideline for when to retry. If there is no `retry-after` response header present, implement an exponential backoff retry strategy.

View file

@ -0,0 +1,65 @@
---
title: Risk levels
description: Explains the risk level classifications for Open Cloud API endpoints and their behavior in the "try it out" feature.
---
Risk levels classify Open Cloud endpoints based on the potential impact of their operations. These classifications apply to the OAuth 2.0 authorization flow and the "try it out" feature here in the API documentation.
Risk levels help protect your data and account when testing API endpoints in the documentation. Always review an endpoint's functionality and potential impact before testing, particularly for medium and high risk operations that can modify your data.
## Try it out risk levels
The "try it out" feature uses four risk levels, which control the warnings and restrictions that you encounter when testing endpoints directly from the documentation.
<table>
<thead>
<tr>
<th>Risk level</th>
<th>"Try it out" allowed</th>
<th>Warning dialog</th>
<th>Warning banner</th>
<th>Cookie attachment</th>
</tr>
</thead>
<tbody>
<tr>
<td>Low</td>
<td>&#9989;</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Medium</td>
<td>&#9989;</td>
<td>Yes (per endpoint, per session)</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>High</td>
<td>&#9989;</td>
<td>Yes (per endpoint, per session)</td>
<td>Yes</td>
<td>Yes (after accepting second warning dialog)</td>
</tr>
<tr>
<td>Critical</td>
<td>&#10060;</td>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
</tr>
</tbody>
</table>
- **Low risk** endpoints primarily retrieve publicly available information and have minimal impact on your data or experiences. These endpoints show a simple informational banner when using "try it out."
- **Medium risk** endpoints can create, update, or access private information. Most changes you make from these endpoints can be reversed using other available endpoints. These endpoints have the following "try it out" safety measures:
- They display a confirmation dialog before you send a request. This dialog appears once per endpoint per browser session.
- They show a warning banner to remind you of the risk level.
- **High risk** endpoints can modify or delete private information in ways that may be difficult or impossible to reverse. This category includes most DELETE operations and other potentially destructive actions. In addition to the confirmation dialog and warning banner from medium risk endpoints, a second confirmation dialog is required to send the first request (enforced only per session, not per endpoint).
- **Critical risk** endpoints access or modify highly sensitive data that could compromise account security or privacy. "Try it out" is disabled for these endpoints.

View file

@ -0,0 +1,10 @@
---
title: Scopes
description: List of Roblox Cloud scopes and their corresponding endpoints.
---
This page lists all Open Cloud scopes and the API endpoints that they support. It also lists the target types that are supported for each scope.
import openapi from '../../reference/cloud/openapi.json'
<ScopeTable spec={openapi}/>

View file

@ -0,0 +1,119 @@
---
title: Types
description: Explains the different data types of Open Cloud web APIs.
---
Open Cloud represents request and response payloads as [standard
JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/).
The standards JSON types are text, values, objects, arrays, numbers, and
strings. Some types have special considerations for representing specific kinds
of data, which are described in the following sections.
## Timestamp
Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3,
6 or 9 fractional digits. Offsets other than "Z" are also accepted.
```json title="Example Timestamp"
{ "timestamp": "1972-01-01T10:00:20.021Z" }
```
## Duration
Generated output always contains 0, 3, 6, or 9 fractional digits, depending on
required precision, followed by the suffix "s". Accepted are any fractional
digits (also none) as long as they fit into nano-seconds precision and the
suffix "s" is required. The range must be from -315,576,000,000 to
315,576,000,000 seconds, inclusive.
```json title="Example Durations"
{
"duration-9": "1.000340012s",
"duration-0": "1s"
}
```
## Bytes
Byte data are encoded as a string using standard base64 encoding with paddings.
Either standard or URL-safe base64 encoding with or without paddings are
supported.
```json title="Example Bytes"
{
"bytes": "YWJjMTIzIT8kKiYoKSctRbLx+"
}
```
## FieldMask
A FieldMask is a string that describes the fields to act on when making a
request. To construct a field mask, you specify comma delimited JSON field names
in a string. For example, given the following:
```json title="Example JSON"
{
"foo": {
"a": "c",
"b": "d"
},
"bar": "x",
"baz": "y"
}
```
If you wanted to specify a field mask to update the values of only `foo.b` and
`bar`, the field mask would look like: `foo.b, bar`
In Open Cloud, update methods that support a field mask have a parameter named
`updateMask`, where you can specify a field mask as a value.
## Money
Generally used to define a price, the `Money` type has a three-letter currency code (as defined in ISO 4217) and a quantity, which uses the `Decimal` type. For example, you might represent a $17.99 price like this:
```json
"myPrice": {
"currencyCode": "USD",
"quantity": {
"significand": 1799,
"exponent": -2
}
}
```
Creator Store products require an `exponent` of -9, so the same price looks like this:
```json
"myPrice": {
"currencyCode": "USD",
"quantity": {
"significand": 17990000000,
"exponent": -9
}
}
```
## Decimal
Represents a decimal number in a form similar to scientific notation, with significant digits and an exponent.
Examples:
- 17
`{"significand": 17, "exponent": 0}` or just `{"significand": 17}`
- -0.005
`{"significand": -5, "exponent": -3}`
- 33.5 million (33,500,000)
`{"significand": 335, "exponent": 5}`
- 11/8 (1.375)
`{"significand": 1375, "exponent": -3}`
When `exponent` is greater than 0, it represents the number of trailing zeroes after the significant digits. When `exponent` is less than 0, it represents how many of the significant digits come after the decimal point. When `exponent` is 0, the value of the `Decimal` is the value of the `significand`.

View file

@ -0,0 +1,538 @@
---
title: Automate right to erasure requests
description: Explains how to automate Right to Erasure requests with webhooks and Open Cloud APIs for data stores.
---
The **General Data Protection Regulation (GDPR)** is a European regulation on data protection and privacy. It grants individuals the right to request the deletion of their personal data, known as the [right to erasure](https://gdpr-info.eu/art-17-gdpr/). If you store any **Personally Identifiable Information (PII)** of your users, such as their User IDs, you must comply with GDPR requirements by deleting this information upon receiving a user's request.
Instead of handling requests manually, you can [set up a webhook](../../cloud/webhooks/webhook-notifications.md) and use a bot within a third-party messaging application to automate the process. As [data stores](../../cloud-services/data-stores/index.md) being the most common way for storing PII data, this tutorial provides an example on how to create a bot within Guilded or Discord that uses the [Open Cloud API for data stores](../../cloud/guides/data-stores/index.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.
1. Roblox webhook is triggered, containing the User ID and a list of Start Place IDs for the experiences they have joined in the payload.
1. Your bot listens for these webhook notifications, verifies their authenticity, and utilizes the [Open Cloud API for data stores](../../cloud/guides/data-stores/index.md) to delete the PII data stored in data stores.
<Alert severity="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.
</Alert>
1. The bot responds to the webhook message in Discord or Guilded with the deletion status.
<img src="../../assets/misc/Webhooks-Bot-Workflow.png" width="100%" />
## 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 Guilded or Discord.
<Tabs>
<TabItem label="Guilded">
1. Create a new Guilded server. If you are unfamiliar with the process, see [Guilded Support](https://support.guilded.gg/hc/en-us/articles/1500002751582-Create-a-Server).
1. Under the **Privacy** settings, set the server to private. The server automatically creates a private **#general** channel as your default channel.
1. Create a webhook integration with the new server and give it a name that you can easily understand, such as `GDPR Hook`. If you are unfamiliar with the process, see [Guilded Support](https://support.guilded.gg/hc/en-us/articles/360038927934-Incoming-Webhooks).
1. 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.
</TabItem>
<TabItem label="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-).
<Alert severity="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.
</Alert>
1. The server automatically creates a **#general** channel as your default channel. Click the **Edit Channel** icon of the **#general** channel.
1. Under **Permissions**, set the channel to private.
1. Create a webhook integration with the new server, name it to `GDPR Hook`. If you are unfamiliar with the process, see [Discord Support](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks).
1. 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.
</TabItem>
</Tabs>
### Configure a webhook on Roblox
After obtaining the third-party server URL, use it to [configure a webhook](../../cloud/webhooks/webhook-notifications.md#configure-webhooks-on-creator-dashboard) on Creator Dashboard. make sure you perform the following settings:
<Alert severity="info">
Currently, only group owners can receive Right to Erasure requests for group-owned experiences. To implement the automation solution for a group-owned experience, make sure that the group owner configures the webhook.
</Alert>
- Add the Guilded or 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](../../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.
<img src="../../assets/misc/Webhooks-Sample-Notification.png" width="50%" alt="Example notification on Guilded"/>
## Configure a bot
After you add the webhook, use it to configure the bot with the following steps:
<Tabs>
<TabItem label="Guilded">
1. Open the **All servers** list by clicking its icon or use the shortcut:
- <kbd>Ctrl</kbd><kbd>S</kbd> on Windows.
- <kbd>⌘</kbd><kbd>S</kbd> on Mac.
1. Select your server for receiving right to erasure notifications.
1. Expand the list under **Server home** and select **Manage Bots**.
1. The server automatically creates a private **#general** channel as your default channel.
1. Click the **Create a bot** button and add a bot name. Guilded redirects you to the bot configuration page.
1. Select the **API** section on the bot configuration page.
1. Under the **Tokens** section, click the **Generate Token** button.
1. Save and store the generated token in a safe place.
</TabItem>
<TabItem label="Discord">
Check out Discord's [documentation](https://discord.com/developers/docs/topics/oauth2#bot-vs-user-accounts) for latest instructions.
1. Navigate to the [Applications page](https://discord.com/developers/applications).
1. Create a new application and name it to `GDPR Bot`.
1. The system redirects you to the **General Information** settings of the bot. Copy and save its application ID in a secure place.
1. Under the settings menu, select **OAuth2**.
1. 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.
1. Copy the generated URL. Leave the application settings page unclosed.
1. Navigate to the generated URL. Select the target server. Click the **Continue** button and then the **Authorize** button.
1. Navigate back to application settings page and navigate to the Bot settings.
1. Under the **Privileged Gateway Intents** section, enable **Message Content Intent**.
1. 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.
</TabItem>
</Tabs>
## 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](../auth/api-keys.md) that can access your experiences 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 experiences and places
For the bot to locate the PII data requested by users for deletion, obtain the following identifiers of all experiences that you intend to use the bot for:
- The **Universe ID**, the unique identifier of your experience.
- The **Start Place ID**, the unique identifier of the start place of an experience.
To obtain these identifiers:
1. Navigate to the [Creator Dashboard](https://create.roblox.com/dashboard/creations).
2. Hover over an experience's thumbnail, click the **&ctdot;** button, and select **Copy Universe ID** and **Copy Start Place ID**, respectively.
<img src="../../assets/creator-dashboard/Options-Button-Experience-Public.png" width="200" />
## 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:
<Tabs>
<TabItem label="Guilded">
```bash title="Install Libraries"
pip3 install guilded.py==1.8.0
pip3 install requests
pip3 install urllib3==1.26.6
```
</TabItem>
<TabItem label="Discord">
```bash title="Install Libraries"
pip3 install discord
pip3 install requests
pip3 install urllib3==1.26.6
```
</TabItem>
</Tabs>
1. Copy and save the following scripts corresponding to different parts of the bot logic in the same directory:
```python title="bot_config.py"
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 title="data_stores_api.py"
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 title="message_parser.py"
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 or Guilded. 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 "", []
```
<Tabs>
<TabItem label="Guilded">
```python title="guilded_bot.py"
import guilded
import json
import bot_config
import data_stores_api
import message_parser
def run():
client = guilded.Client()
@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()
```
</TabItem>
<TabItem label="Discord">
```python title="discord_bot.py"
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()
```
</TabItem>
</Tabs>
1. 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.
1. Execute the following command to run the bot:
<Tabs>
<TabItem label="Guilded">
```bash title="Run Guilded Bot"
python3 guilded_bot.py
```
</TabItem>
<TabItem label="Discord">
```bash title="Run Discord Bot"
python3 discord_bot.py
```
</TabItem>
</Tabs>
1. 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.
<Alert severity="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](../../cloud/webhooks/webhook-notifications.md#delivery-failure-retry-policy).
</Alert>
## 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 Guilded or Discord webhook server with the following request body:
```bash title="Example Request"
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}"
}
}]
}'
```
1. 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`.
1. Put together the `description` in the following format:
```plain title="Description Field Format"
{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 title="Example Description Field"
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 title="Example Body"
{
"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"
}
}
]
}
```

View file

@ -0,0 +1,269 @@
---
title: Webhook notifications
description: Explains how to set up webhooks to automate your notification management workflow.
---
Instead of manually monitoring all events in your experience and requests from users, you can set up webhooks to receive real-time notifications on a third-party messaging tool or your custom endpoint that can receive HTTP requests. This helps you automate your notification management workflow to reduce manual effort handling notifications.
<Alert severity="info">
Currently, Roblox fully supports webhook notifications for Discord, Guilded, and Slack. Using webhooks with other third-party tools carries the risk of not receiving all notifications.
</Alert>
## Webhook workflow
Webhooks send real-time notifications or data between two different applications or services, such as Roblox and a third-party messaging tool. Unlike traditional APIs, which require you to set up a client application to send requests to a server to receive data, webhooks send data to your client endpoint as soon as an event occurs. They're useful for automating workflows between Roblox and third-party applications that you use for collaborating with your team, as they allow for real-time data sharing and processing.
Once you set up a webhook, whenever a target event occurs, Roblox sends a request to the webhook URL you provide. The webhook URL then redirects the request to your receiving application or custom endpoint, which can take action based on the data included in the webhook payload. This could include erasing data for GDPR, sending a confirmation to the user, or triggering another event.
## Supported triggers
Roblox currently supports the following event triggers.
### Subscription
- **Subscription Resubscribed** - When a user resubscribes to a subscription, a message is sent containing the subscription and subscriber.
- **Subscription Renewed** - When a user renews a subscription, a message is sent containing the subscription and subscriber.
- **Subscription Refunded** - When a user receives a refund for their subscription, a message is sent containing the subscription and subscriber.
- **Subscription Purchased** - When a user purchases a subscription, a message is sent containing the subscription and subscriber.
- **Subscription Cancelled** - When a user cancels a [subscription](../../production/monetization/subscriptions.md), a message is sent containing the subscription and subscriber, as well as the reason given for the cancellation.
For more information on subscription events and their fields, see the [Subscription](/cloud/reference/Subscription/) reference.
### Compliance
- **Right to Erasure Request** - When a user submits a data deletion request under the [General Data Protection Regulation](https://gdpr.eu/right-to-be-forgotten/) (GDPR).
### Commerce
- **Commerce Product Order Refunded** - When a user has received a refund for their commerce product order, or the order was cancelled.
- **Commerce Product Order Paid** - When a user has paid for their commerce product order. Please note that duplicate webhook events are possible, so you should deduplicate events using the unique commerce order ID.
## Configure webhooks on Creator Dashboard
To receive notifications through webhooks, you need to configure a webhook that subscribes to certain events for triggering notifications. For group-owned experiences, only group owners can configure and receive webhook notifications.
<Alert severity="info">
If you're setting up webhooks and handling personal data, ensure they comply with the [General Data Protection Regulation (GDPR)](https://gdpr-info.eu/).
</Alert>
To set up a webhook:
1. On the [Creator Dashboard](https://create.roblox.com/dashboard/creations), access the [Webhooks](https://create.roblox.com/settings/webhooks) page.
1. Click the **Add Webhook** button.
1. Complete the configuration fields:
1. **Webhook URL** - Specify the URL where you want to receive notifications. For more information on the requirements, see [Set up webhook URLs](#set-up-webhook-urls).
2. **Name** - Use a custom name to differentiate your configuration from others. By default the value is the same as the Webhook URL.
3. **Secret** (optional) - Supply a secret if you want to verify that notifications you receive are coming from Roblox. For more information, see [Verify webhook security](#verify-webhook-security).
4. **Triggers** - Choose one or more options from the list of [supported triggers](#supported-triggers) of events for which you want to receive notifications.
1. Click the **Save Changes** button.
<Alert severity="info">
Currently, you can configure up to 5 webhooks in total.
</Alert>
## Set up webhook URLs
You can set up a custom HTTP service endpoint as your webhook URL, provided it fulfills the following requirements:
- It must be publicly accessible for handling requests.
- It can handle POST requests.
- It can respond to the request with a 2XX response within 5 seconds.
- It can handle HTTPS requests.
When your endpoint receives a POST request, it must be able to:
- Extract the details required about the notification from the body of the POST message.
- Read the body of the POST message with the generic details on the notification and specific details related to the event type on the notification.
For more information of the schema of POST requests to handle, see the [Payload Schema](#payload-schema).
### Delivery failure retry policy
When a webhook notification fails to reach your specified URL due to errors such as endpoint unavailability, Roblox retries sending the message to the configured URL 5 times using a fixed window size. If the notification still fails to be delivered after 5 attempts, Roblox stops trying to send the notification and assumes that the URL is no longer valid. In this situation, you need to update your webhook configuration with a new URL that is reachable and able to receive notifications. To troubleshoot and confirm that your webhook URL can successfully receive notifications, see [Test webhooks](#test-webhooks).
### Third-party requirements
Third-party tools usually have their own requirements for webhooks that you need to follow when setting up your webhook URL. You can find these requirements by searching for the keyword "webhook" on the support or documentation site of the target tool. For the three supported third-party tools, see the following:
- [Discord Help Center](https://support.discord.com/hc/en-us)
- [Guilded Support](https://support.guilded.gg/hc/en-us)
- [Slack API Reference](https://api.slack.com/)
## Test webhooks
You can test whether the webhook you've configured can successfully receive notifications on the [Creator Dashboard](https://create.roblox.com/dashboard/creations):
1. Navigate to the [Webhooks](https://create.roblox.com/settings/webhooks) configuration page.
2. Select the webhook you want to test from the list of configured webhooks.
3. Click the pencil icon next to the target webhook.
4. Click the **Test Response** button.
The system then sends a `SampleNotification` event, which includes the **User ID** of the user who triggered the notification, as shown here:
```json title="SampleNotification schema"
{
"NotificationId": "string",
"EventType": "SampleNotification",
"EventTime": "2023-12-30T16:24:24.2118874Z",
"EventPayload": {
"UserId": 1
}
}
```
If you are integrating your webhook with a third-party service, you can test it using the third-party URL to confirm that the service can successfully receive notifications from your webhook. If you provide a secret when configuring the webhook, it also generates a `roblox-signature` that you can use to test the `roblox-signature` logic.
## Verify webhook security
After you configure your server to receive payloads, it starts to listen for any payload sent to the endpoint. If you set a secret when configuring your webhook, Roblox sends a `roblox-signature` in each webhook notification to ensure that the request actually came from Roblox. The signature is in the payload header for custom endpoints and in the footer for third-party servers.
```csv title="Signature format with a secret for custom endpoints"
t=<timestamp>,v1=<signature>
```
If you did not set a secret for your webhook, the signature only contains the timestamp of when the notification was sent:
```csv title="Signature format without a secret for custom endpoints"
t=<timestamp>
```
To verify a signature:
<Tabs>
<TabItem label="Custom endpoints">
1. Extract the timestamp and signature values. All signatures for webhooks with secrets share the same format as a CSV string with these two values following by the prefixes:
- `t`: The timestamp of when the notification was sent.
- `v1`: The signature value generated using the secret provided by the Creator Dashboard configuration.
1. Re-create the base string of `roblox-signature` by concatenating:
1. The timestamp as a string.
1. The period character `.`.
1. The JSON string of the request body.
1. Compute a hash-based message authentication code (HMAC) with the SHA256 hash function using the secret you defined during the configuration as the key and the base string you generated through step 2 as the message. Convert the result to Base64 format to get the expected signature.
1. Compare the extracted signature value to the expected signature. If you generated the signature correctly, the value should be the same.
1. (Optional) To prevent replay attacks, a type of cyber attack where attackers intercept and resend data to gain unauthorized access or perform malicious actions, it's helpful to compare the extracted timestamp value with the current timestamp and ensure it falls within a reasonable time limit. For example, a 10-minute window is usually a good reasonable time limit.
</TabItem>
<TabItem label="Third-party servers">
1. Extract the timestamp value from the `Timestamp` footer of the notification.
1. Extract the message body from the notification. For the following example notification, the message body is:
<figcaption>
You have received a new notification for Right to Erasure for the User Id: XXXXXXXX in the game(s) with Ids: YYYYYYY, ZZZZZZZZZ
</figcaption>
<img src="../../assets/misc/Webhooks-Example-Message-Body.png" width="50%" alt="Example message body on Guilded"/>
1. Concatenate the timestamp value and the message with a period character in the format as:
<figcaption>
168487229.You have received a new notification for Right to Erasure for the User Id: XXXXXXXX in the game(s) with Ids: YYYYYYY, ZZZZZZZZZ
</figcaption>
1. Compute a Hash-based message authentication code (HMAC) with the SHA256 hash function using the secret you defined during the configuration as the key and the base string you generated through step 2 as the message. Convert the result to Base64 format to get the expected signature.
1. Compare the extracted signature value to the expected signature. If you generated the signature correctly, the value should be the same.
1. **(Optional)** To prevent replay attacks, a type of cyber attack where attackers intercept and resend data to gain unauthorized access or perform malicious actions, it's helpful to compare the extracted timestamp value with the current timestamp and ensure it falls within a reasonable time limit. For example, a 10-minute window is usually a good reasonable time limit.
</TabItem>
</Tabs>
## Payload schema
When the target event of your webhook is triggered, it sends a request to your webhook URL, including information about the event in the payload. All payloads of requests share the same schema that consists of fixed and variable fields. This ensures that the data transmitted in the payload is structured and consistent, making it easier for the receiving application to process and use the data.
The **fixed payload schema fields** can help maintain consistency across all webhook requests, with the following fields available:
1. `NotificationId` (string): A unique identifier for each notification sent. If the same `NotificationId` is received twice, it is considered a duplicate.
2. `EventType` (string): Indicates the type of event for which the notification was triggered.
3. `EventTime` (string): The timestamp of when the event was triggered.
The **variable payload schema fields** provides flexibility for webhooks to accommodate various types of events, which include:
1. `EventPayload` (object): Contains information specific to the `EventType` that triggered the webhook. The structure of the `EventPayload` schema varies based on the type of event.
The following example shows the payload schema of the **Right To Erasure request** event:
```json title="Example schema for a Right to Erasure request"
{
"NotificationId": "string",
"EventType": "RightToErasureRequest",
"EventTime": "2023-12-30T16:24:24.2118874Z",
"EventPayload": {
"UserId": 1,
"GameIds": [
1234, 2345
]
}
}
```
## Handle notifications
If you store any **Personally Identifiable Information (PII)** of your users, such as their User IDs, you must delete this information when a user submits such a request to comply with the GDPR [right to erasure](https://gdpr-info.eu/art-17-gdpr/) compliance requirements. You can create a bot to handle webhook notifications and help automate data deletion, provided you're storing PII in a data store. See [Automating Right to Erasure Requests Deletion](../../cloud/webhooks/automate-right-to-erasure.md) for an example on how to create a bot within Guilded or Discord that uses the [Open Cloud API for data stores](../../cloud/guides/data-stores/index.md) to delete PII data as an automation solution. This example can be adapted for handling other notifications, such as subscription events.
If you use a custom endpoint as your webhook server instead of a third-party tool, you can extract the data subject to deletion from the webhook payload and build your own automation solution. The following code sample is an example of a server that has prevention against replay attacks by verifying the timestamp and that the request is coming from Roblox:
```javascript title="Extracting PII from Payload"
const crypto = require('crypto');
const express = require('express');
const secret = '<your_secret>' // This can be set as an environment variable
let app = express();
app.use(express.json());
app.all('/*', function (req, res) {
console.log('New request recieved');
// Extract the timestamp and signature from header
const signatureHeader = req.headers['roblox-signature'].split(',');
const timestamp = signatureHeader.find(e => e.startsWith('t=')).substring(2);
const signature = signatureHeader.find(e => e.startsWith('v1=')).substring(3);
// Ensure the request came within a 300 second window to prevent replay attacks
const requestTimestampMs = timestamp * 1000;
const windowTimeMs = 300 * 1000;
const oldestTimestampAllowed = Date.now() - windowTimeMs;
if (requestTimestampMs < oldestTimestampAllowed) {
return res.status(403).send('Expired Request');
}
// Validate signature
const message = `${timestamp}.${JSON.stringify(req.body)}`;
const hmac = crypto.createHmac('sha256', secret);
const calculatedSignature = hmac.update(message).digest('base64');
if (signature !== calculatedSignature) {
return res.status(401).send('Unauthorized Request');
}
// Your logic to handle payload
const payloadBody = req.body;
const eventType = payloadBody['EventType'];
if (eventType === 'RightToErasureRequest'){
const userId = payloadBody['EventPayload']['UserId'];
const gameIds = payloadBody['EventPayload']['GameIds'];
console.log(`Payload data: UserId=${userId} and GameIds=${gameIds}`);
// If you store PII in data stores, use the UserId and GameIds to delete the information from data stores.
}
return res.json({ message: 'Processed the message successfully' });
});
app.listen(8080, function () {
console.log('Server started');
});
```