Skip to main content
TRACK B·PRODUCTION ENGINEERING·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this lesson plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.

HTTP requests & webhooks: PerformHttpRequest

Talking to the outside world, Discord, your panel API, a license-check service, means HTTP. CFX gives you exactly one native for it: PerformHttpRequest. It is callback-based, so the response arrives later, not on the same line. It will surprise you with rate limits. And JSON bodies must be built by hand. This lesson teaches the pattern that scales from a single ban-log to a 50-server fleet.

You'll learn
PerformHttpRequest signature, why HTTP is callback-based, JSON encode for the body, retry on 429, hiding the URL from clients
Time
~20 minutes
Outcome
You can post a ban event to Discord, retry once on a rate-limit, and never leak a webhook URL to a client
BEFORE YOU START

Build it

Make the resource folder

The server has one folder for this lesson.

Using Windows File Explorer (or mkdir in a Linux shell, or the VS Code Explorer), go to your server-data folder's resources directory - the same folder your other resources live in - and create a new folder. The full path is your server-data path plus:

code
resources/qu_http_webhooks

Create the files

Every file named in the manifest exists.

Create this exact file layout:

code
resources/qu_http_webhooks/
fxmanifest.lua
server.lua

Write fxmanifest.lua

FiveM knows which files to load.

Open fxmanifest.lua and paste this:

code
fx_version 'cerulean'
game 'gta5'

server_script 'server.lua'

Older tutorials add a lua54 'yes' line here. As of June 2025 that setting is deprecated and ignored: Lua 5.4 is now the only Lua runtime, so you leave it out. Notice there is no client_script line. Everything in this lesson runs on the server only, which is exactly what keeps the webhook URL off players' machines.

Set the webhook in server.cfg

The secret URL lives in config, not in code.

The webhook URL is a secret. Anyone who has it can post to your Discord channel forever. Never paste the real URL into chat, a screenshot, a public repo, or a screen-share - and if it leaks, delete that webhook in Discord and create a new one (this rotates the token). It does not go in the script. It goes in server.cfg as a convar, above the ensure qu_http_webhooks line you will add in Step 6:

code
set qu_http_webhooks_webhook "https://discord.com/api/webhooks/123456789/your-token-here"

Create the webhook in Discord under Server Settings, then Integrations, then Webhooks. Copy the URL it gives you and paste it in place of the example above.

Write the lesson code

The topic is now represented by runnable code.

Open server.lua and paste this. It reads the URL from the convar, builds a JSON body, POSTs it, and retries once if Discord rate-limits you:

code
local webhook = GetConvar('qu_http_webhooks_webhook', '')

local function postToDiscord(message, attempt)
attempt = attempt or 1
local body = json.encode({ username = 'qu_http_webhooks', content = message })
PerformHttpRequest(webhook, function(code, response, headers)
    if code == 429 and attempt == 1 then
        local data = response and json.decode(response) or nil
        local retryAfter = tonumber(data and data.retry_after)
            or tonumber(headers['retry-after'] or headers['Retry-After'])
            or 1
        print('[qu_http_webhooks] rate-limited, retrying in ' .. retryAfter .. 's')
        SetTimeout(retryAfter * 1000, function()
            postToDiscord(message, 2)
        end)
        return
    end
    print('[qu_http_webhooks] Discord status ' .. code)
end, 'POST', body, { ['Content-Type'] = 'application/json' })
end

RegisterCommand('webhooktest', function(src)
if webhook == '' then
    print('[qu_http_webhooks] missing webhook, set qu_http_webhooks_webhook in server.cfg')
    return
end
local name = src == 0 and 'console' or GetPlayerName(src)
postToDiscord('test from ' .. name, 1)
end, true)

That final true marks the command restricted: it needs the command.webhooktest ACE permission. The server/txAdmin console always has it, which is why you run the test there. A normal in-game player would be silently denied unless you grant them with an add_ace line in server.cfg.

Start and test it

The expected proof appears in the server console.

Open server.cfg and add this line below the set line from Step 4:

code
ensure qu_http_webhooks

Save, then run:

code
restart qu_http_webhooks

Open the txAdmin web panel (by default http://localhost:40120, or your panel URL), click Live Console in the left sidebar, then type this command into the input box at the bottom and press Enter:

code
webhooktest

A message appears in your Discord channel, and the console prints:

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
  • How it works
  • If something went wrong
  • What you can do now
  • Try it yourself

The remainder of HTTP requests and webhooks is available to FiveM School members.