Skip to main content
Test and release

Written lessons and reference material are currently in English.

On this page

Webhooks and privacy

Webhooks are how your server tells Discord what happened: a sale, a ban, an error. Done carelessly they leak the webhook URL, dump player data into a channel, or spam Discord until it rate-limits you. This lesson keeps logging useful and safe.

You'll learn
How to log to Discord webhooks without leaking secrets, spamming channels, or exposing player data.
Time
~20 minutes.
You need
The HTTP/webhooks lesson and a server you can configure.
The arc
Webhooks are secrets, send server-side only, decide what NOT to log, then rate-limit and redact.
BEFORE YOU START

Log safely

Send only from the server

The webhook URL never reaches a client.

All webhook calls happen in server scripts, with the URL read from a convar. A client never sees the URL and never triggers the send directly without a server check. PerformHttpRequest is a server native, so this code can only live in a server_script.

Read the URL from a convar, then post a Discord embed. Discord expects JSON, so set the Content-Type header and encode the body with json.encode.

code
local hook = GetConvar('logs_webhook', '')

local function sendLog(title, description)
    if hook == '' then return end -- no webhook configured, do nothing
    local body = json.encode({
        embeds = {
            {
                title = title,
                description = description,
                color = 3447003, -- a blue stripe down the embed
            }
        }
    })
    PerformHttpRequest(hook, function(status)
        if status ~= 200 and status ~= 204 then
            print(('[logs] webhook returned %s'):format(status))
        end
    end, 'POST', body, { ['Content-Type'] = 'application/json' })
end

Decide what NOT to log

No private or sensitive data goes to a channel.

Log events, not secrets. Never send license keys, passwords, full IPs, tokens, or anything that identifies a real person beyond what you actually need.

Do not use FiveM's GetHashKey as privacy protection. It is a fast 32-bit game hash, not a cryptographic hash, so it can collide and does not provide a meaningful irreversible pseudonym. Keep raw identifiers in your access-controlled database when they are genuinely required. Post an internal audit-row id or a short-lived server source to Discord, then let authorized staff look up the private record.

code
-- The private database row contains the durable identifier and evidence.
-- Discord receives only the internal case id and current server slot.
local caseId = 1842
local src = source
sendLog('Ban issued', ('Case #%d, current source %d'):format(caseId, src))

Rate-limit your sends

Discord does not throttle or block your webhook.

Discord rate-limits webhooks. Batch or throttle high-frequency events instead of firing one request per tick. A log that spams itself stops logging when Discord cuts it off.

Push lines onto a queue and flush them on a timer. One request every few seconds carries many events instead of one request per event.

code
local queue = {}

local function logEvent(line)
    queue[#queue + 1] = line
end

CreateThread(function()
    while true do
        Wait(3000) -- flush at most once every 3 seconds
        if #queue > 0 then
            local batch = table.concat(queue, '\n')
            queue = {}
            sendLog('Activity', batch)
        end
    end
end)

Separate channels by severity

Important logs are not buried in noise.

Send errors and security events to a private staff channel, routine activity to another. Mixing a critical ban alert into a firehose of chat logs means nobody sees it.

Keep reading the full lesson

Sign in to start, then unlock every step of this lesson and the full FiveM School with a membership.

Still ahead in this lesson
  • Common mistakes
  • What you can do now

The remainder of Webhooks and privacy is available to FiveM School members.

Open the full lesson to mark it complete.