Discord whitelist for FiveM: allow players by Discord role
Build a Discord role whitelist for your FiveM server: playerConnecting with deferrals, the Discord identifier, a bot role check through the REST API, and a safe token.
You want only players with a certain Discord role to join: members, supporters, whitelisted. FiveM has no built-in Discord check, so you add one in playerConnecting: read the player's Discord identifier, ask Discord for their roles through a bot, and accept or reject the connection.
You need three things: a Discord bot in your server, a script on the FiveM server, and a safe place for the token.
How it works
- A player connects. FiveM triggers
playerConnectingand gives you their identifiers. - You pause the connection with deferrals (see playerConnecting and deferrals).
- Your server asks the Discord REST API: is this user in the guild, and which roles do they have?
- You call
deferrals.done()to let them in, ordeferrals.done('reason')to reject.
Create the bot
- Open the Discord developer portal and create an application, then add a Bot to it.
- Copy the bot token. Treat it like a password.
- No privileged intent is needed to look up one member by ID. (The Server Members Intent only matters if a bot lists every member or listens to member events.)
- Invite the bot to your Discord server. It does not need admin rights, it only has to be a member.
- In Discord, turn on Developer Mode, then copy your server (guild) ID and the role ID of the whitelist role.
Keep the token out of the code
Set it as a convar in server.cfg, or in a cfg file that you exec and never share:
set discord_bot_token "paste-token-here"
set discord_guild_id "123456789012345678"
set discord_whitelist_role "123456789012345679"Use set, not setr. A setr convar is replicated to every client, which would leak the token to players.
Read them in the script:
local TOKEN = GetConvar('discord_bot_token', '')
local GUILD = GetConvar('discord_guild_id', '')
local ROLE = GetConvar('discord_whitelist_role', '')Warning: if the token ever ends up in a screenshot, a support message or a public repository, reset it in the developer portal straight away.
Read the Discord identifier
A player's identifiers look like discord:123456789012345678. Loop over them and strip the prefix:
local function getDiscordId(src)
for _, id in ipairs(GetPlayerIdentifiers(src)) do
if id:sub(1, 8) == 'discord:' then
return id:sub(9)
end
end
endAsk Discord for the roles
The endpoint is GET https://discord.com/api/v10/guilds/{guild.id}/members/{user.id}, authorised with Authorization: Bot <token>. The reply contains a roles array of role IDs.
local function hasWhitelistRole(discordId, cb)
PerformHttpRequest(
('https://discord.com/api/v10/guilds/%s/members/%s'):format(GUILD, discordId),
function(status, body)
if status == 200 then
local member = json.decode(body)
for _, role in ipairs(member.roles or {}) do
if role == ROLE then return cb(true) end
end
return cb(false)
elseif status == 404 then
return cb(false, 'not_in_guild')
end
cb(nil, status) -- rate limit or Discord error
end,
'GET', '',
{ ['Authorization'] = 'Bot ' .. TOKEN }
)
endA 404 means the user is not a member of your Discord server. Any other status, such as 429 or 5xx, is a problem on the way, not a "no".
The connection handler
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
local src = source
deferrals.defer()
Wait(0)
deferrals.update('Checking your Discord role...')
local discordId = getDiscordId(src)
if not discordId then
return deferrals.done('Open the Discord app on your PC, then reconnect.')
end
hasWhitelistRole(discordId, function(ok, info)
if ok then
deferrals.done()
elseif ok == false and info == 'not_in_guild' then
deferrals.done('Join our Discord server first.')
elseif ok == false then
deferrals.done('You do not have the whitelist role.')
else
deferrals.done('Could not reach Discord (' .. tostring(info) .. '). Try again in a moment.')
end
end)
end)Save source in a local first: after Wait, the global source no longer points to this player.
Rate limits and caching
Discord limits API requests per route and globally. On a restart, many players reconnect at once and each one triggers a request. Two simple protections:
- Cache results for a few minutes per Discord ID, so a player who retries does not call the API again.
- Handle
429. Discord answers with aRetry-Afterheader. Show a "try again" message and do not loop on it.
local cache = {}
local TTL = 5 * 60 * 1000
local function cachedCheck(discordId, cb)
local hit = cache[discordId]
if hit and GetGameTimer() - hit.at < TTL then
return cb(hit.ok)
end
hasWhitelistRole(discordId, function(ok, info)
if ok ~= nil then cache[discordId] = { ok = ok, at = GetGameTimer() } end
cb(ok, info)
end)
endUse cachedCheck in the handler in place of hasWhitelistRole. A cached "yes" means a removed role works up to five minutes longer, which is fine for a whitelist.
Other points
- Do not fail open. If Discord is down, reject with a clear message instead of letting everyone in, unless you have decided that is acceptable.
- Staff bypass. If you want admins to join without the role, check their identifiers against a list before calling Discord.
- Players without Discord get no identifier. Your message above tells them what to do.
- To block a cheater for good, ban them instead of removing a role. See moderating with txAdmin.
Checklist
| Symptom | Fix |
|---|---|
No discord: identifier |
The player's Discord app must be running and logged in |
401 from Discord |
Wrong or reset bot token, or missing Bot prefix |
403 from Discord |
The bot is not in that server, or the token belongs to another bot |
404 from Discord |
The user is not in your Discord server |
429 from Discord |
Rate limit: cache results and show a retry message |
| Token visible to players | Use set, not setr, and keep it out of shared files |
| Player stuck on "Checking" | Every code path must end with deferrals.done |
Quick answers
Does the player need Discord open to join?
Yes. FiveM only sends the discord: identifier when the Discord desktop app is running and logged in on the player's PC. Without it, the identifier is missing and your script should show a clear message.
Where do I put the bot token?
In a convar set in server.cfg or a separate cfg file, read with GetConvar. Never put it in a Lua file that is shared, uploaded or stored in a repository.
Do I call the Discord API on every connection?
You can, but cache the result for a few minutes. Discord rate limits requests and a restart with many players reconnecting can hit the limit.

