Skip to content

Commit e4d81f7

Browse files
committed
Update webhook-notifications.md
1 parent 6d4ea24 commit e4d81f7

File tree

1 file changed

+81
-80
lines changed

1 file changed

+81
-80
lines changed

content/en-us/cloud/webhooks/webhook-notifications.md

Lines changed: 81 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,25 @@ Once you set up a webhook, whenever a target event occurs, Roblox sends a reques
1919

2020
Roblox currently supports the following event triggers for notifications:
2121

22-
- **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.
23-
- **Subscription Purchased** - When a user purchases a subscription, a message is sent containing the subscription and subscriber.
24-
- **Subscription Refunded** - When a user receives a refund for their subscription, a message is sent containing the subscription and subscriber.
25-
- **Subscription Renewed** - When a user renews a subscription, a message is sent containing the subscription and subscriber.
26-
- **Subscription Resubscribed** - When a user resubscribes to a subscription, a message is sent containing the subscription and subscriber.
27-
- ["Right to be forgotten"](https://gdpr.eu/right-to-be-forgotten/) data deletion requests under the General Data Protection Regulation (**GDPR**).
22+
### Subscriptions
23+
24+
- **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.
25+
- **Subscription Purchased** — When a user purchases a subscription, a message is sent containing the subscription and subscriber.
26+
- **Subscription Refunded** — When a user receives a refund for their subscription, a message is sent containing the subscription and subscriber.
27+
- **Subscription Renewed** — When a user renews a subscription, a message is sent containing the subscription and subscriber.
28+
- **Subscription Resubscribed** — When a user resubscribes to a subscription, a message is sent containing the subscription and subscriber.
2829

2930
For more information on subscription events and their fields, see the [Cloud API Subscription](../../cloud/reference/Subscription/) reference.
3031

32+
### Compliance
33+
34+
- **Right to Erasure Request** — When a user submits a data deletion request under the [General Data Protection Regulation (**GDPR**)](https://gdpr.eu/right-to-be-forgotten/).
35+
36+
### Commerce
37+
38+
- **Commerce Product Order Refunded** — When a user has recieved a refund for their commerce product order.
39+
- **Commerce Product Order Paid** — When a user has paid for their commerce product order.
40+
3141
## Configure webhooks on Creator Dashboard
3242

3343
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.
@@ -41,7 +51,7 @@ To set up a webhook:
4151
1. Navigate to the [Webhooks](https://create.roblox.com/settings/webhooks) section of the Creator Dashboard.
4252
1. Click the **Add Webhook** button.
4353
1. Complete the configuration fields:
44-
1. **Webhook URL** — Specify the URL where you want to receive notifications and accept incoming webhook URLs from third-party entities. For more information on the requirements, see [Set up webhook URLs](#set-up-webhook-urls).
54+
1. **Webhook URL** — Specify the URL where you can receive notifications. For more information on the requirements, see [Set up webhook URLs](#set-up-webhook-urls).
4555
2. **Name** — Use a custom name to differentiate your configuration from others. By default the value is the same as the Webhook URL.
4656
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).
4757
4. **Triggers** — Choose one or more options from the list of [supported triggers](#supported-triggers) of events for which you want to receive notifications.
@@ -88,15 +98,15 @@ You can test whether the webhook you've configured can successfully receive noti
8898
3. Click the pencil icon next to the target webhook.
8999
4. Click the **Test Response** button.
90100

91-
The system then sends a notification in the `SampleNotification` type, which includes the **User ID** of the user who triggers the notification, as the following example schema shows:
101+
A `SampleNotification` event is sent, which includes the **User ID** of the user who triggers the notification, as the following example schema shows:
92102

93103
```json title="SampleNotification Schema"
94-
Body: {
104+
{
95105
"NotificationId": "string",
96106
"EventType": "SampleNotification",
97-
"EventTime": "2023-12-30T16:24:24.2118874Z", // Type: ISO 8601 Timestamp
107+
"EventTime": "2023-12-30T16:24:24.2118874Z",
98108
"EventPayload": {
99-
"UserId": 1 // Type: Long
109+
"UserId": 1
100110
}
101111
}
102112
```
@@ -105,19 +115,19 @@ If you are integrating your webhook with a third-party service, you can test it
105115

106116
## Verify webhook security
107117

108-
Once 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` along with every webhook notification to help protect your data security. This way, you can use the it to verify that the notification is from Roblox and limit your server to only receive requests originating from Roblox. The signature is in the payload header for custom endpoints and in the footer for third-party servers.
118+
Once 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.
109119

110-
```csv title="Signature Format with Secret for Custom Endpoints"
120+
```csv title="Signature Format with a Secret for Custom Endpoints"
111121
112-
"roblox-signature": "t=<timestamp>,v1=<signature>"
122+
t=<timestamp>,v1=<signature>
113123
114124
```
115125

116-
If you don't have a secret for your webhook, the signature you receive only contains the timestamp value on when the notification is sent:
126+
If you did not set a secret for your webhook, the signature will only contain the timestamp of when the notification was sent:
117127

118-
```csv title="Signature Format without Secret for Custom Endpoints"
128+
```csv title="Signature Format without a Secret for Custom Endpoints"
119129
120-
"roblox-signature": "t=<timestamp>"
130+
t=<timestamp>
121131
122132
```
123133

@@ -128,17 +138,16 @@ To verify a signature:
128138

129139
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:
130140

131-
- `t`: The timestamp value on when the notification is sent.
141+
- `t`: The timestamp of when the notification is sent.
132142
- `v1`: The signature value generated using the secret provided by the Creator Dashboard configuration.
133-
You can extract these two values using the `split()` function, which separates the string based on a delimiter, in this case, the `,` character.
134143

135144
1. Re-create the base string of `roblox-signature` by concatenating:
136145

137146
1. The timestamp as a string.
138147
1. The period character `.`.
139148
1. The JSON string of the request body.
140149

141-
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.
150+
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.
142151
1. Compare the extracted signature value to the expected signature. If you generated the signature correctly, the value should be the same.
143152

144153
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.
@@ -178,95 +187,87 @@ When the target event of your webhook is triggered, it sends a request to your w
178187

179188
The **fixed payload schema fields** can help maintain consistency across all webhook requests, with the following fields available:
180189

181-
1. `NotificationId`, `string`: A unique identifier for each notification sent. If the same `NotificationId` is received twice, it is considered a duplicate.
182-
2. `EventType`, `string`: A string represents the type of event for which the notification was triggered.
183-
3. `EventTime`, `timestamp`: An approximate timestamp indicating when the event was triggered.
190+
1. `NotificationId` (string): A unique identifier for each notification sent. If the same `NotificationId` is received twice, it is considered a duplicate.
191+
2. `EventType` (string): Indicates the type of event for which the notification was triggered.
192+
3. `EventTime` (string): The timestamp of when the event was triggered.
184193

185194
The **variable payload schema fields** provides flexibility for webhooks to accommodate various types of events, which include:
186195

187-
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.
196+
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.
188197

189198
The following example shows the payload schema of the **Right To Erasure Request** event:
190199

191200
```json title="Example Schema for Right to Erasure Request"
192-
193-
Body:{
194-
201+
{
195202
"NotificationId": "string",
196-
197203
"EventType": "RightToErasureRequest",
198-
199204
"EventTime": "2023-12-30T16:24:24.2118874Z",
200-
201205
"EventPayload": {
202-
203-
"UserId": 1, // Type: Long
204-
205-
"GameIds": [ // Type: An array of Longs
206-
206+
"UserId": 1,
207+
"GameIds": [
207208
1234, 2345
208-
209209
]
210-
211210
}
212-
213211
}
214212
```
215213

216214
## Handle notifications
217215

218216
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/usage-data-stores.md) to delete PII data as an automation solution. This example can be adapted for handling other notifications, such as subscription events.
219217

220-
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 provides an example solution and adds prevention to replay attacks by verifying that the request is coming from Roblox:
218+
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:
221219

222-
```php title="Extracting PII from Payload"
223-
const crypto = require('crypto')
220+
```javascript title="Extracting PII from Payload"
221+
const crypto = require('crypto');
224222
const express = require('express');
223+
224+
const secret = '<Your secret>' // This can be set as an environment variable
225+
225226
let app = express();
226227
app.use(express.json());
227-
app.use(express.urlencoded({ extended: true }));
228-
// This is a sample only code
228+
229229
app.all('/*', function (req, res) {
230-
console.log('-------- New Request Seen -------');
231-
// 1. Extract the timestamp and signature
232-
const shared_secret = '<Your secret>' // This can be set as an environment variable
233-
const hmac = crypto.createHmac('sha256', shared_secret)
234-
const roblox_signature_header = req.headers['roblox-signature'].split(',')
235-
// 'roblox-signature' is present in all requests:
236-
// Timestamp(t) is present in all requests, however signature value(v1) is not set unless a secret is shared during the webhook configuration.
237-
// Fetch header component at Index 0 -> 't=' and Index 1 -> 'v1='
238-
const timestamp = roblox_signature_header.find(e => e.startsWith('t=')).substring(2);
239-
const extracted_signature = roblox_signature_header.find(e => e.startsWith('v1='));
240-
// 2. Prevent Replay attack: 300 seconds window
241-
const request_timestamp_ms = timestamp * 1000;
242-
const window_time_ms = 300 * 1000
243-
const oldest_timestamp_allowed = Date.now() - window_time_ms;
244-
if (request_timestamp_ms < oldest_timestamp_allowed) {
245-
res.status(403).send('Expired Request')
230+
console.log('New request recieved');
231+
232+
// Extract the timestamp and signature from header
233+
const signatureHeader = req.headers['roblox-signature'].split(',');
234+
const timestamp = signatureHeader.find(e => e.startsWith('t=')).substring(2);
235+
const signature = signatureHeader.find(e => e.startsWith('v1=')).substring(3);
236+
237+
// Ensure the request came within a 300 second window to prevent replay attacks
238+
const requestTimestampMs = timestamp * 1000;
239+
const windowTimeMs = 300 * 1000;
240+
const oldestTimestampAllowed = Date.now() - windowTimeMs;
241+
242+
if (requestTimestampMs < oldestTimestampAllowed) {
243+
return res.status(403).send('Expired Request');
246244
}
247-
// 3. Validate Signature
248-
if (extracted_signature !== undefined) {
249-
const signature_v1 = extracted_signature.substring(3);
250-
const message = `${timestamp}.${JSON.stringify(req.body)}`
251-
const base64_signature = hmac.update(message).digest('base64')
252-
if (signature_v1 !== base64_signature) {
253-
res.status(401).send('Unauthorized Request')
254-
}
245+
246+
// Validate signature
247+
const message = `${timestamp}.${JSON.stringify(req.body)}`;
248+
const hmac = crypto.createHmac('sha256', secret);
249+
const calculatedSignature = hmac.update(message).digest('base64');
250+
251+
if (signature !== calculatedSignature) {
252+
return res.status(401).send('Unauthorized Request');
255253
}
256-
// 4. Your logic to handle payload
257-
const payloadBody = req.body
258-
const eventType = payloadBody['EventType']
254+
255+
// Your logic to handle payload
256+
const payloadBody = req.body;
257+
const eventType = payloadBody['EventType'];
258+
259259
if (eventType === 'RightToErasureRequest'){
260-
const userId = payloadBody['EventPayload']['UserId']
261-
const gameIds = payloadBody['EventPayload']['GameIds']
262-
const gameIdString = gameIds.toString()
263-
console.log(`The payload: UserId=${userId} and GameIds=${gameIdString}`)
264-
// If you store PII in data stores, use the UserId and GameIds to make a data store call to delete the information.
260+
const userId = payloadBody['EventPayload']['UserId'];
261+
const gameIds = payloadBody['EventPayload']['GameIds'];
262+
263+
console.log(`Payload data: UserId=${userId} and GameIds=${gameIds}`);
264+
// If you store PII in data stores, use the UserId and GameIds to delete the information from data stores.
265265
}
266-
// 5. Return Response
267-
res.json({ message: 'Processed the message Successfully' });
268-
})
266+
267+
return res.json({ message: 'Processed the message successfully' });
268+
});
269+
269270
app.listen(8080, function () {
270-
console.log('This is a Sample application')
271-
})
271+
console.log('Server started');
272+
});
272273
```

0 commit comments

Comments
 (0)