Realtime for server integrations
Receive changes over WebSocket and perform writes over HTTP.
Authentication
Server integrations send X-Api-Key in the WebSocket connection header. The browser's native WebSocket does not allow this header to be set. Userscripts use the game client API, without a separate connection using a key.
Choose a gateway in the same environment as the key: dev-gateway.lootlog.pl or gateway.lootlog.pl. The SDK provides a client for the existing realtime protocol and supports a server-side WebSocket implementation.
Bun example
Run this example on a server using Bun. In a TypeScript project, use Bun types and the ES2022 or ESNext library without DOM so the WebSocket constructor accepts server options. Set LOOTLOG_API_KEY and LOOTLOG_ORGANIZATION_ID for the development environment. The WebSocket factory adds the key header; do not pass it in the URL.
import {
RealtimeClient,
REALTIME_JSON_SUBPROTOCOL,
} from "@lootlog/sdk/realtime";
const apiKey = process.env.LOOTLOG_API_KEY;
const organizationId = process.env.LOOTLOG_ORGANIZATION_ID;
if (!apiKey || !organizationId)
throw new Error("Missing Lootlog configuration");
const client = new RealtimeClient({
url: "wss://dev-gateway.lootlog.pl",
protocols: [REALTIME_JSON_SUBPROTOCOL],
frameEncoding: "json",
webSocketFactory: (url, protocols) =>
new WebSocket(url, {
protocols,
headers: { "X-Api-Key": apiKey },
}),
});
const stopEvents = client.subscribe((event) => {
if (event.type === "timer.created") console.log(event.data);
});
const stopState = client.subscribeState((state) => console.log(state));
// Store the session and subscription before connecting so they can be restored.
await client.join({});
await client.subscribeScope({
topic: "organization.timers",
organizationId,
world: "fobos",
});
client.connect();
process.once("SIGINT", () => {
stopEvents();
stopState();
client.disconnect();
});The ready state means the client has joined the session. The SDK restores the stored session and subscriptions after reconnecting. This example logs only timer.created events; fetch the initial timer list separately over HTTP. In a running integration, limit retry duration or attempts after a persistent authentication error and notify the operator.
Allowed actions
An integration receives available events and uses permitted subscriptions and queries. Its scope is limited by the key's Organizations and the user's current permissions. Perform writes, such as adding a timer, over HTTP.
An integration connection does not represent a character's presence in the game. The game client publishes presence and game observations.
Presence snapshots
presence.fetch accepts an Organization ID, an optional world, and optional
delivery: "response". With a requestId, this option returns the snapshot only
in the correlated response. Without the option, the gateway also emits
presence.snapshot for compatibility with existing integrations. Commands
without a requestId always receive the snapshot event.
Clients using response delivery must hydrate from the response and apply
presence.delta events for subsequent changes. Older gateways ignore the option
and still send both frames; ignore their snapshot events to avoid applying the
same collection twice. Deploying the gateway first enables the bandwidth saving
as soon as clients opt in. The existing permission and location filters apply to
both delivery modes.
Connection latency
session.joined and the session.join response may list capabilities. When
the list contains connection.ping, the gateway answers a connection.ping
command with an empty data object immediately, without touching presence or
other state. Send it only when the capability is listed: older gateways close
the connection on an unknown command.
The SDK's probeLatency() sends one ping when the gateway supports it and the
client is ready, and reports the round trip through
subscribeHeartbeatLatency. Without the capability, that subscription keeps
reporting the presence heartbeat, which runs every 25 seconds. Probe only while
the latency is on screen instead of on a fixed fast interval.
Disconnection
Mark data as stale when the connection is lost. The SDK restores subscriptions after reconnecting; fetch the current state over HTTP at that point. Do not assume you will receive every event from the outage.
A busy gateway rejects session.join with a retryable error that carries
retryAfterMs. Wait at least that long before you reconnect or join again. The
SDK applies this delay to its next reconnect.
If you register setReconnectHandler, it replaces the default session restore.
The handler still runs when a synchronous connection-state observer starts a
join. Coordinate these callbacks so one owns the join, and use the handler for
any additional recovery work. Without a custom handler, a synchronous observer's
join suppresses the default join to avoid duplicate startup requests.
Authorization must be renewed at least every 60 seconds. Deleting the key, losing access, or failing to renew blocks further communication. Do not retry indefinitely when a connection is rejected because of an invalid key.