Freeflow

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:

Complete Roblox Webhooks Guide

Freeflow Team•Sep 28, 2026•9 min read
01 /

How 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.

02 /

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.

03 /

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
04 /

Step 3: Create a Discord Webhook

Inside your Discord server:

  1. Open the channel.
  2. Open channel settings.
  3. Find Integrations.
  4. Create a webhook.
  5. Choose the channel.
  6. 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
05 /

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.

06 /

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);
07 /

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()
			}
		]
	};
}
08 /

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()
			}
		]
	};
}
09 /

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()
			}
		]
	};
}
10 /

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()
			}
		]
	};
}
11 /

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
)
12 /

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()
	}
)
13 /

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

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`
}
const gameUrl =
	`https://www.roblox.com/games/${payload.server.placeId}`;

This can make staff logs easier to use.

16 /

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"
	});

}
17 /

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.

18 /

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
19 /

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.

20 /

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.

21 /

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.