Guides
Roblox Discord Webhooks: Complete Setup Guide
A Roblox Discord webhook lets your Roblox game send useful events to a Discord channel.
Developers commonly use these integrations for:
- Purchase alerts
- Admin logs
- Server monitoring
- Player reports
- Moderation events
- Error alerts
- Game analytics
- Staff notifications
However, there is an important detail.
A reliable production Roblox-to-Discord setup should generally use your own web endpoint between Roblox and Discord.
The architecture looks like this:
Roblox
↓
HttpService
↓
Your Relay/API
↓
Discord Webhook
This guide shows you how to build the full system.
For the broader topic first, see:
Freeflow Team•Sep 28, 2026•9 min readHow Roblox Discord Webhooks Work
Discord webhooks accept an HTTP POST request containing JSON.
A simple Discord webhook payload looks like:
{
"content": "Hello from Roblox!"
}
A more detailed Discord message can use an embed.
{
"embeds": [
{
"title": "Player Joined",
"description": "ExamplePlayer joined the game.",
"color": 5763719
}
]
}
Instead of storing the actual Discord webhook inside Roblox, we will send data to our own server.
Step 1: Enable Roblox HTTP Requests
Open Roblox Studio.
Then go to:
Game Settings
→ Security
→ Allow HTTP Requests
Enable the setting and save the experience.
Step 2: Create the Roblox Webhook Module
Create a ModuleScript inside:
ServerScriptService
Name it:
WebhookService
Add:
local HttpService = game:GetService("HttpService")
local WebhookService = {}
local ENDPOINT =
"https://YOUR-DOMAIN.com/api/roblox-webhook"
local API_TOKEN =
"YOUR_SHARED_TOKEN"
function WebhookService.Send(eventName, data)
local payload = {
event = eventName,
data = data,
server = {
placeId = game.PlaceId,
jobId = game.JobId,
privateServerId = game.PrivateServerId
},
timestamp = os.time(),
requestId = HttpService:GenerateGUID(false)
}
local success, response = pcall(function()
return HttpService:RequestAsync({
Url = ENDPOINT,
Method = "POST",
Headers = {
["Content-Type"] =
"application/json",
["Authorization"] =
"Bearer " .. API_TOKEN
},
Body =
HttpService:JSONEncode(payload)
})
end)
if not success then
warn(
"[WebhookService] Request failed:",
response
)
return false
end
if not response.Success then
warn(
"[WebhookService] HTTP error:",
response.StatusCode,
response.StatusMessage
)
return false
end
return true
end
return WebhookService
Step 3: Create a Discord Webhook
Inside your Discord server:
- Open the channel.
- Open channel settings.
- Find Integrations.
- Create a webhook.
- Choose the channel.
- Copy its webhook URL.
Treat the webhook URL like a password.
Anyone who obtains it may be able to send messages through that webhook.
Do not publish it inside:
- LocalScripts
- Public GitHub repositories
- Paste sites
- Tutorials
- Screenshots
Step 4: Create Your Relay
Here is a simple Node.js Express relay.
Install:
npm init -y
npm install express
Create:
server.js
Add:
const express = require("express");
const app = express();
app.use(express.json());
const PORT =
process.env.PORT || 3000;
const DISCORD_WEBHOOK =
process.env.DISCORD_WEBHOOK;
const ROBLOX_TOKEN =
process.env.ROBLOX_TOKEN;
app.post(
"/api/roblox-webhook",
async (req, res) => {
const authorization =
req.headers.authorization;
if (
authorization !==
`Bearer ${ROBLOX_TOKEN}`
) {
return res
.status(401)
.json({
error: "Unauthorized"
});
}
const payload = req.body;
console.log(
"Roblox event:",
payload
);
const discordPayload = {
content:
`Roblox event: ${payload.event}`
};
try {
const response =
await fetch(
DISCORD_WEBHOOK,
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(
discordPayload
)
}
);
if (!response.ok) {
console.error(
"Discord returned:",
response.status
);
return res
.status(502)
.json({
error:
"Discord request failed"
});
}
res.json({
success: true
});
} catch (error) {
console.error(error);
res
.status(500)
.json({
error:
"Webhook relay failed"
});
}
}
);
app.listen(PORT, () => {
console.log(
`Listening on port ${PORT}`
);
});
Set these environment variables:
DISCORD_WEBHOOK=https://discord.com/api/webhooks/...
ROBLOX_TOKEN=YOUR_RANDOM_SECRET
Your Discord URL remains safely on your web server.
Player Join Discord Webhook
Inside Roblox:
local Players =
game:GetService("Players")
local Webhook =
require(
game.ServerScriptService.WebhookService
)
Players.PlayerAdded:Connect(function(player)
Webhook.Send(
"player_join",
{
name = player.Name,
displayName =
player.DisplayName,
userId =
player.UserId
}
)
end)
Now change your Node relay to build a nicer Discord message.
function buildDiscordMessage(payload) {
if (payload.event === "player_join") {
return {
embeds: [
{
title:
"Player Joined",
description:
`${payload.data.name} joined the game.`,
fields: [
{
name:
"Username",
value:
payload.data.name,
inline: true
},
{
name:
"User ID",
value:
String(
payload
.data
.userId
),
inline: true
}
],
timestamp:
new Date()
.toISOString()
}
]
};
}
return {
content:
`Roblox event: ${payload.event}`
};
}
Then change:
const discordPayload = {
content:
`Roblox event: ${payload.event}`
};
to:
const discordPayload =
buildDiscordMessage(payload);
Player Leave Webhook
Roblox:
Players.PlayerRemoving:Connect(
function(player)
Webhook.Send(
"player_leave",
{
name =
player.Name,
userId =
player.UserId
}
)
end
)
Add to Node:
if (payload.event === "player_leave") {
return {
embeds: [
{
title:
"Player Left",
description:
`${payload.data.name} left the server.`,
fields: [
{
name:
"User ID",
value:
String(
payload
.data
.userId
)
}
],
timestamp:
new Date()
.toISOString()
}
]
};
}
Roblox Purchase Discord Webhook
Roblox:
local MarketplaceService =
game:GetService("MarketplaceService")
local Players =
game:GetService("Players")
local Webhook =
require(
game.ServerScriptService.WebhookService
)
MarketplaceService.ProcessReceipt =
function(receiptInfo)
local player =
Players:GetPlayerByUserId(
receiptInfo.PlayerId
)
if not player then
return Enum
.ProductPurchaseDecision
.NotProcessedYet
end
-- Grant purchase here.
-- Your actual implementation
-- should prevent duplicate grants.
Webhook.Send(
"purchase",
{
username =
player.Name,
userId =
player.UserId,
productId =
receiptInfo.ProductId,
purchaseId =
receiptInfo.PurchaseId
}
)
return Enum
.ProductPurchaseDecision
.PurchaseGranted
end
Discord formatter:
if (payload.event === "purchase") {
return {
embeds: [
{
title:
"New Roblox Purchase",
description:
"A player purchased a developer product.",
fields: [
{
name:
"Player",
value:
payload
.data
.username,
inline: true
},
{
name:
"User ID",
value:
String(
payload
.data
.userId
),
inline: true
},
{
name:
"Product ID",
value:
String(
payload
.data
.productId
),
inline: true
},
{
name:
"Purchase ID",
value:
String(
payload
.data
.purchaseId
)
}
],
timestamp:
new Date()
.toISOString()
}
]
};
}
Roblox Admin Log Discord Webhook
Suppose somebody executes:
:kick PlayerName Spam
Your admin system could call:
local function logAdminAction(
admin,
command,
target,
reason
)
Webhook.Send(
"admin_action",
{
admin = {
name =
admin.Name,
userId =
admin.UserId
},
command =
command,
target =
target,
reason =
reason
}
)
end
Example:
logAdminAction(
player,
"kick",
"ExamplePlayer",
"Spam"
)
Discord embed:
if (payload.event === "admin_action") {
const data =
payload.data;
return {
embeds: [
{
title:
"Admin Action",
fields: [
{
name:
"Admin",
value:
`${data.admin.name} (${data.admin.userId})`
},
{
name:
"Command",
value:
data.command
},
{
name:
"Target",
value:
String(
data.target
)
},
{
name:
"Reason",
value:
String(
data.reason ||
"No reason"
)
}
],
timestamp:
new Date()
.toISOString()
}
]
};
}
Roblox Player Report Webhook
You could build an in-game report interface.
Client:
local ReplicatedStorage =
game:GetService("ReplicatedStorage")
local ReportEvent =
ReplicatedStorage
:WaitForChild("ReportPlayer")
ReportEvent:FireServer(
targetUserId,
"Exploiting"
)
Never trust the client blindly.
Server:
local ReplicatedStorage =
game:GetService("ReplicatedStorage")
local Players =
game:GetService("Players")
local Webhook =
require(
game.ServerScriptService.WebhookService
)
local ReportEvent =
ReplicatedStorage
:WaitForChild("ReportPlayer")
ReportEvent.OnServerEvent:Connect(
function(
player,
targetUserId,
reason
)
if typeof(targetUserId) ~= "number" then
return
end
if typeof(reason) ~= "string" then
return
end
reason =
string.sub(
reason,
1,
300
)
Webhook.Send(
"player_report",
{
reporter = {
name =
player.Name,
userId =
player.UserId
},
targetUserId =
targetUserId,
reason =
reason
}
)
end
)
Discord:
if (payload.event === "player_report") {
const data =
payload.data;
return {
embeds: [
{
title:
"New Player Report",
fields: [
{
name:
"Reporter",
value:
`${data.reporter.name} (${data.reporter.userId})`
},
{
name:
"Reported User ID",
value:
String(
data.targetUserId
)
},
{
name:
"Reason",
value:
data.reason
}
],
timestamp:
new Date()
.toISOString()
}
]
};
}
Protect Against Webhook Spam
Do not allow players to trigger unlimited reports.
Example Roblox cooldown:
local lastRequest = {}
local COOLDOWN = 10
local function canSend(player)
local now = os.clock()
local previous =
lastRequest[player.UserId]
if previous and
now - previous < COOLDOWN then
return false
end
lastRequest[player.UserId] =
now
return true
end
Use it:
ReportEvent.OnServerEvent:Connect(
function(player, targetUserId, reason)
if not canSend(player) then
return
end
-- continue
end
)
Clean up:
Players.PlayerRemoving:Connect(
function(player)
lastRequest[player.UserId] =
nil
end
)
Add Server Information
It can be useful to know exactly which Roblox server generated an event.
Webhook.Send(
"important_event",
{
placeId =
game.PlaceId,
jobId =
game.JobId,
privateServerId =
game.PrivateServerId,
playersOnline =
#Players:GetPlayers()
}
)
Discord Embed Colours
Discord embeds use decimal colour values.
Example:
{
title: "Success",
color: 5763719
}
You could define reusable values:
const COLORS = {
success: 5763719,
warning: 16776960,
error: 15548997,
info: 5793266
};
Then:
color: COLORS.error
Add Roblox Profile Links
You can create useful Discord links.
const profileUrl =
`https://www.roblox.com/users/${data.userId}/profile`;
Embed:
{
title: data.username,
url:
`https://www.roblox.com/users/${data.userId}/profile`
}
Add Roblox Game Links
const gameUrl =
`https://www.roblox.com/games/${payload.server.placeId}`;
This can make staff logs easier to use.
Send Different Events to Different Discord Channels
You can create multiple Discord webhooks.
Environment variables:
DISCORD_MODERATION_WEBHOOK=
DISCORD_PURCHASE_WEBHOOK=
DISCORD_ERRORS_WEBHOOK=
Then:
function getWebhook(event) {
if (
event === "player_report" ||
event === "admin_action"
) {
return process.env
.DISCORD_MODERATION_WEBHOOK;
}
if (event === "purchase") {
return process.env
.DISCORD_PURCHASE_WEBHOOK;
}
if (event === "game_error") {
return process.env
.DISCORD_ERRORS_WEBHOOK;
}
return null;
}
Use:
const webhook =
getWebhook(payload.event);
if (!webhook) {
return res.status(400).json({
error: "Unknown event"
});
}
Add an Event Allowlist
Do not let Roblox request arbitrary Discord payloads.
Instead of letting Roblox say:
{
"discordMessage": "anything"
}
only allow known event names.
const ALLOWED_EVENTS =
new Set([
"player_join",
"player_leave",
"purchase",
"player_report",
"admin_action",
"game_error"
]);
Then:
if (
!ALLOWED_EVENTS.has(
payload.event
)
) {
return res.status(400).json({
error: "Invalid event"
});
}
This is much safer.
Never Let the Client Control the Webhook
Bad:
RemoteEvent.OnServerEvent:Connect(
function(player, message)
Webhook.Send(
"discord_message",
{
message = message
}
)
end
)
This could turn your game into a spam mechanism.
Instead, validate:
- Who can trigger the event
- What event they can trigger
- Message length
- Cooldown
- Data types
- Permissions
Complete Roblox Discord Webhook System
The final architecture should look like this:
LocalScript
↓
RemoteEvent
↓
Server validation
↓
WebhookService
↓
HttpService
↓
Your HTTPS endpoint
↓
Authentication
↓
Rate limiting
↓
Payload validation
↓
Discord webhook
This gives you control over every stage.
Roblox Webhook Troubleshooting
HTTP Requests Are Not Enabled
Check:
Game Settings
→ Security
→ Allow HTTP Requests
HTTP 401
Your authentication token may be wrong.
HTTP 400
The JSON payload may be invalid.
HTTP 403
The destination may be rejecting your request.
HTTP 429
You are probably sending requests too quickly.
Reduce request frequency and introduce batching or queues.
Works in Studio but Not Live
Always test webhook integrations in an actual published Roblox server.
Studio networking conditions and live Roblox server networking are not necessarily identical.
Frequently Asked Questions
Can Roblox send messages to Discord?+
Yes. A common architecture uses Roblox HttpService, your own external relay and a Discord webhook.
Why doesn't my Discord webhook work in Roblox?+
Direct live-server requests to Discord webhooks have historically been blocked. Using a properly designed external webhook relay avoids relying on direct Roblox-to-Discord traffic.
Should I expose my Discord webhook URL in Roblox?+
No. Keep it on your external server where possible.
Can I log Roblox purchases to Discord?+
Yes. Purchase events can be sent to your own endpoint and formatted into Discord notifications.
Can I log admin commands?+
Yes. Server-side admin actions can trigger webhook events.
Can players abuse my webhook?+
They can if your RemoteEvents and server logic are poorly secured. Always validate and rate-limit client-triggered requests.
Continue Learning
Read the broader:
Roblox Webhooks Complete Guide
Or learn about Roblox's official platform webhooks:
Roblox Open Cloud Webhooks Guide
You can also browse Roblox scripts and assets on Freeflow.