Discord webhook logs from a FiveM server: PerformHttpRequest guide
Send logs to Discord from FiveM with PerformHttpRequest and JSON embeds. Server side only, URL in a convar, a queue for rate limits, and what to log.
You want to know who bought what, who gave money, who ran an admin command and who joined, without watching the console all day. A Discord webhook does it: your server posts a message to a channel. This guide shows the server side Lua with PerformHttpRequest, how to build embeds, how to keep the URL safe and how to avoid the rate limit.
Create the webhook
In Discord, open the channel settings, then Integrations, then Webhooks, create one and copy its URL. It looks like https://discord.com/api/webhooks/<id>/<token>. Anyone with that URL can post in your channel, so treat it like a password.
Keep the URL in a convar
Put it in server.cfg:
set discord_webhook_logs "https://discord.com/api/webhooks/ID/TOKEN"Use set, not setr or sets. A replicated convar is sent to every client, which would leak the URL. Read it on the server:
local WEBHOOK = GetConvar('discord_webhook_logs', '')If the convar is empty, skip sending instead of erroring. You can keep one convar per type of log (money, admin, joins) to send them to different channels.
Send a message
local function sendLog(title, description, color, fields)
if WEBHOOK == '' then return end
local embed = {
title = title,
description = description,
color = color or 3447003,
fields = fields,
timestamp = os.date('!%Y-%m-%dT%H:%M:%SZ'),
}
PerformHttpRequest(WEBHOOK, function(status, body, headers)
if status ~= 204 and status ~= 200 then
print(('webhook failed: %s'):format(status))
end
end, 'POST', json.encode({ username = 'Server logs', embeds = { embed } }), {
['Content-Type'] = 'application/json',
})
endPoints to know:
coloris a decimal number, not a hex string.3447003is blue and15158332is red. Convert hex withtonumber('FF0000', 16).timestampmust be an ISO 8601 date in UTC. The!in the format makesos.dateuse UTC.fieldsis a list of{ name = '...', value = '...', inline = true }. A field value cannot be empty.- A successful webhook call returns status 204, with an empty body.
- Discord limits sizes. Titles are short (256 characters), descriptions are long but capped (4096), and a message holds a limited number of fields and embeds. Cut long text before you send it.
An example call, for a player who sent money:
sendLog('Money transfer', 'A player sent money', 3066993, {
{ name = 'From', value = ('%s (id %d)'):format(GetPlayerName(src), src), inline = true },
{ name = 'To', value = ('%s (id %d)'):format(GetPlayerName(target), target), inline = true },
{ name = 'Amount', value = ('$%d'):format(amount), inline = false },
})Server side only, never from the client
Warning: do not call
PerformHttpRequestfor a webhook in a client script, and do not send the URL to the client. Client files can be read by every player, so the URL leaks and anyone can flood your channel.
If an event on the client should be logged, send an event to the server, and let the server decide what to log from data it trusts. A client that can trigger "log this text" can also trigger fake logs. For event safety in general, read secure events in FiveM. Log what the server did, not what the client says it did.
Rate limits: use a queue
Discord rejects webhook calls that arrive too fast, with a 429 response. A burst of logs, such as 50 players triggering the same event, can hit it. Send the messages from a queue at a steady pace:
local queue = {}
local function enqueue(payload)
queue[#queue + 1] = payload
end
CreateThread(function()
while true do
local item = table.remove(queue, 1)
if item then
PerformHttpRequest(WEBHOOK, function(status, _, headers)
if status == 429 then
-- put it back and wait a bit longer
table.insert(queue, 1, item)
Wait(5000)
end
end, 'POST', json.encode(item), { ['Content-Type'] = 'application/json' })
end
Wait(2000)
end
end)Waiting two seconds between messages stays under the limit. If you need more, group several embeds into one message: one request can carry up to 10 embeds in its embeds list. For the use of Wait, see Wait explained.
What to log
Log the events you would want when something goes wrong:
- Money: large transfers, purchases, anything that creates money. This is where abuse shows first.
- Admin actions: who used which admin command on whom, bans, warnings, teleports. See ban and warn players.
- Joins and leaves, with the player's identifiers, which helps with disputes and bans.
- Item spawning and weapon use, if you allow them.
Do not log everything. A channel with a message every second is a channel nobody reads. Pick a few channels, put the important ones where staff look, and keep the rest short. Keep personal data to what you need, and do not log private chat.
Tip: put your logging function in a small resource and call it with an export from your other scripts, so the webhook URL and the rate limit queue live in one place. See exports in FiveM.
Checklist
| Symptom | Fix |
|---|---|
| Nothing arrives | Check the convar name, that the URL is complete and that the call runs on the server |
| Status 400 | The JSON is invalid: empty field value, too long text, or a hex colour string |
| Status 429 | Send through a queue with a pause between messages |
| Timestamp ignored | Use UTC ISO 8601: os.date('!%Y-%m-%dT%H:%M:%SZ') |
| URL leaked | Delete the webhook in Discord, create a new one, and keep it in a set convar |
| Too many messages | Log only money, admin actions and joins |
Quick answers
Can I send a Discord webhook from the client?
No. Anyone can read client code and steal the webhook URL, then spam your channel. Trigger the log on the server and send it from there.
How many webhook messages can I send?
Discord rate limits webhooks, and roughly 30 messages per minute per webhook is a safe ceiling. Beyond that you get a 429 response, so queue your messages and send them at a steady pace.
Where should I keep the webhook URL?
In a server convar set with set in server.cfg, read with GetConvar. Never put it in a client file or in a convar set with setr or sets.
Scripts that skip this problem
Advanced BoostingTablet-driven vehicle boosting: contracts from class D to S+, crews and a live queue.View script →
Crypto MiningBuy a warehouse, build rigs part by part and mine coins on a market that moves.View script →
Shop CreatorBuild a shop in under a minute — owners, employees, vaults and robberies included.View script →