Skip to contentLootlog Developers
Docs

Contract changelog

A reference for HTTP, realtime, and game client integrations.

Unreleased (breaking): Game client API v2 lists online characters

window.lootlogGameClientApi now reports apiVersion: 2. Only the online-players contract changed:

  • getOnlinePlayers() results and online-players:changed events carry players as a PublicOnlinePlayer[] sorted by name, instead of a map from Discord ID to sessions.
  • Entries no longer include discordId, sessionId, platform, status, guildId, characterId, or accountId. Character fields moved from player to the entry itself. Web app sessions, which have no character, are left out.

To migrate, accept apiVersion === 2 and read result.players as an array. Timers and Organizations are unchanged.

Public battle links no longer return the uploader's userId from GET /battles/public/{battleId}.

Unreleased (breaking): Party gatherings belong to one Lootlog

A party gathering is created in exactly one Lootlog (Discord guild) and is visible only to its members.

  • POST /messaging/party-gathering accepts guildId. The deprecated guildIds is still accepted with exactly one element; send one of the two fields, not both. Two or more Lootlogs, or neither field, is a 400.
  • POST /messaging with isGatheringParty: true requires exactly one guildIds element; more is a 400. Ordinary NPC notifications still accept up to ten Lootlogs.
  • A Lootlog the caller cannot create gatherings in is a 403, instead of being dropped from a list of targets.
  • Responses and realtime events keep guildIds; a new gathering carries one element. Gatherings created before this release can carry several until they expire, at most 30 minutes later.

Deploy the API before the game client. Older game clients that send one Lootlog keep working; when they target several, creation fails instead of broadcasting the gathering. No persistence migration is required.

Unreleased (breaking): One error body, filter and page shape across the main API

Every main API operation now fails, filters and pages the same way, as described in errors and limits. Release the SDK as a breaking version.

  • Error bodies are { code, message?, details? }. Bodies that were empty, { statusCode }, { message } with a machine reason, or carried extra top-level fields now put the reason in code and the extra values in details. Validation failures move issues to details.issues.
  • A 429 sends Retry-After instead of retryAfterMs in the body.
  • npcId in PUT /guilds/{guildId}/lootlog-config/{npcId} is an integer; a non-integer is a 400 instead of a 404.
  • Lists that paged by offset or returned a bare array return { items, nextCursor } and take an opaque cursor.
  • GET /guilds/{guildId}/events/{eventId}/kill-history returns its page in items instead of data, next to the unchanged kind, member and nextCursor. A malformed or differently scoped cursor fails with code INVALID_CURSOR, and a limit outside 1 to 100 or a memberId that is not a positive integer is a 400. Cursors issued before this release are rejected; restart pagination without a cursor.
  • GET /guilds/{guildId}/loots returns { items, nextCursor } instead of a bare array, newest first. cursor is the opaque nextCursor of the previous page instead of a loot id, and is valid only with the filters and limit it was issued for; any other cursor fails with code INVALID_CURSOR. nextCursor is null on the last page.
  • Loot filters: an unknown npcTypes or rarities value is a 400 instead of a 500, a level range whose minimum exceeds its maximum is a 400, search is trimmed and limited to 100 characters, and % and _ in it match literally.
  • npcTypes on GET /guilds/{guildId}/loots/stats is a repeated parameter instead of a comma-separated string; an unknown type is a 400 instead of being ignored. world on loot endpoints, including GET /users/@me/stats/loots, is trimmed and lower-cased.
  • A loot comment's content is trimmed and must be 1 to 256 characters. Its member adds id, always carries avatar and roles, and a role's color is always present (null when unset).
  • The loot.snapshot realtime event carries each submission's member with id and a non-optional avatar, as GET /guilds/{guildId}/loots does.
  • activeOnly on GET /guilds/{guildId}/events accepts only true or false; any other value is a 400. Omitting it still lists active events.
  • world on event and timer endpoints, including the world of a new event, is trimmed and lower-cased, and an empty world is a 400. The same applies to the world written by POST /loots, POST /kills and the timer create and reset requests, so a record is always found by the filter that names its world.
  • NPC level filters are named npcLevelMin and npcLevelMax everywhere.
  • A Member inside another resource is always member: { id, name, avatar, userId }, with avatar set to null when there is none.
  • Kill statistics (/guilds/{guildId}/stats/kills and its top-npcs, top-killers, npcs/{npcId}/killers and members/{memberId} reads, and /users/@me/stats/kills, /users/@me/kills/npcs): minLvl and maxLvl become npcLevelMin and npcLevelMax, and a minimum above the maximum is a 400. period also accepts 90d and 180d, and an omitted period now means the last 7d instead of all time; send period=all for all-time statistics. An unknown npcTypes or npcType is a 400, search is trimmed, limited to 100 characters and matches % and _ literally, and world is trimmed and lower-cased.
  • limit on top-npcs and top-killers is optional, defaults to 10 and 5, and must be 1 to 100. topNpcsLimit on /users/@me/stats/kills must be 1 to 100.
  • GET /users/@me/kills/npcs returns { items, nextCursor, total } instead of { npcs, pagination }. GET /guilds/{guildId}/stats/kills/members/{memberId} replaces pagination with top-level total and nextCursor. On both, cursor is the opaque nextCursor of the previous page instead of an offset, and is valid only with the filters, sort and period it was issued for; any other cursor fails with code INVALID_CURSOR.
  • Kill rankings (memberRanking, top-killers entries, NPC killers) and the member of members/{memberId} carry member: { id, name, avatar, userId } instead of memberId, memberName, memberAvatar and memberUserId.

Unreleased: Public item and NPC catalog

The Search API adds two read-only endpoints that work without an API key or a session: GET /public/catalog/items and GET /public/catalog/npcs. They serve game facts only. No player, world or Organization appears in a response, and neither does when Lootlog first saw an item or NPC, which would date boss kills.

  • Each item lists droppedBy, up to 20 NPCs it dropped from. A fight's drop counts for its only NPC, or for its only boss (elite II or III, hero, event hero, titan or colossus) when it fought a group.
  • Each droppedBy entry carries retired: true when the NPC no longer drops the item, such as after a rework replaced its loot. An entry is retired when Lootlog has recorded the NPC's loot far too often since the item's last drop for the gap to be chance at the item's own drop rate. Lootlog records a fight only when it drops something above common, so the rate is per recorded fight, not per kill. Lootlog recomputes it daily, and a new drop clears it at once.
  • Each NPC lists locations: the maps it is fought on, most fought first, each with its share of the NPC's latest 1000 recorded fights, from 0 to 1 and rounded to a hundredth. Maps under 1% of those fights are left out, so maps players only carry loot to stay off the list, while a hero keeps every map it spawns on. Lootlog recomputes them daily, so an NPC first seen today may have none yet.
  • Each item carries elasticLevel: true when Margonem gives it the level of the character who loots it, as Lootlog saw it dropped at more than four levels. Its lvl and stat then describe one of those levels, the one most recently seen. minLevel and maxLevel find an elastic item whenever the range reaches into the levels it comes at, 40 to 300.
  • Items sort by lvl, name or id:desc, which lists the items Margonem added most recently first. They filter by ids, rarities, types, professions, minLevel, maxLevel, droppedBy (an NPC key), dropSourceTypes, statKeys (a listed set of stats, all required; etiquette marks event items), excludedStatKeys (stats an item must not have), event (an event's name and year as a slug, such as wakacje-2026 or dzien-mlodego-smoka) and legendaryBonuses (any of the listed bonuses, such as verycrit or curse); NPCs filter by ids, keys and types. Both endpoints accept gameVersion (pl or en) to list one edition. limit is capped at 96.
  • Requests share the public rate limit of the main API's /public routes.

Unreleased (breaking): Gains and best days removed from personal kill analytics

GET /users/@me/stats/kills/analytics no longer returns npcGains or the bestDay of each entry in npcs. Release the SDK as a breaking version.

  • npcs lists up to 500 monsters killed in the period, most killed first, instead of the top 20. Each entry still has deltaKills and deltaPercent; there is no separate ranking by growth.
  • records.bestDay, records.bestWeek and records.bestMonth for the whole period are unchanged.
  • The SDK's UserKillAnalyticsResponseDtoOutput loses npcGains, and its npcs items lose bestDay.

Unreleased: Kill numbers in event kill history

GET /guilds/{guildId}/events/{eventId}/kill-history returns killNumber on every event and member summary. It is the one-based position of the kill among all recorded kills of its hero in the event, ordered by killedAt and then id, so the hero's first kill is 1.

  • Manual window closes are counted like other kills.
  • The heroId and memberId filters do not renumber kills: a member's list shows the hero's kill numbers, with gaps for kills the member missed.

Unreleased (breaking): Deprecated event kill lists removed

The event, member and hero kill lists deprecated in favour of GET /guilds/{guildId}/events/{eventId}/kill-history are removed. There is no compatibility window: once this API version is deployed, the removed routes return 404, including for SDK 0.1.1 and earlier and any other client still calling them. Migrate to /kill-history before the deployment. Release the SDK as a breaking version.

Removed GET routeReplacement query on /kill-history
/guilds/{guildId}/events/{eventId}/killsOptional heroId
/guilds/{guildId}/events/{eventId}/members/{memberId}/killsmemberId, optionally heroId
/guilds/{guildId}/events/{eventId}/heroes/{heroId}/killsheroId
  • UUID cursors from the removed lists are not accepted by /kill-history. Restart pagination without a cursor and pass its nextCursor unchanged.
  • Lists return summaries with participantCount. Read participants, scoring and map presence from GET /guilds/{guildId}/events/{eventId}/heroes/{heroId}/kills/{killId} and its /timeline; both are unchanged.
  • The SDK no longer exports eventsRankingControllerGetEventKillHistory, eventsRankingControllerGetMemberKillHistory, eventsRankingControllerGetHeroKillHistory, their URL helpers and parameter and error types, EventKillHistoryResponseDto or EventMemberKillHistoryResponseDto. Use listEventKillHistory.

Unreleased (breaking): Time-ordered battle IDs and opaque list cursors

Battle IDs are now UUIDv7 values whose first 48 bits are the battle's createdAt, so newer battles have greater IDs. Release the SDK as a breaking version.

  • Every existing battle has a new ID. Battle, list, timeline, analytics and POST /battles responses return the new ID. Paths that take a battle ID, including /battles/public/{battleId}, still accept the old ID and return the battle with its new ID. Store the new ID: old IDs keep resolving only until the timelines of those battles are removed from object storage.
  • GET /battles/@me cursors are opaque. Pass nextCursor or previousCursor back unchanged; do not build or parse them. Cursors issued before this change, and any other malformed cursor, are rejected with 400 instead of returning the first page.
  • previousCursor is returned whenever hasPrev is true. The previous page holds the size battles directly before the current page. Battles saved since the first page was read appear on the pages before it, and hasPrev stays true until the newest battle is reached.
  • createdAt is unchanged: the time Battlelog saved the battle, with millisecond precision. startDate and endDate filters compare against it.
  • Raw battle data (/battles/{battleId}/raw and its public variant) keeps its shape. For battles saved after this change, timestamp is the battle's createdAt.

Unreleased (breaking): Battle participants no longer return id

Battlelog identifies a participant by its battle and Margonem character, so the separate participant ID is removed. Release the SDK as a breaking version.

  • Battle, battle list, public battle and timeline responses no longer return warriors[].id. Identify a participant by battleId and originalId; originalId is unique within a battle.
  • statistics is unchanged. Battlelog now derives it from the participants when it reads a battle instead of storing it, with the same values.
  • POST /battles rejects a fight event whose f.w key differs from the warrior's originalId with 400. Margonem keys player warriors by their ID, so payloads captured from the game are unaffected.

Unreleased: Relative NPC icons on loots, kills and timers

Margonem stores an NPC icon relative to its NPC image directory and adds the CDN address only to render it. Some clients report the rendered URL instead, so one observation of an NPC produced two revisions.

  • POST /loots, POST /kills and POST /timers/auto are unchanged. The API now stores npcs[].icon and npc.icon relative to the NPC image directory: https://micc.garmory-cdn.cloud/obrazki/npc/her/a.gif, //micc.garmory-cdn.cloud/obrazki/npc/her/a.gif, /obrazki/npc/her/a.gif and /her/a.gif are all stored as her/a.gif, and identify the same NPC revision.
  • A one-time repair rewrites stored icons of NPC revisions, kill statistics, timers, timer history and event heroes the same way. NPC revisions that differed only in the icon form are merged; a loot can therefore link a different NPC revision id with the same name, level and type.
  • Loot, kill, timer and NPC search responses return the stored icon. Build the image URL by prefixing https://micc.garmory-cdn.cloud/obrazki/npc/. Search documents written earlier can still hold a full URL; use an absolute icon as is.

Unreleased (breaking): Game version required, deprecated search fields removed

The one-time repair of per-edition revisions is complete: every loot and NPC search hit has a game version. The compatibility paths kept for the rollout are removed. Release the SDK as a breaking version.

  • POST /loots requires gameVersion (pl or en). A loot without it, or with null, is rejected with 400. The API no longer derives an edition from world. Derive it from the game page's hostname; when the host is not a recognized edition domain, do not submit the loot.
  • /npcs and /items no longer declare the world query parameter. The service ignores it if an older client still sends it. On /all, world still filters players.
  • NPC hits no longer return world; use worlds. gameVersion on NPC hits is no longer nullable.
  • Notification rule responses, including the rule of a watched item, no longer return unresolvedSelections. The selections it would have reported were never recorded, so the array was always empty.

Unreleased: NPC and item revisions per game edition

Every world of a Margonem edition uses the same NPC and item ids. NPC revisions and search results are now one per edition instead of one per world, and every loot has a game version.

  • POST /loots: send gameVersion with every loot, derived from the game page's hostname. Omitting it, or sending null, is deprecated, and a later breaking release will reject such loots. Until then the API uses the edition of world: cronus, husaria and steamrealm are en, every other world is pl. A value you send always wins.
  • NPC search (/npcs, and the NPC hits of /all) returns one hit per game version, identity namespace, id and Margonem type, with a new worlds list of the edition's worlds where the NPC was observed. world on an NPC hit is deprecated: it names the world of the latest observation. gameVersion is always set; the field stays nullable for compatibility.
  • The world query parameter of /npcs and /items is deprecated and ignored, and on /all it only filters players. A later breaking release removes it from NPC and item search. Item hits were already one per edition, id and name with a worlds list.
  • A one-time repair gives existing loots and revisions an edition. Loot links from the other edition move to that edition's revision; an item link takes that edition's name and icon, or keeps its stored name when the edition has none. Loot, item and NPC ids are unchanged.

Some saved notification selections do not identify what their rule matches: timer rules saved with an NPC id from the legacy search catalog, and watched items whose saved name came from the other game edition.

  • Notification rule responses, including the rule of a watched item, add unresolvedSelections. Each entry names the kind (npc or item), the saved selectedId and selectedName, a reason, and for a timer rule an optional suggestedId and suggestedName. The array is empty for most rules.
  • The API never changes a rule's filters to resolve an entry. It disappears when the rule is saved without that id, or when the watched item is added again. Treat suggestedId as a suggestion; applying it is a normal rule update.
  • Saving or rebuilding a timer rule no longer schedules notifications for timers whose spawn window has already closed, and does not send a notification again for an occurrence that was already sent.
  • A one-time repair moves some historical loot links to new NPC and item revisions: an NPC level observed on timers of the same edition at the time of the loot, or the name and icon of the loot's own edition. A loot's NPC level can therefore change, and with it which roles can read the loot. Loot and item ids are unchanged.

Unreleased: Item names, icons and instance stats per loot

A loot item now shows the name and icon it was reported with. Earlier, a later loot of an item with equal stats reused the first stored name and icon, so a Polish loot could show an English name, and an icon change was lost.

  • Loot item stat includes that loot's own created, gold, amount and opis values. Earlier it repeated the values of the first loot stored with the same stats. Loot items accepted before this change keep their stored stat.
  • Item search can return several hits with the same id: one per game version and name, such as a Polish and an English name of one item. Deduplicate by id and name rather than by id alone.
  • POST /loots is unchanged. Send name, icon and stat exactly as the game shows them; do not translate them.

Margonem runs a Polish edition on *.margonem.pl and an international edition on *.margonem.com. The same template or item id can mean different things in each edition, and a world name does not identify the edition.

  • POST /loots accepts an optional nullable gameVersion: pl or en. Derive it from the game page's hostname: the edition domain or any subdomain of it. Send null, or omit the field, when the host is not a recognized edition domain; never default to pl. The API validates the value but cannot verify where the client ran, so do not treat it as proof of anything else.
  • Retrying a loot, or submitting one another client already reported, keeps the game version it was first accepted with.
  • NPC search hits add a nullable gameVersion. Equal ids with different game versions are different NPCs. See "NPC and item revisions per game edition" for how the API now fills a missing game version.

Deploy the database migration, the search service, the API and the game client in that order. Older clients keep working and their loots stay unknown.

Unreleased: Timer NPC search follows timer visibility

GET /guilds/{guildId}/timers/npcs/search now applies the same visibility as the timer list. Timers whose NPC type or level is outside the caller's roles are omitted before limit is applied, so they no longer reveal an NPC's identity, location or respawn configuration.

  • world is optional. Without it, the search covers every world of the Organization, and each result adds world.
  • search is optional when npcIds or templateIds is present. Both accept repeated positive integers: npcIds matches a timer's runtime NPC id and templateIds matches its template id. A request needs at least one of search, npcIds or templateIds; otherwise it returns 400.
  • Manual timers remain excluded.

Existing callers that send search and world keep their behavior, except that hidden timers no longer appear. Generated clients include the new parameters and field for the next SDK release.

Unreleased: Explicit NPC runtime and template identities

Margonem gives every NPC spawn a runtime id (npc.id, the battle originalId) and every monster template a template id (npc.tpl). Several spawns share one template.

  • POST /loots accepts optional npcs[].runtimeId and nullable npcs[].templateId. When either is present, npcs[].id is ignored for identity; keep sending it for older API revisions. Send templateId: null when the template is unknown; never substitute the runtime id.
  • POST /timers/auto accepts optional nullable npc.templateId. npc.id remains the runtime id and still identifies the timer, so spawns sharing a template keep separate timers.
  • Timer responses, timer history and GET /guilds/{guildId}/timers/npcs/search add templateId, which is null until a client reports it. A later submission without it keeps the stored value.
  • Notification rules accept npcTemplateIds, at most five together with npcId and npcIds. A template id matches every timer observed with that template; npcId and npcIds still match a timer's own id.
  • Search NPC hits add identityNamespace: template, runtime (a spawn observed without a template) or legacy (an overloaded id from an older client). Equal ids in different namespaces are different NPCs.

Deploy the database migration, the search service, the API, the web app and the game client in that order. Older clients keep working and write legacy identities. Existing saved filters and legacy records are not remapped.

Unreleased: Conditional party observations

POST /messaging/party-gathering/{notificationId}/party-observation accepts an optional positive integer expectedRevision. Use the latest room projection's revision to prevent a delayed observation from overwriting a newer roster. When supplied, a stale revision or a concurrent write returns 409 REVISION_CONFLICT. A successful conditional observation advances the room revision even if the roster is unchanged and was observed moments ago. Requests without the field keep their existing behavior, including returning the current room without a new revision for an unchanged roster observed within 45 seconds.

After a conflict or an uncertain HTTP result, read the room again and retry the latest roster with its current revision. Bound both the requests and retry count; aborting HTTP does not cancel a server write. Deploy the API to every writer before relying on the precondition. No persistence migration is required. Generated clients include the optional field for the next SDK release.

Unreleased: Event hero lists keep history

PATCH /guilds/{guildId}/events/{eventId} no longer rebuilds the event's heroes and maps when the request includes heroNpcs. Heroes are matched by npcName and maps by mapId, so retained heroes and maps keep their IDs, kills, kill points, tracking history and ranking entries. The list can add heroes, add maps, rename maps and set a hero's npcId; omitting npcId keeps the stored value. As with the map create operation, a map added to an existing hero starts with an open unassigned coverage gap.

A heroNpcs list that omits an existing hero or map now returns 400 and changes nothing. Previously the omitted records were deleted with their history. Repeated hero names, or a repeated mapId within one hero, also return 400. Remove a hero with DELETE /guilds/{guildId}/events/{eventId}/heroes/{heroId} and a map with DELETE /guilds/{guildId}/events/{eventId}/heroes/{heroId}/maps/{mapId}.

Deleting a hero removes its maps, kills, kill points and tracking history, and now also its ranking entries, so event ranking totals match the remaining kill points. No migration is required.

Unreleased: Event kill history

GET /guilds/{guildId}/events/{eventId}/kill-history adds a paginated summary of kills visible to the caller. It requires LOOTLOG_EVENTS_READ and supports read-only API keys within their Organization scope. Optional query parameters are heroId, memberId, cursor and limit. The limit is an integer from 1 to 100 and defaults to 20.

The response contains kind, data and nextCursor. kind is event unless memberId is supplied, when it is member. Summaries include hero identity, timestamps, manual closure status and participantCount. This count includes all saved participant point records, including participants with wasPresent: false. Member summaries also include the selected member and their scoring and tracking data. Lists omit map presence, roles and other participants' full records. Existing detail and timeline endpoints remain available.

Treat the new cursor as opaque and pass it unchanged with the same Organization, event and filters. Results use killedAt DESC, id DESC; the cursor remains usable if its boundary kill is deleted. Invalid cursors, mismatched filters and invalid limits return 400. Each request applies current access rules. Pagination does not freeze a snapshot of the history.

The existing event, hero and member kill-list endpoints are deprecated. Their response shapes and UUID cursors remain supported with corrected chronological pagination. A missing, hidden or out-of-scope UUID cursor returns the same 400 Invalid cursor response. Inaccessible resources return 404.

Apply the database index migration and deploy the API before migrating clients to the new endpoint. The migration changes indexes only. Generated clients include the endpoint for the next SDK release; SDK publication remains a separate release step.

Unreleased — Realtime latency probe

The realtime v1 session.joined event and session.join response add an optional capabilities list. Gateways that list connection.ping accept the connection.ping command and answer it without side effects, including on integration connections. The SDK realtime client adds probeLatency(), which reports through subscribeHeartbeatLatency.

Deploy the gateway before relying on the probe. Clients send the command only when the capability is listed, so clients connected to older gateways keep the heartbeat measurement. No migration is required.

Unreleased — Notification schedules and role level ranges

Notification rule create and update requests accept scheduledUntil: null. For a recurring rule, null removes the end date; omitting the field preserves it. Switching to a one-time schedule clears recurring fields, including the end date. Send only fields relevant to the selected schedule type. For daily and weekly schedules, omit scheduledAt to calculate the next occurrence from the selected time, weekday and timezone when the schedule changes.

Role update requests reject lvlRangeFrom and lvlRangeTo values that are not integers from 0 to 500, and ranges where lvlRangeTo is lower than lvlRangeFrom, with 400 Bad Request. A role saved earlier with a reversed range must receive a corrected range on its next update.

Deploy the API before clients begin sending null. Existing valid requests remain compatible, and no database migration is required. Generated SDK types include these changes for the next SDK release.

2026-09-17 — SDK 0.1.1

Documentation update with runnable organization, timer and loot examples, filtering, cursor pagination and error handling. No runtime or API contract changes; no migration is required.

2026-09-17 — SDK 0.1.0

First npm release of @lootlog/sdk:

  • Typed HTTP functions for the main, activity, battlelog and search APIs.
  • A shared realtime client for server-side integrations.
  • Public OpenAPI JSON exports and bundled TypeScript declarations.

Install with npm install @lootlog/sdk or bun add @lootlog/sdk. See the SDK guide for configuration and usage. This package release does not change the game client API version or introduce new HTTP endpoints.

Portal baseline

Documentation for the public game client API v1, an HTTP reference in Scalar, personal API keys, and an SDK for server integrations. The development environment supports verification before production releases.

The public game client object retains apiVersion: 1. Published contract changes will be documented here with migration instructions. Check the reference generated from service schemas for current HTTP operation parameters.