F5 StudioF5 Studio
Skip to main content

Discord Webhooks

F5 Combat HUD has exactly one Discord integration: the marketplace log. Eight events — three by players, five by moderators — reach Discord as rich embeds, optionally with a rendered picture of the crosshair involved.

Nothing else is logged to Discord. Kills, killstreaks, profile saves and settings changes never leave your server.

Where It Lives

server/config_server.lua
F5Cfg.MarketplaceWebhook = {
enabled = true,
url = 'CHANGE_ME',

botName = 'F5 Combat HUD',
avatarUrl = '',
attachPreview = true,

colors = { ... },
logEvents = { ... },

footerText = 'F5 Combat HUD — Marketplace',
footerIcon = '',
}
Server-only, and it must stay that way

server/config_server.lua is listed under server_scripts and nowhere else, so the URL is never downloaded by a client. The only configuration the NUI ever receives is a hand-written whitelist of booleans that contains no webhook fields. Never move this table into config/ — those files are shared scripts and every client would get your webhook URL.

Setup

  1. In Discord: Server Settings → Integrations → Webhooks → New Webhook, pick a channel, copy the URL.
  2. Paste it into url.
  3. Restart the resource.
server/config_server.lua
F5Cfg.MarketplaceWebhook = {
enabled = true,
url = 'https://discord.com/api/webhooks/123456789/abcdef...',
}

Until you do, the server says so once at startup and sends nothing:

[f5_combathud] MarketplaceWebhook.enabled = true, but url is still the placeholder — no marketplace event will reach Discord. Paste the webhook url in server/config_server.lua, or set enabled = false to silence this.
A wrong URL is silent

That warning fires for the literal 'CHANGE_ME', for an empty string, and when url is not a string at all. Any other value passes — there is no check that the URL is even a Discord address. A mistyped URL produces no startup warning; you only find out from one yellow line per dropped event (see Failures).

There is also no way to test a webhook from in game — no command, no button. The only proof it works is a real marketplace action arriving in the channel.

Options

OptionTypeDefaultDescription
enabledbooleantrueMaster switch. Any value other than false/nil counts as on
urlstring'CHANGE_ME'The Discord webhook URL. 'CHANGE_ME' and '' mean "not configured"
botNamestring'F5 Combat HUD'Sent as the webhook username. Not validated
avatarUrlstring''Sent as avatar_url. Always sent, even when empty
attachPreviewbooleantrueAttach a rendered PNG of the crosshair. Only the literal false turns it off
colorstablesee belowEmbed accent colour per event, as a decimal integer
logEventstableall trueWhich events reach Discord. Compared with == true
footerTextstring'F5 Combat HUD — Marketplace'Prefix of the embed footer, followed by | and the server-local date
footerIconstring''Footer icon URL
logEvents is strict, everything else is loose

logEvents.publish = 1 or = 'yes' disables that event — only the boolean true enables it. enabled, by contrast, treats any truthy value as on. If the whole logEvents table is missing, nothing is ever sent.

Events and colours

EventFired byDefault colour
publishA player publishing a crosshair3066993 — green
updateA player editing their listing3447003 — blue
unpublishA player removing their own listing15158332 — red
admin_removeA moderator deleting a listing10038562 — dark red
admin_hideA moderator hiding a listing15105570 — orange
admin_unhideA moderator restoring a listing3066993 — green
admin_banA moderator banning a player from the marketplace10038562 — dark red
admin_unbanA moderator lifting a ban3447003 — blue

Colours are decimal, not hex strings: 0x2ECC71 is written 3066993. An event with no entry in colors uses Discord blurple (7506394).

To log moderation only:

server/config_server.lua
logEvents = {
publish = false,
update = false,
unpublish = false,
admin_remove = true,
admin_hide = true,
admin_unhide = true,
admin_ban = true,
admin_unban = true,
},

What an Embed Contains

Every embed opens with the same three inline fields, describing the player who performed the action:

FieldContent
🎮 PlayerIn-game name and server id
🆔 Citizen IDcitizenid on QBCore / QBox, identifier on ESX
👤 CharacterThe character's name

and closes with 🔑 Identifiers — Steam, License, Discord and FiveM, with their prefixes stripped. Missing ones read N/A; the Discord id is rendered as a mention that does not ping (allowed_mentions is empty).

Between them come the event's own fields:

EventExtra fields
publish, update📋 Listing (id + title), 📁 Type, 📝 Description (first 200 bytes), 🎯 Crosshair
unpublish📋 Listing, 📁 Type, 🎯 Crosshair
admin_remove, admin_hide, admin_unhide📋 Listing, 📁 Type, 👤 Listing Owner, 🎯 Crosshair
admin_ban🚫 Target, 📝 Reason, ⏳ Duration, 🗑️ Listings removed (how many of their listings were deleted)
admin_unban🚫 Target (the target's license)

⏳ Duration is one of 1 days, 3 days, 7 days, 30 days, 90 days or permanent — the ban callback refuses any other length before anything is written or logged.

On a moderation embed, the identifiers are the moderator's

🎮 Player, 🆔 Citizen ID, 👤 Character and 🔑 Identifiers always describe who acted. The affected player appears as 👤 Listing Owner or 🚫 Target. Reading the identifier block as "the offender" is the classic mistake.

The 🎯 Crosshair field

The crosshair is described in words, so a moderator can judge a listing without opening the game:

Type **CROSS** • Size 14 • Thickness 2 • Gap 4
Colour `#00FFFF` • Outline `#000000` (1px)
Effects: dynamic, glow, rainbow

Recognised effect words: center dot, dynamic, rainbow, glow, HP colour, the animation name, ADS only, hidden on ADS. A custom-image crosshair is described instead as Custom image (external URL | uploaded image) • Size N • Opacity N% • Rotation N°.

For publish and update the description is built from the profile the server just validated and stored; for unpublish and the moderation events it is rebuilt from the listing row in the database. Either way it is never text the client supplied.

Crosshair Preview Images

With attachPreview = true most embeds carry a 200×200 PNG of the crosshair, uploaded as crosshair.png.

How it gets there: the acting player's NUI renders it on a canvas and base64-encodes it; the server decodes it and attaches it to the POST. The server accepts it only if:

  • the base64 string is at most 196 608 characters,
  • the decoded image is between 1 byte and 96 KB,
  • and the bytes start with a valid PNG signature.

Anything else is dropped silently and the text-only embed is sent.

The picture comes from a player's game client

Because the image is produced client-side, a modified client could attach a picture of its own choosing — it would end up permanently hosted on Discord's CDN in your channel. The server checks the size and the PNG header, nothing else. Set attachPreview = false if you would rather not have player-supplied images in your Discord; the written description of the crosshair is sent either way.

Three cases produce no image even when attachPreview is on:

  • Custom-image crosshairs — the NUI does not render a preview for them.
  • admin_ban / admin_unban — those events never carry a picture.
  • A listing the acting client has not got loaded — unpublish and the moderation events draw the picture from the listing the client currently has in view, so an action taken on a listing that is no longer in its cached page arrives as text only.

Delivery

BehaviourValue
BatchingNone — one event, one POST
Spacing250 ms between two posts to the same URL
Attempts3
BackoffRetry-After if Discord sends one, otherwise 5 s then 10 s
HTTP timeout10 s
SuccessHTTP 200 or 204
RetriedTimeouts, 429, and any status ≥ 500
Dropped immediatelyEverything else, including 400, 401, 403 and 404
PersistenceNone — the queue lives in memory

Events are queued per URL and delivered strictly in order by a single worker. A retry waits at the front of the queue, so a Discord outage delays everything behind it, and a resource restart during that wait discards whatever was still queued.

Failures

Console lineMeaning
MarketplaceWebhook.enabled = true, but url is still the placeholder …Printed once at startup. Nothing will ever be sent
F5 Combat HUD webhook failed (status: 401, attempts: 1)Discord rejected it. 400/401/403/404 usually mean a wrong URL, an over-long botName, or a malformed avatarUrl / footerIcon
F5 Combat HUD webhook failed (status: 429, attempts: 3)Rate-limited past the retries — several servers sharing one webhook URL, or a very busy marketplace
F5 Combat HUD webhook: image upload rejected (status: …) — resending without itThe POST carrying the image failed; the event is re-sent as text only. A status of 0 here means the host was unreachable, not that the image was bad

A player never sees any of this — the in-game action succeeds regardless of whether Discord accepted the log.

The webhook block is not validated at startup

shared/config_validator.lua does not inspect this table. A typo'd key, a colour written as a hex string, or a missing logEvents produces no complaint — the events simply never arrive. Check the console after your first publish.

The ban embed names the target as the moderator's client sent it

On admin_ban, 🚫 Target is the player name the moderator's menu supplied, sanitised — the banned license appears there only when that name comes through empty. admin_unban is the opposite and always shows the raw license.

If you need an authoritative record of who was banned, read the f5_combathud_admin_audit table: it stores the license itself.

Discord Is Not Your Only Audit Trail

Every moderation action is also written to the f5_combathud_admin_audit table, independently of Discord. Turning the webhook off does not lose the audit trail — it only stops the Discord copy. See Database → Admin audit.

See Also