mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
update rules
This commit is contained in:
parent
4c390fdd37
commit
d5522b2fef
8549 changed files with 11783 additions and 1888 deletions
241
.cursor/rules/roblox/en-us/cloud/auth/api-keys.md
Normal file
241
.cursor/rules/roblox/en-us/cloud/auth/api-keys.md
Normal 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"
|
||||
}
|
||||
```
|
||||
206
.cursor/rules/roblox/en-us/cloud/auth/oauth2-develop.md
Normal file
206
.cursor/rules/roblox/en-us/cloud/auth/oauth2-develop.md
Normal 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"
|
||||
}
|
||||
```
|
||||
167
.cursor/rules/roblox/en-us/cloud/auth/oauth2-overview.md
Normal file
167
.cursor/rules/roblox/en-us/cloud/auth/oauth2-overview.md
Normal 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.
|
||||
427
.cursor/rules/roblox/en-us/cloud/auth/oauth2-reference.md
Normal file
427
.cursor/rules/roblox/en-us/cloud/auth/oauth2-reference.md
Normal 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"
|
||||
]
|
||||
}
|
||||
```
|
||||
144
.cursor/rules/roblox/en-us/cloud/auth/oauth2-registration.md
Normal file
144
.cursor/rules/roblox/en-us/cloud/auth/oauth2-registration.md
Normal 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¶m2=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>
|
||||
79
.cursor/rules/roblox/en-us/cloud/auth/oauth2-sample.md
Normal file
79
.cursor/rules/roblox/en-us/cloud/auth/oauth2-sample.md
Normal 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`.
|
||||
339
.cursor/rules/roblox/en-us/cloud/guides/data-stores/index.md
Normal file
339
.cursor/rules/roblox/en-us/cloud/guides/data-stores/index.md
Normal 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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
14
.cursor/rules/roblox/en-us/cloud/guides/index.md
Normal file
14
.cursor/rules/roblox/en-us/cloud/guides/index.md
Normal 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>
|
||||
442
.cursor/rules/roblox/en-us/cloud/guides/instance.md
Normal file
442
.cursor/rules/roblox/en-us/cloud/guides/instance.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
- 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** → **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.
|
||||
159
.cursor/rules/roblox/en-us/cloud/guides/inventory.md
Normal file
159
.cursor/rules/roblox/en-us/cloud/guides/inventory.md
Normal 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
|
||||
74
.cursor/rules/roblox/en-us/cloud/guides/secrets-store.md
Normal file
74
.cursor/rules/roblox/en-us/cloud/guides/secrets-store.md
Normal 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>'
|
||||
```
|
||||
414
.cursor/rules/roblox/en-us/cloud/guides/usage-assets.md
Normal file
414
.cursor/rules/roblox/en-us/cloud/guides/usage-assets.md
Normal 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 **⋯** 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"'
|
||||
|
||||
```
|
||||
149
.cursor/rules/roblox/en-us/cloud/guides/usage-messaging.md
Normal file
149
.cursor/rules/roblox/en-us/cloud/guides/usage-messaging.md
Normal 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 **⋯** 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.
|
||||
|
|
@ -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 **⋯** 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).
|
||||
35
.cursor/rules/roblox/en-us/cloud/index.md
Normal file
35
.cursor/rules/roblox/en-us/cloud/index.md
Normal 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).
|
||||
219
.cursor/rules/roblox/en-us/cloud/reference/errors.md
Normal file
219
.cursor/rules/roblox/en-us/cloud/reference/errors.md
Normal 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>
|
||||
32
.cursor/rules/roblox/en-us/cloud/reference/openapi.md
Normal file
32
.cursor/rules/roblox/en-us/cloud/reference/openapi.md
Normal 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. |
|
||||
234
.cursor/rules/roblox/en-us/cloud/reference/patterns.md
Normal file
234
.cursor/rules/roblox/en-us/cloud/reference/patterns.md
Normal 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
|
||||
63
.cursor/rules/roblox/en-us/cloud/reference/rate-limits.md
Normal file
63
.cursor/rules/roblox/en-us/cloud/reference/rate-limits.md
Normal 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.
|
||||
65
.cursor/rules/roblox/en-us/cloud/reference/risk-levels.md
Normal file
65
.cursor/rules/roblox/en-us/cloud/reference/risk-levels.md
Normal 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>✅</td>
|
||||
<td>No</td>
|
||||
<td>Yes</td>
|
||||
<td>Yes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Medium</td>
|
||||
<td>✅</td>
|
||||
<td>Yes (per endpoint, per session)</td>
|
||||
<td>Yes</td>
|
||||
<td>Yes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>High</td>
|
||||
<td>✅</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>❌</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.
|
||||
10
.cursor/rules/roblox/en-us/cloud/reference/scopes.md
Normal file
10
.cursor/rules/roblox/en-us/cloud/reference/scopes.md
Normal 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}/>
|
||||
119
.cursor/rules/roblox/en-us/cloud/reference/types.md
Normal file
119
.cursor/rules/roblox/en-us/cloud/reference/types.md
Normal 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`.
|
||||
|
|
@ -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 **⋯** 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -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');
|
||||
});
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue