Skip to main content

Server events and webhooks

Administrators can manage subscriptions under Settings > Integrations > Webhooks. The list shows registered webhooks. Select Add webhook to create one, or click a webhook's name to open its editor. Each row has an Actions menu with Edit, Run test, Deliveries, and Delete. Deletion requires confirmation and removes the webhook's pending deliveries and history.

Set a name, HTTP(S) endpoint, event types, timeout (1-30 seconds), and maximum attempts (1-10, including the first attempt). A signing secret is optional. When used, choose a unique random secret of 32-1024 characters for each subscription and configure the same secret at the receiver. LANCommander never returns a saved secret. Leave the field blank when editing to preserve it, or select Remove existing signing secret to disable signing.

Without a signing secret, deliveries have no signature and the receiver cannot use LANCommander to verify their authenticity or integrity.

The dedicated editor has General and Deliveries sections. General groups short settings such as timeout and attempt limits in paired columns. Use the searchable event picker to add event tags and remove a tag to unsubscribe.

The General form includes Preview and test event. Select a lifecycle event to see its example JSON envelope and payload before saving. Save and send test saves the current form and queues that example for this endpoint only, regardless of the subscription's event filter. The subscription must be enabled. Examples use synthetic data, do not change games or other resources, and may trigger automation at the receiving endpoint. IDs and timestamps are generated when the test is queued, so they differ from the preview.

The queued delivery opens on the Deliveries page. Use Refresh to see its status, attempts, HTTP status, and errors. View JSON on any delivery shows its stored request body, including the event ID and event timestamp, pretty-printed with syntax highlighting. Use Show raw to see the exact bytes that were sent (for example, when verifying signatures).

URLs cannot contain embedded credentials, query strings, or fragments. HTTPS is recommended; HTTP and private network addresses are permitted for trusted LAN integrations. Only administrators may configure destinations. Restrict outbound network access at the host/firewall if administrators must not reach certain services. Redirects, cookies, proxies, and automatic HTTP-client request logging are disabled for webhook delivery.

Event envelope​

Event names are stable, lowercase, dot-separated names formatted as {source}.{resource}.{action}. The source prefix identifies where the action originated and always matches the envelope's source property: server for actions performed on the server (for example an administrator creating a game or uploading an archive), and launcher for actions performed in a launcher and reported to the server (installs, play sessions, and save uploads). Times are UTC; IDs are UUIDs. Consumers should ignore unknown properties to allow additive schema changes.

{
"eventId": "8071d99c-cd01-495e-95c7-28355a10a814",
"eventType": "server.game.archive.upload_completed",
"occurredAt": "2026-11-09T05:00:00Z",
"source": "server",
"actorId": null,
"targetType": "game",
"targetId": "56ad8a83-31e7-437b-8882-45c5dcf47e28",
"status": "completed",
"correlationId": null,
"payload": {
"archiveId": "88cc1ede-95fd-45ca-969c-ea4ec210dc81",
"gameVersionId": "bfdd87ef-f3aa-4ac4-83d6-c24c17d79f47",
"version": "1.0"
}
}

actorId identifies the authenticated user when known, otherwise it is null. correlationId is optional and currently null for built-in events. The status is completed, except for launcher.game.install.failed, which uses failed. Targets identify the game, user, dedicated server, tool, or redistributable, not a local file path.

EventsPayload
server.game.created, server.game.updated, server.game.deletedtitle
server.user.createduserName, approved
server.user.approved, server.user.deleteduserName
server.game.issue.createdissueId, description
server.game.issue.resolvedissueId
server.game.match.createdmatchId, serverId, map, mode
server.game.match.endedmatchId, serverId, status, reason, results
server.game.archive.upload_completedarchiveId, gameVersionId, version
launcher.game.play.startedsessionId, start
launcher.game.play.endedsessionId, start, end, timedOut
server.user.login, server.user.logoutEmpty object
launcher.game.save.uploadedsaveId, size, platforms
server.game_server.started, server.game_server.stoppedserverName
launcher.game.install.completed, launcher.game.install.failed, launcher.game.uninstall.completed, launcher.tool.installed, launcher.redistributable.installedreportedByLauncher: true
server.webhook.testmessage

Game, user, issue, match, archive upload, and play session events reuse the server's existing plugin lifecycle events, so changes a plugin makes raise them too. Game events cover add-ons. server.user.created covers registration, administrator-created accounts, and first external sign-in; when approved is false, server.user.approved follows once an administrator approves the account. Issue events target the game the issue is about; the actor is the reporting or resolving user.

Match events target the match's game; serverId is null when the match wasn't reported by a managed dedicated server. server.game.match.ended is raised both for ended and abandoned matches; its payload status is Ended or Abandoned, and results is empty for an abandoned match. It is raised again if a reporter resends results, so receivers should replace stored standings rather than add to them. Each result has participantId, playerKey, userId, team, rank, score, status, and timeMilliseconds; a reporter's free-form result payload is not included. Archive completion is raised only after an uploaded archive is stored and linked, not for packages created by import or packaging. Save completion is raised after the file is written. Ended play sessions include those ended by the stale-session sweep. Dedicated server notifications describe transitions into running and out of running, including an unexpected exit; an initial stopped status or repeated running status does not emit a notification.

Login/logout cover token authentication and application-cookie sign-in/sign-out, including external providers. Token refresh does not emit a login. Logout means the client reported a logout or its application cookie was signed out; it does not mean an already-issued bearer token was revoked.

Installation events are authenticated launcher reports, not proof of an installation. The server supplies the actor and accepts only the five supported installation outcomes for existing targets. The report contains no local paths, script output, credentials, or arbitrary payload. Reports to offline/older servers are best-effort and logged by the launcher; they are not replayed later. Older launchers do not report these outcomes. Already-detected redistributables do not emit a new installation notification. Both queued launcher installations and direct SDK game installations report their outcomes; canceled direct installations do not report a failure.

Signing and receiving​

Each POST contains UTF-8 JSON with these headers:

HeaderMeaning
X-LANCommander-Event-IdStable event ID across subscriptions and retries
X-LANCommander-Delivery-IdStable ID for this subscription's delivery
X-LANCommander-TimestampUnix timestamp in seconds, regenerated for each attempt
X-LANCommander-SignatureWhen signing is configured, sha256= followed by lowercase hexadecimal HMAC-SHA256

When configured, the signature covers the UTF-8 bytes of timestamp + "." + raw_request_body, using the UTF-8 signing secret as the key. Verify against the raw bytes, not reformatted/deserialized JSON; compare signatures in constant time, reject stale timestamps (for example, over five minutes old), and deduplicate by delivery ID. Return a 2xx response promptly, then do expensive work asynchronously.

For example, the receiver computes:

var signed = Encoding.UTF8.GetBytes(timestamp + "." + rawBody);
var expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), signed);
var supplied = Convert.FromHexString(signature["sha256=".Length..]);
bool valid = CryptographicOperations.FixedTimeEquals(expected, supplied);

Validate the header format, timestamp, and signature length before comparison. Never log the signing secret or untrusted response bodies.

Delivery behavior and administration​

Matching, enabled subscriptions get database-backed pending deliveries before an event is handed to Rebus. HTTP runs in a background consumer, not in the original upload/login/installation request. Pending deliveries are recovered every five seconds, including after a server restart. Up to four deliveries and consumers run concurrently, so a slow destination does not delay core operations or hold up all other notifications. Each attempt is claimed with a conditional database update, so concurrent handlers (including other server instances sharing the database) never send the same attempt twice.

Any non-2xx response, timeout, or connection failure retries with exponential backoff: 5, 10, 20, 40 seconds, and so on, capped at one hour. The configured attempt limit is absolute, including attempts interrupted by a restart. Delivery is at-least-once within that limit: a receiver may accept a request immediately before the server exits without recording success. There is no global ordering guarantee when retries are involved.

Deliveries shows the latest 100 records, including IDs, pending/delivered/ failed/canceled status, attempt count, next attempt time, HTTP status, and a redacted failure classification. Response bodies, reason phrases, exception messages, and secrets are not retained. Disable a subscription to cancel pending work when it is next processed; an HTTP request already in flight may finish. Edits apply to pending deliveries, including key rotation and destination changes. Deleting a subscription removes its pending deliveries and history. History is otherwise retained until the subscription is deleted.

The subscription list's Run test action queues a server.webhook.test event for that subscription only, regardless of its event filter; disabled subscriptions cannot be tested. Refresh the delivery list to see the result. A test payload has targetType: "webhook", the subscription's ID as targetId, and:

{ "message": "LANCommander test webhook" }

API callers can select a lifecycle example with POST /api/Webhooks/{id}/Test?eventType=server.game.created. Omitting eventType preserves the server.webhook.test connectivity test. Retrieve an exact request body with GET /api/Webhooks/{id}/Deliveries/{deliveryId}/Body; this endpoint also requires an administrator and returns 404 if the delivery does not belong to the subscription.

Administrator API endpoints are /api/Webhooks, /api/Webhooks/EventTypes, /api/Webhooks/{id}, /api/Webhooks/{id}/Test, and /api/Webhooks/{id}/Deliveries. Create with POST, edit with PUT, and delete with DELETE. Creation/edit accepts:

{
"name": "Archive announcements",
"endpoint": "https://receiver.example/events",
"secret": "replace-with-a-unique-random-secret-at-least-32-characters",
"eventTypes": ["server.game.archive.upload_completed"],
"enabled": true,
"timeoutSeconds": 10,
"maxAttempts": 5
}

Omit secret or leave it empty to create an unsigned subscription. On edit, an omitted or empty secret preserves the existing one; set clearSecret to true without a replacement secret to remove it. The subscription response reports hasSigningSecret without revealing the secret. Signing changes apply to pending deliveries on their next attempt.

Secrets are encrypted with ASP.NET Core Data Protection. Preserve the server identity's Data Protection key ring as well as the database when backing up or moving the server. If a configured secret cannot be decrypted, delivery fails with "Signing key unavailable" rather than being sent unsigned.

Architecture and plugins​

Core producers use IServerEventPublisher, not HTTP or a notification provider. The common ServerEvent message is carried by Rebus using System.Text.Json. The ServerEvent envelope and the ServerEventTypes / ServerEventSources constants are in the LANCommander.SDK.Events namespace of the SDK package, so launchers, launcher plugins, and .NET webhook receivers can share them. The extension points below are in the LANCommander.Server.Plugins.Messaging namespace of the server plugin package. Register implementations from a plugin's ConfigureServices.

Consumers. Implement IServerEventConsumer to react to every published event, for example to post to Slack. Webhooks are the built-in consumer. Consumers run in the background after the originating operation and are isolated: an exception is logged (type only) and does not affect other consumers. Consumers own their own filtering, retries, persistence, and secrets; they do not inherit webhook durability.

public sealed class SlackPlugin : IPlugin
{
public void ConfigureServices(IServiceCollection services) =>
services.AddSingleton<IServerEventConsumer, SlackEventConsumer>();
}

public sealed class SlackEventConsumer : IServerEventConsumer
{
public async Task HandleAsync(ServerEvent serverEvent, CancellationToken cancellationToken)
{
if (serverEvent.EventType != ServerEventTypes.ArchiveUploaded)
return;
// Post to Slack, handling your own retries.
}
}

Transports. By default events travel over an in-process transport that needs no broker. Implement IServerEventTransport to replace it, for example with a RabbitMQ transport from the corresponding Rebus package. If several are registered, the first is used and a warning is logged. All instances consume from the server-events queue, so each event, and each scheduled webhook delivery, is processed by one instance. Transports without native publish/subscribe must also configure subscription storage through ConfigureSubscriptions.

public sealed class RabbitMqTransport(string connectionString) : IServerEventTransport
{
public void ConfigureTransport(StandardConfigurer<ITransport> transport, string queueName) =>
transport.UseRabbitMq(connectionString, queueName);
}

Limitations​

Webhook deliveries are durable once persisted, but the originating domain change and notification persistence are not one atomic transaction. A process exit between them, or a database failure while publishing, can lose a notification. Publication failures are logged without turning a successful operation into a failure. With the default transport, events not yet handed to a plugin consumer are lost if the process exits; webhooks recover from the database.

Delivery is at-least-once: receivers must deduplicate by delivery ID. Running several instances requires them to share the database, the Data Protection key ring, and a broker transport. Routing commands to a particular instance (for example satellite servers) is not part of this event system. Plugins cannot yet publish custom event types. Discord-specific formatting is a follow-up; a generic event body cannot be posted directly to a Discord webhook.