Skip to contentLootlog Developers
Docs

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.