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.
Log safely
Send only from the server
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.
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
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.
-- 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 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.
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
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.
- Common mistakes
- What you can do now
The remainder of Webhooks and privacy is available to FiveM School members.