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

  1. A player connects. FiveM triggers playerConnecting and gives you their identifiers.
  2. You pause the connection with deferrals (see playerConnecting and deferrals).
  3. Your server asks the Discord REST API: is this user in the guild, and which roles do they have?
  4. You call deferrals.done() to let them in, or deferrals.done('reason') to reject.

Create the bot

  1. Open the Discord developer portal and create an application, then add a Bot to it.
  2. Copy the bot token. Treat it like a password.
  3. 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.)
  4. Invite the bot to your Discord server. It does not need admin rights, it only has to be a member.
  5. 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:

cfg
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:

lua
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:

lua
local function getDiscordId(src)
    for _, id in ipairs(GetPlayerIdentifiers(src)) do
        if id:sub(1, 8) == 'discord:' then
            return id:sub(9)
        end
    end
end

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

lua
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 }
    )
end

A 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

lua
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 a Retry-After header. Show a "try again" message and do not loop on it.
lua
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)
end

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

Scripts that skip this problem

Tebex TemplateA code-free premium theme for your Tebex store, edited entirely from the Tebex panel.View script →Mic PhoneA foldable phone that unfolds into a tablet and carries onto a player's real phone.View script →

Keep reading