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​