Webhooks
GZ::CTF (this fork) can push live game events to an external HTTP endpoint as they happen. Each game carries its own webhook URL, so the platform fires an HTTP POST with a Discord-compatible JSON body whenever a noteworthy event occurs in that game — a blood is taken, a new challenge or hint drops, an admin posts an announcement, or the anti-cheat flags a team.
This webhook integration is exclusive to this fork — it does not exist in upstream GZ::CTF. The payload is shaped as a Discord webhook message, so it works out-of-the-box against a Discord channel webhook, but any endpoint that accepts a JSON POST (a SIEM ingest URL, a chat bridge, an automation runner) can consume it.
What webhooks are for
The feature exists to fan platform activity out to systems that live outside GZ::CTF:
- Chat / ops visibility — drop bloods, new releases, and announcements into a Discord channel (the native format) or anything that speaks the same shape.
- SIEM / monitoring — forward
Cheat Detectedevents to a security pipeline. - Automation — trigger downstream workflows when a challenge is released or a blood is scored.
The platform is the sender; your endpoint is the receiver. There is no inbound webhook, no callback, and no acknowledgement contract beyond the HTTP status code.
How delivery works
When an event is recorded, the relevant repository looks up the game's configured webhook URL and, if one is set, fires the send out-of-band (_ = webhookService.Send...) so the game flow is never blocked or failed by a slow or broken endpoint.
- Events (
EventType) are sent fromGameEventRepository.AddEventviaSendGameEventAsync. - Notices (
NoticeType) are sent fromGameNoticeRepository.AddNoticeviaSendNoticeAsync— but only when the notice is broadcast (broadcast: true).
The sender (SendWebhookService) has these characteristics:
There is no signing, no HMAC, no secret header, and no custom header of any kind. The only authentication is the secret embedded in the webhook URL itself (e.g. the token segment of a Discord webhook URL). Treat the URL as a credential, prefer https, and do not log or commit it. If your receiver needs to verify authenticity, put a secret token in the URL path/query and validate it server-side.
If the endpoint returns a non-2xx status, the failure is logged server-side (status code, reason phrase, and the first 400 chars of the response body) and otherwise swallowed — the event is not retried or queued. Invalid (non-http/https) URLs are skipped with a warning.
Which events fire a webhook
Although the platform records many internal event and notice types, the webhook sender deliberately filters down to a small, low-noise set. Anything not listed below produces no outbound webhook.
Notices (NoticeType)
Every broadcast GameNotice is forwarded. The sender renders each type into a titled, colored embed:
Blood notices carry Values = [teamName, challengeName] (set in GameNotice.FromSubmission). NewChallenge / NewHint carry Values = [title]. Normal announcements carry the announcement text as the first value.
Events (EventType)
Only one event type is forwarded:
For CheatDetected, the event's Values are [challengeName, teamName, sourceTeamName] (set in FlagChecker), and these are joined with , into the Details: line. The embed footer is the game title.
Payload shape
The body is a Discord webhook message object, serialized camelCase with nulls omitted. In practice every webhook this fork sends contains exactly one embed (no top-level content, username, or avatarUrl are set by the code).
The full set of fields the sender can emit (from the Models classes in SendWebhookService.cs):
Discord-compatible truncation. Before sending, the message is clamped to Discord's documented limits: title ≤ 256, description ≤ 4096, footer ≤ 2048, field name ≤ 256, field value ≤ 1024, ≤ 25 fields, and a 6000-char total per embed (overflow is trimmed from the description). If you build a non-Discord consumer, expect these caps on long values.
How to configure it
The webhook URL is per game, not global. It is stored on the Game entity as DiscordWebhook (a nullable string, max length 255 — Limits.UrlLength) and edited through the same admin API/UI as the rest of a game's settings.
Via the admin UI
In the admin panel, open Games → (your game) → Info. The Discord Webhook field (placeholder https://discord.com/api/webhooks/..., helper text "Send notifications to this webhook") sets the URL. Save the game to persist it. Leaving it blank disables webhooks for that game.
Via the API
The field is exposed on the game-edit model as discordWebhook (GameInfoModel.DiscordWebhook) and is round-tripped on game create/update:
Set discordWebhook to null or an empty string to turn webhooks off for that game (the sender only fires when the stored value has Length > 0).
There is no appsettings.json key for webhooks. Webhooks are configured entirely as per-game data through the admin API/UI — they are not a static configuration block, environment variable, or section in appsettings. Nothing about webhooks needs a service restart; changing the game's discordWebhook takes effect on the next event.
Testing your endpoint
Because the body is plain JSON over POST, you can validate any receiver with the exact shape the platform sends:
To see real traffic end-to-end, point discordWebhook at a temporary request-capture URL (e.g. a request-bin service or a local listener), then trigger an event — score a first blood, release a challenge, or publish an announcement — and inspect the captured body.
The webhook URL is a bearer credential. Anyone holding it can post to your channel/endpoint, and there is no additional signature to fall back on. Rotate it (re-issue the Discord webhook / your receiver token and update the game) if it is ever exposed in logs, exports, or screenshots.
Related
- appsettings reference — global platform configuration (note: webhooks are not configured here).
- Scoring — blood mechanics that drive
FirstBlood/SecondBlood/ThirdBloodnotices. - Attack & Defense — runs in the same game model that carries the per-game
discordWebhook. - Challenge YAML — authoring challenges whose release triggers
NewChallenge/NewHintnotices.